Skip to content

fix(flags): make exact boolean matching explicit - #90694

Merged
trunk-io[bot] merged 11 commits into
masterfrom
fix/local-eval-boolean-exact-semantics
Sep 4, 2026
Merged

trunk-io[bot] merged 11 commits into
masterfrom
fix/local-eval-boolean-exact-semantics

Conversation

@marandaneto

@marandaneto marandaneto commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Problem

People can receive unexpected feature flag results when an exact filter contains boolean-like scalar values or multiple options.

The released matcher evaluates aggregate truthiness before array membership. This produces results such as:

Filter Property Released exact Version 2 exact Product analytics exact
false \"banana\" true false false
false 0 true false false
[\"true\", \"false\"] \"true\" false true true
[\"true\", \"false\"] \"pro\" true false false
[] true true true true
[] [] true true true

Changing is_not false globally could expand existing flag audiences. The new behavior therefore defaults to released semantics and rolls out per team.

Current flow:

flowchart TD
    A[Exact or is_not filter] --> B[Use released truthiness behavior]
    classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000;
    classDef phRed fill:#f54e00,stroke:#f54e00,color:#fff;
    class A phYellow;
    class B phRed;
Loading

Proposed flow:

flowchart TD
    A[Team property matching version] --> B{Version}
    B -->|1 or missing| C[Use released truthiness behavior]
    B -->|2| D[Use explicit exact matching]
    A --> E[Expose version to local-evaluation SDKs]
    C --> F[Live flags and batch cohort evaluation]
    D --> F
    classDef phBlue fill:#1d4aff,stroke:#1d4aff,color:#fff;
    classDef phYellow fill:#f9bd2b,stroke:#f9bd2b,color:#000;
    classDef phGray fill:#e5e7eb,stroke:#c7ccd1,color:#000;
    class A phYellow;
    class B phBlue;
    class C,D,E,F phGray;
Loading

Changes

  • Teams retain released matching until staff selects property matching version 2.
  • Version 2 compares scalar values without truthiness coercion and matches non-empty arrays by member equality.
  • Empty arrays preserve released /flags behavior, and is_not remains the complement of exact.
  • Live and batch evaluation read the same durable team setting.
  • Local-evaluation responses and team metadata expose the selected version for SDK discovery.
  • Static cohort generation pins both flag and matching versions across pages, then stops on either conflict.
  • Mechanical: add a versioned team setting, staff API validation, audit data, cache refreshes, and generated API types.
  • Mechanical: add an additive smallint migration whose database default preserves version 1 for older writers.

How did you test this code?

  • Regression coverage verifies released and explicit scalar, array, direct flag, dynamic cohort, and batch behavior.
  • Cohort tests verify every page uses one matching version and version conflicts are not retried.
  • Metadata and local-evaluation tests verify configured, default, cached, and no-flags response paths.
  • Staff API tests verify updates, validation, audit behavior, and cache refresh tasks.
  • uv run pytest products/feature_flags/backend/test/test_local_evaluation.py products/feature_flags/backend/test/test_staff_team_config_api.py products/feature_flags/backend/test/test_team_feature_flags_config.py posthog/storage/test/test_team_metadata_cache.py posthog/api/test/test_flags_service.py products/feature_flags/backend/api/test/test_feature_flag.py::TestCohortGenerationForFeatureFlag --reuse-db -q
  • cargo test -p feature-flags --test test_batch_flag_evaluation
  • cargo test -p feature-flags --lib test_match_properties
  • cargo test -p feature-flags --lib test_evaluate_dynamic_cohorts
  • cargo check -p feature-flags --all-targets
  • cargo clippy -p feature-flags --all-targets -- -D warnings
  • cargo fmt --all -- --check
  • uv run mypy --cache-fine-grained .
  • hogli build:openapi
  • hogli ci:preflight --fix

Automatic notifications

  • Publish to changelog?

Docs update

The Rust feature-flags service overview documents the durable property matching version and compatibility behavior.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Pi addressed reviewer feedback using /address-pr-comments, /django-migrations, /improving-drf-endpoints, /writing-tests, /writing-code-comments, /writing-user-facing-copy, /writing-pr-descriptions, and /running-ci-preflight.

The rollout moved from a process-local allowlist to durable team configuration so remote, local, and paginated evaluations can stay aligned. The duplicate search found no competing fix. All examples are invented.

@marandaneto marandaneto self-assigned this Aug 28, 2026
@trunk-io

trunk-io Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

😎 Merged successfully - details.

@marandaneto
marandaneto requested a review from a team August 28, 2026 05:53
@posthog-project-board-bot posthog-project-board-bot Bot moved this to In Progress in Feature Flags Aug 28, 2026
@github-actions

github-actions Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

⚠️ Trunk lane — backend Python lane

This PR is assigned to the backend Python lane. It runs backend Python tests and may merge in parallel with PRs in other lanes.

✅ Complexity (TypeScript) — clean

Cyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions.

⚠️ Bundle size — 🔺 +409 B (+0.0%)

Uncompressed size of every built .js bundle, compared against the base branch.

Total: 68.29 MiB · 🔺 +409 B (+0.0%)

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

✅ Eager graph — within budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

Root Eager (shipped) Δ vs base Budget
entry (logged-out pages, app bootstrap)
src/index.tsx
1.45 MiB · 22 files no change ███░░░░░░░ 32.3% of 4.51 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
8.90 MiB · 3,268 files 🔺 +8 B (+0.0%) █████████░ 91.6% of 9.71 MiB

🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 [object Object] stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx
🟢 [object Object] stays out of src/scenes/AuthenticatedShell.tsx

Largest files eagerly shipped from src/index.tsx
Size File
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js
4.5 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
3.9 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.4 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.3 KiB src/RootErrorBoundary.tsx
912 B ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js
854 B src/scenes/ChunkLoadErrorBoundary.tsx
762 B src/index.tsx
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
309.3 KiB ../node_modules/.pnpm/posthog-js@1.427.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
275.9 KiB ../node_modules/.pnpm/posthog-js@1.427.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
267.7 KiB ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
255.2 KiB src/taxonomy/core-filter-definitions-by-group.json
154.2 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js
104.4 KiB src/lib/api.ts
95.8 KiB ../packages/quill/packages/quill/dist/index.js
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js

Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of #32479

✅ Toolbar bundle — eager 2.28 MiB within budget

What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.

Metric Size Δ vs base Budget
Eager (shipped)
entry + static imports
2.28 MiB · 18 files no change ████░░░░░░ 39.8% of 5.72 MiB
Deferred (lazy) 2.11 MiB · 45 files no change n/a — loads on demand
Loader dist/toolbar.js 1.1 KiB no change █░░░░░░░░░ 5.8% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
754.5 KiB dist/toolbar/toolbar-app-UB54KCUA.css
602.9 KiB dist/toolbar/chunk-chunk-PSSB2NES.js
484.7 KiB dist/toolbar/chunk-chunk-EL6RQSMS.js
135.6 KiB dist/toolbar/chunk-chunk-FV2N34MB.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
71.3 KiB dist/toolbar/toolbar-app-73KR5GY4.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-LTYHHK34.js
21.0 KiB dist/toolbar/chunk-chunk-JPI7DKFZ.js
6.8 KiB dist/toolbar/chunk-chunk-DV7IWQNF.js

Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile

✅ Dist folder size — 🔺 +1.1 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 1459.04 MiB · 🔺 +1.1 KiB (+0.0%)

ℹ️ MCP UI apps size — 33 app(s), 17665.2 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 600.0 KB 195.2 KB
action 458.2 KB 195.2 KB
action-list 564.9 KB 195.2 KB
cohort 457.2 KB 195.2 KB
cohort-list 563.8 KB 195.2 KB
email-template 457.0 KB 195.2 KB
error-details 472.9 KB 195.2 KB
error-issue 457.9 KB 195.2 KB
error-issue-list 564.7 KB 195.2 KB
experiment 562.0 KB 195.2 KB
experiment-list 565.6 KB 195.2 KB
experiment-results 567.1 KB 195.2 KB
feature-flag 567.6 KB 195.2 KB
feature-flag-list 571.4 KB 195.2 KB
feature-flag-testing 461.4 KB 195.2 KB
inline-scan 457.7 KB 195.2 KB
insight-actors 563.0 KB 195.2 KB
invite-email-preview 456.4 KB 195.2 KB
llm-costs 560.0 KB 195.2 KB
session-recording 459.0 KB 195.2 KB
survey 458.8 KB 195.2 KB
survey-global-stats 562.7 KB 195.2 KB
survey-list 565.5 KB 195.2 KB
survey-stats 562.7 KB 195.2 KB
trace-span 457.6 KB 195.2 KB
trace-span-list 564.7 KB 195.2 KB
vision-observation-list 563.9 KB 195.2 KB
workflow 457.5 KB 195.2 KB
workflow-list 564.2 KB 195.2 KB
loops-review 461.9 KB 195.2 KB
query-results 756.3 KB 195.2 KB
render-ui 839.5 KB 195.2 KB
visual-review-snapshots 462.0 KB 195.2 KB
✅ Playwright — all passed

All tests passed.

View test results →

⚠️ Backend snapshots — 1 updated (1 modified, 0 added, 0 deleted)

Query snapshots: Backend query snapshots updated

Changes: 1 snapshots (1 modified, 0 added, 0 deleted)

What this means:

  • Query snapshots have been automatically updated to match current output
  • These changes reflect modifications to database queries or schema

Next steps:

  • Review the query changes to ensure they're intentional
  • If unexpected, investigate what caused the query to change

Review snapshot changes →

⚠️ Django migration SQL — 1 new migration to review

We've detected new migrations on this PR. Review the SQL output for each migration:

products/feature_flags/backend/migrations/0017_teamfeatureflagsconfig_property_matching_version.py

BEGIN;
--
-- Add field property_matching_version to teamfeatureflagsconfig
--
ALTER TABLE "feature_flags_teamfeatureflagsconfig" ADD COLUMN "property_matching_version" smallint DEFAULT 1 NOT NULL;
COMMIT;

Last updated: 2026-09-04 12:38 UTC (cde54f2)

✅ Django migration risk — migration analysis complete

We've analyzed your migrations for potential risks.

Summary: 1 Safe | 0 Needs Review | 0 Blocked

✅ Safe

Brief or no lock, backwards compatible

feature_flags.0017_teamfeatureflagsconfig_property_matching_version
  └─ #1 ✅ AddField
     Adding NOT NULL field with constant default (safe in PG11+)
     model: teamfeatureflagsconfig, field: property_matching_version

Last updated: 2026-09-04 12:39 UTC (cde54f2)

ℹ️ Docs preview — preview build triggered

Docs from this PR will be published at posthog.com.

Project Preview Updated (UTC)
posthog.com Open preview Sep 4, 2026, 12:35 PM

The preview should be ready in about 10 minutes. Open the preview at /handbook/engineering/.

@marandaneto
marandaneto marked this pull request as ready for review August 28, 2026 06:09
@marandaneto
marandaneto requested a review from a team August 28, 2026 06:09
@posthog-project-board-bot posthog-project-board-bot Bot moved this from In Progress to In Review in Feature Flags Aug 28, 2026
@marandaneto

Copy link
Copy Markdown
Member Author

found while checking PostHog/sdk-specs#51

@trunk-io

trunk-io Bot commented Aug 28, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

View Full Report ↗︎ ⋅ Docs

@greptile-apps

greptile-apps Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(flags): make exact boolean matching ..." | Re-trigger Greptile

@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "fix(flags): make exact boolean matching ..." | Re-trigger Greptile

@marandaneto

Copy link
Copy Markdown
Member Author

maybe i should rollback the changes for [] so it'd keep as is and match the current prod analytics as well

Comment thread rust/feature-flags/src/config.rs Resolved
@github-actions

github-actions Bot commented Sep 4, 2026 •

Copy link
Copy Markdown
Contributor

🦔 Hogbox preview · ✅ ready

▶ Open the preview

🔑 Login test@posthog.com / 12345678 (demo data)
🧩 Running this PR's backend and frontend, on the PostHog :master base
🔗 Link stable across rebuilds — a re-push swaps the box underneath, the URL stays
🔒 Access tailnet only (PostHog VPN)
🛠️ Admin inspect & debug state in hogland
💤 Idle sleeps after ~30 min idle (snapshot to S3, zero node cost) and wakes on your next visit in ~30s, behind a brief "waking up" screen

commit cde54f2 · box box-29d1529bb225 · ready in 856s (push → usable) · build log · rebuilds on every push, torn down on close

Comment thread posthog/api/services/flags_service.py Resolved
@github-actions
github-actions Bot requested a deployment to preview-pr-90694 September 4, 2026 12:26 In progress
@trunk-io
trunk-io Bot merged commit 3315c11 into master Sep 4, 2026
260 of 261 checks passed
@trunk-io
trunk-io Bot deleted the fix/local-eval-boolean-exact-semantics branch September 4, 2026 14:18
@github-project-automation github-project-automation Bot moved this from Approved to Done in Feature Flags Sep 4, 2026
@deployment-status-posthog

deployment-status-posthog Bot commented Sep 4, 2026 •

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-09-04 15:00 UTC Run
prod-us ✅ Deployed 2026-09-04 15:21 UTC Run
prod-eu ✅ Deployed 2026-09-04 15:20 UTC Run

This branch was successfully deployed

1 active deployment
preview-pr-90694 — cde54f24 Deployed Sep 4, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

4 participants