Repository navigation
fix(flags): fall back for incompatible local regex evaluation - #387
Merged
Merged
Conversation
Contributor
posthog-go Compliance ReportDate: 2026-10-23 10:41:27 UTC ✅ All Tests Passed!111/111 tests passed Capture_V1 Tests✅ 94/94 tests passed View Details
Feature_Flags Tests✅ 17/17 tests passed View Details
|
Contributor
There was a problem hiding this comment.
PostHog Review · Standard · reviewed 0e4b57d: 2 Should fix.
This is the only automatic review while this pull request is a draft. Comment @posthog review to review new changes sooner. Otherwise, a review of the new changes runs when you mark it ready for review.
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
ioannisj
approved these changes
Oct 23, 2026
eli-r-ph
pushed a commit
that referenced
this pull request
Oct 25, 2026
* fix(flags): fall back for incompatible local regex evaluation * fix(flags): preserve regex fallback priority and bound repetition scans * fix(flags): bound malformed escape scans and reject reversed counts * fix(flags): defer malformed regex repetitions (cherry picked from commit e8c7d37)
3 of 5 tasks
eli-r-ph
pushed a commit
that referenced
this pull request
Oct 28, 2026
* fix(flags): fall back for incompatible local regex evaluation * fix(flags): preserve regex fallback priority and bound repetition scans * fix(flags): bound malformed escape scans and reject reversed counts * fix(flags): defer malformed regex repetitions (cherry picked from commit e8c7d37)
eli-r-ph
pushed a commit
that referenced
this pull request
Oct 29, 2026
* fix(flags): fall back for incompatible local regex evaluation * fix(flags): preserve regex fallback priority and bound repetition scans * fix(flags): bound malformed escape scans and reject reversed counts * fix(flags): defer malformed regex repetitions (cherry picked from commit e8c7d37)
eli-r-ph
added a commit
that referenced
this pull request
Oct 29, 2026
…390) * fix(flags): load shared definitions without a secret key (#366) * fix(flags): load shared definitions without a secret key * fix(flags): make keyless cache consumers read-only (cherry picked from commit 88d8397) * chore(deps): bump the github-actions group with 2 updates (#373) Bumps the github-actions group with 2 updates: [github/codeql-action/init](https://github.com/github/codeql-action) and [github/codeql-action/analyze](https://github.com/github/codeql-action). Updates `github/codeql-action/init` from 4.38.2 to 4.38.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@2892aa5...24c5418) Updates `github/codeql-action/analyze` from 4.38.2 to 4.38.3 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@2892aa5...24c5418) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.38.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.38.3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 155480d) * chore(deps): bump google.golang.org/grpc from 1.83.1 to 1.83.2 in /otel (#377) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.83.1 to 1.83.2. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](grpc/grpc-go@v1.83.1...v1.83.2) --- updated-dependencies: - dependency-name: google.golang.org/grpc dependency-version: 1.83.2 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 863b330) * chore(deps): bump github.com/klauspost/compress from 1.17.11 to 1.18.7 in /posthogmcpsdk in the go_modules group across 1 directory (#379) chore(deps): bump github.com/klauspost/compress Bumps the go_modules group with 1 update in the /posthogmcpsdk directory: [github.com/klauspost/compress](https://github.com/klauspost/compress). Updates `github.com/klauspost/compress` from 1.17.11 to 1.18.7 - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](klauspost/compress@v1.17.11...v1.18.7) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.7 dependency-type: indirect dependency-group: go_modules ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 5c2220b) * fix(mcp): set $exception_source and strip reserved exception metadata (#357) * fix(mcp): set $exception_source and correct mechanism.synthetic The mcp-analytics spec requires the $exception emitted alongside a failed tool call to carry $exception_source: "mcp.tool_call", and to mark mechanism.synthetic only for an isError result that carried no thrown error. buildException set neither correctly: the source property was absent, and synthetic was hardcoded true on a path only reached for a thrown error. Also strip $exception_source from caller-supplied properties, matching how $exception_level is already protected. Generated-By: PostHog Desktop Task-Id: b4b6b8cc-da38-4c24-a978-9e2e9b5ac5f0 * fix(mcp): strip reserved exception metadata * fix(mcp): keep mechanism.synthetic true until the adapter can mark synthesized errors --------- Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Ioannis J <yiannis@posthog.com> (cherry picked from commit de638c7) * fix(flags): match explicit null properties with string operators (#381) (cherry picked from commit 870af8a) * feat(flags): honor experiment holdouts in local flag evaluation (#360) * feat(flags): honor experiment holdouts in local flag evaluation Local evaluation ignored `filters.holdout`, so an identifier inside an experiment's holdout was bucketed into a regular variant and disagreed with the server, silently and with no fallback. Holdout resolution now runs after the inactive-flag check and before release conditions, returning the synthetic `holdout-<id>` variant. Membership uses the flag-level bucketing identity (group key for group-aggregated flags, device id for device-bucketed flags, distinct id otherwise) and the server's hash of `holdout-<value>`: the "-" separator, not the "." used by rollout hashing, selects a different population. Percentages are clamped to 0-100 without truncating fractions, compared inclusively, and 100 includes everyone without hashing. A holdout missing `id` or `exclusion_percentage` is skipped, leaving ordinary evaluation unchanged. Tested: go build, go vet, gofmt, make api-diff, and go test -race -count=1 ./... at the repository root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: 2887d167-f4e8-446a-a056-202893f6ecd9 * fix(flags): keep device-bucketed partial holdouts inconclusive without a device id --------- Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Ioannis J <yiannis@posthog.com> (cherry picked from commit d170fc5) * chore(deps): bump the github-actions group with 4 updates (#385) Bumps the github-actions group with 4 updates: [github/codeql-action/init](https://github.com/github/codeql-action), [github/codeql-action/analyze](https://github.com/github/codeql-action), [actions/create-github-app-token](https://github.com/actions/create-github-app-token) and [pnpm/setup](https://github.com/pnpm/setup). Updates `github/codeql-action/init` from 4.38.3 to 4.39.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@24c5418...6f2ac81) Updates `github/codeql-action/analyze` from 4.38.3 to 4.39.0 - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](github/codeql-action@24c5418...6f2ac81) Updates `actions/create-github-app-token` from 3.2.0 to 3.3.0 - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Changelog](https://github.com/actions/create-github-app-token/blob/main/CHANGELOG.md) - [Commits](actions/create-github-app-token@bcd2ba4...64ab729) Updates `pnpm/setup` from 3.0.0 to 3.1.1 - [Release notes](https://github.com/pnpm/setup/releases) - [Commits](pnpm/setup@fbda4c8...a088197) --- updated-dependencies: - dependency-name: github/codeql-action/init dependency-version: 4.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: github/codeql-action/analyze dependency-version: 4.39.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: actions/create-github-app-token dependency-version: 3.3.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions - dependency-name: pnpm/setup dependency-version: 3.1.1 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: github-actions ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 6ffac5a) * feat(otel): add per-request context to span processor (#371) * feat: span processor request context * fix: span processor review feedback * fix: span processor review follow-ups (cherry picked from commit c203229) * chore(deps): bump github.com/klauspost/compress from 1.17.11 to 1.18.7 in /otel (#380) chore(deps): bump github.com/klauspost/compress in /otel Bumps [github.com/klauspost/compress](https://github.com/klauspost/compress) from 1.17.11 to 1.18.7. - [Release notes](https://github.com/klauspost/compress/releases) - [Commits](klauspost/compress@v1.17.11...v1.18.7) --- updated-dependencies: - dependency-name: github.com/klauspost/compress dependency-version: 1.18.7 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> (cherry picked from commit 460c7e6) * chore: release v1.35.0 [version bump] [skip ci] (cherry picked from commit 5b7997a) * chore: release v1.35.1 [version bump] [skip ci] (cherry picked from commit 4139880) * chore: release v1.35.2 [version bump] [skip ci] (cherry picked from commit 87a8371) * chore: release v1.35.3 [version bump] [skip ci] (cherry picked from commit 8b65bef) * chore: release v1.36.0 [version bump] [skip ci] (cherry picked from commit 4f9ba34) * build!: release the otel bridge in lockstep with the core major The otel bridge now imports the core SDK for the request context, so it moves to github.com/posthog/posthog-go/otel/v2 and shares the core release version, like posthogmcpsdk. The release workflow sets both nested modules' core requirement to the new version and tags them after the core release. Both nested modules run the pinned-core build check through one script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: require signed commits in contributor docs (#388) Contributors often open PRs with unsigned commits because signing isn't documented here. (cherry picked from commit 56aa91d) * fix(flags): fall back for incompatible local regex evaluation (#387) * fix(flags): fall back for incompatible local regex evaluation * fix(flags): preserve regex fallback priority and bound repetition scans * fix(flags): bound malformed escape scans and reject reversed counts * fix(flags): defer malformed regex repetitions (cherry picked from commit e8c7d37) * feat(otel): add opt-in exception trace context (#374) * feat(otel): add opt-in exception trace context Generated-By: PostHog Desktop Task-Id: 5681be44-4986-48db-b140-1bda88f75f96 * fix(otel): preserve request trace overrides for enqueue clients Generated-By: PostHog Desktop Task-Id: 5681be44-4986-48db-b140-1bda88f75f96 (cherry picked from commit 729bcf7) * fix(otel)!: add derived exception trace IDs only as a pair Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * chore: release v1.36.1 [version bump] [skip ci] (cherry picked from commit aef332c) * chore: release v1.36.2 [version bump] [skip ci] (cherry picked from commit 32f2522) * chore: release v1.37.0 [version bump] [skip ci] (cherry picked from commit 5d73532) * docs: note non-finite numbers and default trace IDs in the v2 migration guide Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: give the non-finite number and trace ID notes their own sections Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: v2 sends non-finite numbers as null (#394) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(otel): add request context to application spans (#375) * feat(otel): add request context to application spans Generated-By: PostHog Desktop Task-Id: 5681be44-4986-48db-b140-1bda88f75f96 * fix: stop context enrichment after processor shutdown Generated-By: PostHog Desktop Task-Id: 5681be44-4986-48db-b140-1bda88f75f96 * fix: skip span context for disabled clients Generated-By: PostHog Desktop Task-Id: bfd18848-e498-4352-a58a-b9ba85de4198 (cherry picked from commit 94ff576) * chore: release v1.38.0 [version bump] [skip ci] (cherry picked from commit b0a9fae) * fix(otel): build the context span processor against the v2 core Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(otel): say which trace IDs can mix Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: Dustin Byrne <dustin@posthog.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com> Co-authored-by: Ioannis J <yiannis@posthog.com> Co-authored-by: Manoel Aranda Neto <5731772+marandaneto@users.noreply.github.com> Co-authored-by: Carlos Marchal <carlos@posthog.com> Co-authored-by: releaser-posthog-go[bot] <250614987+releaser-posthog-go[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Anna Garcia <11654201+turnipdabeets@users.noreply.github.com> Co-authored-by: Paul D'Ambra <paul@posthog.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
💡 Motivation and Context
Go can return a definitive local flag result that disagrees with the flags service. For example, RE2's
^\d+$does not match١, while the service's Unicode-aware regex does. Lookarounds, backreferences and class-local backspace also compile on the service but not in Go. Their compilation failures previously becamefalse, nil, which skipped remote fallback and could become a match through cohort negation.This adapts the agreed local-subset and remote-fallback approach from JS #5354 and its cross-SDK discussion to Go's RE2 engine. It does not depend on that PR being merged or released.
The regex cache now retains capability metadata alongside compiled patterns. Unsupported syntax, differing set and repetition parsing, repeated assertions, and Unicode or vertical-tab inputs with RE2 shorthand classes use the existing server-required error path. Word boundaries also defer because they can exhaust the service's backtracking budget even on short ASCII inputs.
GetFeatureFlagcan then request a remote result. Unsupported regex now preserves the existingRequiresServerEvaluationErrorcategory through matcher, cohort and dependency boundaries. This prevents an unresolved earlier regex from being replaced by a later catch-all variant. Ordinary inconclusive or missing-property conditions still allow independently matching later rules, as required by the evaluator contract. Local-only unsupported-regex calls remain unresolved, including through negated cohorts.Repetition candidates are scanned forward without repeatedly searching or splitting the remaining suffix. This bounds the reviewed braces-and-commas case without changing the regex cache. Unterminated braced x/p/P escapes now stop classification at the first missing close and reach the existing compiler categorization once. Bare malformed p/P patterns stay definitively invalid, while unsupported verbose-mode carriers still defer. Proven reversed uint32 repetition bounds such as
a{2,1}now fail both operators without a remote request. RE2 size caps, overflow and leading-zero uncertainty still require the server. There is no negative cache or new parser framework. Known invalid shared syntax still fails both regex operators. Negative operators only negate a valid match. Ordinary ASCII patterns, escaped punctuation, supported flags, strict anchors, Unicode property classes and scalar matching remain local. Empty patterns, explicit null matching and existing stringification are preserved. This reduces local coverage conservatively without adding an engine, dependency or public option. It does not claim universal regex, Unicode-table or resource-budget equivalence.Malformed unescaped brace candidates such as
a{x},a{,a{2 }anda{1,2 }now require a decimal lower bound and a closing}before local compilation is eligible. Valid{n},{n,m}and{n,}forms remain eligible, as do escaped and class-local literal braces. Contract limitation relative to the local evaluator spec: the spec requests definitive false for invalid regex syntax; these ambiguous brace forms conservatively defer to the service instead, because Go accepts them as literals. Local-only evaluation remains unresolved rather than returning the wrong definitive boolean.💚 How did you test it?
Added regressions before production changes and recorded RED failures for wrong local booleans and skipped fallback. Focused compatibility tests now pass for both operators, direct flags, cohorts, negated cohorts and local-only calls. Only the remote request boundary is stubbed. Missing-property and portable-pattern controls verify the fallback gate is not always on or always off.
Compared both operators against the frozen actual-service oracle across 4,868 expanded vectors, 19 review vectors and 72 additional probes. All definitive results agreed. These corpora overlap and are finite.
Added ordered two-variant regressions before the feedback fix. They returned
secondlocally for^a\b$againstabefore the change. They now reach remote fallback, or remain server-required in local-only mode, through direct, cohort, nested-cohort and dependency paths. Missing-context, safe-local and invalid-pattern controls remain local as appropriate.A standard Go benchmark for 4,000 opening braces followed by 4,000 commas and
}allocated about 262 MB/op and took 145-149 ms/op before the scanner fix. It now allocates 0 B/op and 0 allocations/op. Steady measurements were about 6.8-7.0 microseconds/op on this machine, with approximately linear scaling at 1,000/2,000/4,000 braces. Timings are observations, not unit-test thresholds or a universal complexity guarantee.Added actual-service-verified reversed-bound regressions before the fix. Both operators previously requested remote evaluation or returned unresolved locally. They now return definitive false for a single rule and permit the later ordered variant, including local-only calls. Large valid counts and native nested-count limits still defer.
Benchmarked repeated unterminated x/p/P escapes and an unsupported
(?x)#comment carrier at 12/48/192 KiB, both classification and actual property matching. At 192 KiB the old classifier medians were about 64-74 ms/op. Steady fixed medians were about 2 microseconds/op for classification, 4-5 microseconds for x/comment matching, and 124-135 microseconds for p/P matching including native parsing. This is bounded, machine-specific evidence rather than a universal regex latency guarantee. Semantic category, class and escape controls pass before and after.After merging latest
origin/main, rango test -mod=readonly -count=1 -run '^TestRegexCompatibility' ., full rootmake buildwith race tests and vet,make api-diff, gofmt and diff checks. All passed on Go 1.26.4, macOS arm64.Completed diff-scoped deslop and test-audit Authoring checks. Final branch autoreview passed on committed HEAD
c8126ce3c3c4ad09ffc4bc8930c98efc5cbd17e4againstorigin/mainat56aa91d692651b853c9c9857517a9cda077392a8with no actionable findings.Follow-up review fix (
680ba36): added regressions before production edits for malformed brace patterns, both operators, remote/local-only evaluation, negated cohorts and ordered variants through nested cohorts and dependencies. They failed with definitive local results and zero remote calls before the fix; the same commands and full compatibility suite pass afterward. Retained escaped/class-local brace and valid repetition controls. Re-ranmake build(root race tests, vet and build),make api-diff, gofmt and diff checks on Go 1.26.4/macOS arm64; all passed. The existing repetition-candidate benchmark still reports zero allocations and now stops at the first unsupported brace.The minimum Go version and other platforms were not run locally. Separate
otelandposthogmcpsdkmodule checks were not run because this changes neither their code nor the core public API. No live-service tests were used. GitHub CI results are separate from these local checks.📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file. The equivalentposthog-gopatch Changeset is included directly. No version was bumped.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
The operator directed this Go port and its publication. Pi assisted with implementation and test-audit using Git, GitHub CLI and Go tooling. An isolated Pi autoreview checked the final committed branch. The implementation keeps RE2 and uses existing remote fallback rather than adding another engine or translating unsupported patterns. Human review is still required.