Skip to content

chore(experiments): drop the exposure-event rollout flag check - #106333

Closed
posthog[bot] wants to merge 12 commits into
masterfrom
posthog-self-driving/choreexperiments-remove-the-fully-ee3bbb
Closed

posthog[bot] wants to merge 12 commits into
masterfrom
posthog-self-driving/choreexperiments-remove-the-fully-ee3bbb

Conversation

@posthog

@posthog posthog Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Every experiment result query for a post-cutoff experiment evaluated a feature flag that has been fully rolled out since 2026-09-01, on the hot path.
  • Worse than the wasted work: the check ran with only_evaluate_locally, and an unresolved local evaluation silently fell back to $feature_flag_called. The same experiment could be counted on either event depending on whether local definitions happened to be loaded.
  • The flag was a rollout switch, not a kill switch: 100% rollout, no targeting conditions, no dependents, no scheduled changes, not tied to an experiment or a survey.

Origin

  • Scout: a custom scout
  • First signal: 2026-09-24
  • Inbox report: open
  • Task started by: auto-start, after the report was rated P4 and ready to fix

Changes

  • Experiments started at or after the cutoff always count exposures on $experiment_exposure. resolve_default_exposure_event decides from the start date, with no flag evaluation on the query path.
  • The cutoff is now the EXPERIMENT_EXPOSURE_EVENT_CUTOFF setting, an ISO 8601 value from the environment. It defaults to 2026-09-01 UTC, when PostHog Cloud started the copy, and Cloud does not override it.
  • Ingestion now copies multivariate $feature_flag_called into $experiment_exposure for all teams on every deployment: self-hosted, local dev and tests. Before, only Cloud set EXPERIMENT_EXPOSURE_DUPLICATION_TEAMS, and new self-hosted experiments would have counted an event that never arrived.
  • generate_experiment_data drops its --exposure-event option, because ingestion now makes the copy and the option would count every exposure twice.
  • The exposure criteria panel still labels a new draft $experiment_exposure, and now passes that event explicitly instead of reading it from a flag.
  • resolvedExposureEvent keeps $feature_flag_called as its fallback. This is the one bullet with behavioral risk, so it is worth checking: the fallback fires for any payload missing resolved_exposure_event, and ExperimentBasicSerializer omits the field, so list-sourced experiments hit it. A pre-cutoff experiment must not be named on the new event, so only the creation form overrides the fallback.
  • Mechanical: team became unused and is dropped from resolve_default_exposure_event, resolve_exposure_config_for_builder, and get_exposure_config_params_for_builder; the FEATURE_FLAGS entry, the EXPERIMENT_EXPOSURE_EVENT_FLAG constant and a test mock that only served the flag are gone; the resolved_exposure_event help_text drops its team-rollout clause, and the generated OpenAPI types follow it; tests read the cutoff from settings; comments and the session replays skill that described the gate as a rollout now describe the cutoff.

Warning

A self-hosted install starts making $experiment_exposure when it upgrades to this change. With the default cutoff, an experiment it started between 2026-09-01 and the upgrade counts only the exposures after the upgrade. Setting EXPERIMENT_EXPOSURE_EVENT_CUTOFF to the upgrade time, for example 2026-10-15T00:00:00Z, keeps those experiments on $feature_flag_called.

The PostHog flag itself is untouched. It can be archived once this deploys.

Nothing looks different on Cloud. On a self-hosted install, the exposure criteria panel now names $experiment_exposure for a new draft, which matches what the backend counts.

How did you test this code?

  • Ran the products/experiments/backend suite and the experiments Jest suites locally. CI reports the counts with more authority, so they are not repeated here.
  • Backend test fixtures needed real changes, not just deletions: several tests built a running experiment (start date = now, past the cutoff) and seeded $feature_flag_called. They passed before only because an unpatched posthoganalytics.feature_enabled returned nothing in tests, which is exactly the fallback this PR removes. _expose_person in the freeze test now defaults to $experiment_exposure, and the two cases that mean "the legacy event specifically" say so explicitly.
  • The parameterized *_flag_disabled cases are removed rather than replaced. They asserted a branch that no longer exists; the cutoff cases they sat beside still cover both events.
  • exposureContract.test.ts covers the fallback three ways: no field, caller-supplied override, and a server-resolved value winning over both. The regression it catches is a pre-cutoff experiment being named on $experiment_exposure.
  • No frontend fixture was edited to accommodate this change. Three fixtures asserting $feature_flag_called pass unmodified, which is the check on the fallback bullet above.
  • TestResolveDefaultExposureEvent overrides the cutoff and checks both sides of it. It catches a resolver that ignores the setting, which would put a self-hosted install's older experiments on an event it did not have yet.
  • A local run of products/experiments/backend fails 92 tests, and the same 92 fail on a clean master worktree. The causes are local: ClickHouse lacks the aggregate_funnel_array UDF, and data warehouse snapshots carry a different storage path. No other test fails.
  • Not checked: no manual browser testing, no run on a hobby install, no repo-wide mypy run, and the frontend typecheck run in this sandbox still reports two pre-existing @posthog/hogvm module errors in an unrelated debug scene.

Release status

  • No feature flag controls this change
  • This change is behind a feature flag and is not available to users
  • This change makes a previously flagged feature available to everyone

Automatic notifications

  • Publish to changelog?

Docs update

None in this repository. No doc under docs/ lists self-hosted environment variables, so EXPERIMENT_EXPOSURE_EVENT_CUTOFF is documented by the comment on the setting and the warning above.

🤖 Agent context

Autonomy: Fully autonomous

Agent: Claude Code, Opus 5

Skills invoked: /cleaning-up-stale-feature-flags (which set the code-first ordering and the "retained path" rule), /writing-pr-descriptions, and the repo's Simplified Technical English guidance.

Decisions along the way:

  • The flag's definition and history were read before editing, per the skill: fully rolled out boolean, so the retained path is the enabled one. The originating report described the flag as stable since its creation date; it actually reached 100% on 2026-09-01, which is the same day as EXPERIMENT_EXPOSURE_EVENT_CUTOFF. That does not change the fix.
  • Dropping the now-unused team parameter was a judgment call. It widens the diff to a dozen call sites but avoids leaving plumbing that exists only to feed a deleted flag check.
  • An earlier commit in this PR moved resolvedExposureEvent to fall back to $experiment_exposure, on the assumption that only a local draft can lack the field. That was wrong, for the reason given under Changes. Three test fixtures needing a field they never carried was the signal, and the fallback is now back to the legacy event. An earlier comment on this PR describes that intermediate state and is obsolete.
  • default_exposure_event stays in the session bucket and delta cache keys. It follows the start date and the EXPERIMENT_EXPOSURE_EVENT_INGESTED setting, and the setting can change while an entry is warm.
  • CodeRabbit flagged the stale resolved_exposure_event help_text, which feeds the generated OpenAPI schema. Fixed, and the generated types were regenerated with it.
  • The two scenes-app-signals-artefactloglist visual diffs came from a stale baseline, not from this PR, which touches no signals code. The branch predated #106388, which pins the date in those stories. Master is merged in, so the next run rebuilds them against the pinned story.
  • No duplicate: gh pr list --state open --search over open PRs (drafts included) found no PR removing this flag.
  • CodeRabbit CLI: not available in this environment, so this PR opens without a local review pass.
  • Public artifact: nothing here comes from a non-public source. The flag key, the event names, and the cutoff constant were already in this repository.

Follow-up commits: Claude Code, Claude Opus 5.5 (claude-opus-5-5), directed by a reviewer who asked for a master merge, a review and fixes.

Skills invoked: /writing-tests, /writing-code-comments, /writing-skills, /reviewing-with-coderabbit, /writing-pr-descriptions.

  • Master is merged in. Two conflicts, in experiment_query_builder.py and exposure_query_logic.py, kept this PR's resolver and master's newer docstring.
  • The review found the self-hosted gap: the flag returned false off Cloud, and Cloud alone duplicates the event, for all teams since before the cutoff.
  • A first fix kept self-hosted installs on $feature_flag_called through an EXPERIMENT_EXPOSURE_EVENT_INGESTED setting. The reviewer chose a different design: copy the event on every deployment, and let self-hosted installs move the cutoff instead. That setting is gone.
  • CodeRabbit CLI, --deep, ran on the first fix with one finding: keep the resolved event in the session bucket cache key, because a setting can change it. Fixed. The point still holds for the configurable cutoff.

Created with PostHog Desktop from this inbox report.

The experiment-exposure-event flag reached full rollout, so resolve_default_exposure_event
no longer evaluates it. A post-cutoff experiment now always counts exposures on
$experiment_exposure, decided from the experiment start date alone.

This also removes the silent inconsistency the gate carried: the check ran with
only_evaluate_locally, so an unresolved local evaluation quietly returned the legacy event
on the query hot path.

The `team` argument becomes unused and is dropped from resolve_default_exposure_event and
the two builder helpers that only threaded it through. The frontend gate and its
FEATURE_FLAGS entry go with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 565b560e-1c84-4501-9b76-73bf9f72f117
@posthog posthog Bot added the self-driving label Sep 24, 2026
@trunk-io

trunk-io Bot commented Sep 24, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here

@github-actions

github-actions Bot commented Sep 24, 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) — 1 function above the limit (max 12)

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.

Function Location Complexity Limit
ExposureCriteriaPanel frontend/src/scenes/experiments/ExperimentForm/ExposureCriteriaPanel.tsx:243 12 10
⚠️ Duplication (Python) — 2 new duplicated blocks (worst 168 tokens)

New Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

First copy Second copy Lines Tokens
products/experiments/backend/hogql_queries/test/test_experiment_mean_metric_events_preaggregation.py:35 products/experiments/backend/hogql_queries/test/test_experiment_retention_metric_events_preaggregation.py:64 25 168
products/experiments/backend/hogql_queries/test/test_experiment_funnel_metric_events_preaggregation.py:54 products/experiments/backend/hogql_queries/test/test_experiment_mean_metric_events_preaggregation.py:36 25 164
✅ Duplication (TypeScript) — clean

New TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying.

🚨 Comment density — 21% of added code lines are comments (40 of 187)

This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out.

Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See .agents/skills/writing-code-comments/SKILL.md for the house rules.

Files with the most added comment lines:

File Comment lines Added lines
posthog/settings/ingestion.py 7 14
frontend/src/scenes/experiments/exposureContract.ts 4 6
products/experiments/backend/session_buckets.py 4 5
products/experiments/backend/session_event_deltas.py 4 5
products/experiments/backend/test/test_freeze_exposure_clickhouse.py 3 19
frontend/src/scenes/experiments/ExperimentForm/ExposureCriteriaPanel.tsx 2 3
nodejs/src/ingestion/config.ts 2 3
products/experiments/backend/hogql_queries/exposure_query_logic.py 2 13

This check does not block merging. It updates on every push and clears when the share drops.

⚠️ Bundle size — 🔺 +691.6 KiB (+1.0%)

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

Total: 70.77 MiB · 🔺 +691.6 KiB (+1.0%)

File Size Δ vs base
exporter/src/lib/hog.js 435.1 KiB 🔺 +435.1 KiB (new)
posthog-app/_parent/products/canvas/frontend/scene/CanvasScene.js 54.6 KiB 🔺 +54.6 KiB (new)
posthog-app/_parent/products/canvas/frontend/sidePanel/CanvasSidePanel.js 51.4 KiB 🔺 +51.4 KiB (new)
exporter/_parent/products/posthog_ai/frontend/scenes/TaskTracker/TaskTracker.js 77.3 KiB 🔺 +33.9 KiB (+78.2%)
posthog-app/_parent/products/posthog_ai/frontend/scenes/TaskTracker/TaskTracker.js 79.6 KiB 🔺 +28.4 KiB (+55.3%)
posthog-app/_parent/products/tasks/frontend/spaces/SpaceScene.js 87.0 KiB 🔺 +20.4 KiB (+30.7%)
render-query/src/render-query/render-query.js 20.23 MiB 🔺 +14.6 KiB (+0.1%)
posthog-app/src/scenes/web-analytics/WebAnalyticsScene.js 253.7 KiB 🔺 +10.2 KiB (+4.2%)
posthog-app/src/scenes/views/Views.js 9.8 KiB 🔺 +9.8 KiB (new)
posthog-app/_parent/products/ai_observability/frontend/aiObservabilityColumnRenderers.js 5.1 KiB 🔺 +5.1 KiB (new)
posthog-app/_parent/products/canvas/frontend/newCanvas/CanvasNewScene.js 4.2 KiB 🔺 +4.2 KiB (new)
posthog-app/src/scenes/AuthenticatedShell.js 320.3 KiB 🔺 +3.9 KiB (+1.2%)
posthog-app/_parent/products/visual_review/frontend/scenes/VisualReviewRunScene.js 54.2 KiB 🟢 -3.6 KiB (-6.3%)
posthog-app/_parent/products/replay_vision/frontend/replay_scanners/ReplayScannersScene.js 101.5 KiB 🔺 +3.4 KiB (+3.4%)
posthog-app/_parent/products/mcp_analytics/frontend/MCPAnalyticsScene.js 187.8 KiB 🔺 +3.0 KiB (+1.6%)
exporter/_parent/products/ai_observability/frontend/aiObservabilityColumnRenderers.js 3.0 KiB 🔺 +3.0 KiB (new)
posthog-app/_parent/products/workflows/frontend/Broadcasts/BroadcastScene.js 84.0 KiB 🔺 +2.5 KiB (+3.0%)
exporter/src/exporter/scenes/ExporterNotebookScene.js 3.70 MiB 🔺 +2.4 KiB (+0.1%)
posthog-app/_parent/products/signals/frontend/inbox/InboxScene.js 483.2 KiB 🔺 +1.6 KiB (+0.3%)
posthog-app/_parent/products/autoresearch/frontend/AutoresearchPipelineScene.js 54.7 KiB 🔺 +1.5 KiB (+2.8%)
exporter/_parent/products/alerts/frontend/views/EditAlertModal.js 114.1 KiB 🔺 +1.4 KiB (+1.2%)
posthog-app/_parent/products/feature_flags/frontend/staff/FeatureFlagsStaffToolsScene.js 60.3 KiB 🔺 +1.2 KiB (+2.0%)
posthog-app/src/lib/hog.js 1.0 KiB 🔺 +1.0 KiB (new)

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.62 MiB · 22 files 🔺 +9.2 KiB (+0.6%) █████████░ 88.0% of 1.84 MiB
logged-out boot: index + App + bootApp (preloaded by every page, including /login)
src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
3.58 MiB · 630 files 🔺 +13.7 KiB (+0.4%) █████████░ 88.8% of 4.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
7.76 MiB · 2,488 files 🔺 +165.9 KiB (+2.1%) █████████░ 93.1% of 8.34 MiB
dashboard scene
src/scenes/dashboard/Dashboard.tsx
11.68 MiB · 4,476 files (no base measurement) █████████░ 86.7% of 13.48 MiB
project home scene
src/scenes/project-homepage/ProjectHomepage.tsx
14.45 MiB · 5,366 files (no base measurement) █████████░ 87.9% of 16.44 MiB
events scene
src/scenes/activity/explore/EventsScene.tsx
10.91 MiB · 4,135 files (no base measurement) █████████░ 86.3% of 12.64 MiB
replay detail scene
src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
13.80 MiB · 5,057 files (no base measurement) █████████░ 87.8% of 15.72 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/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 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
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts 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
🟢 [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/index.tsx
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
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
219.9 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
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
100.5 KiB src/lib/api.ts
92.0 KiB src/products.tsx
69.4 KiB src/lib/lemon-ui/icons/icons.tsx
40.1 KiB src/lib/utils/eventUsageLogic.ts
38.7 KiB ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js
33.9 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
28.7 KiB src/scenes/scenes.ts
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
279.9 KiB src/taxonomy/core-filter-definitions-by-group.json
219.9 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
153.7 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
110.0 KiB ../packages/quill/packages/quill/dist/index.js
100.5 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
92.0 KiB src/products.tsx
90.6 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
Largest files eagerly shipped from src/scenes/dashboard/Dashboard.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
279.9 KiB src/taxonomy/core-filter-definitions-by-group.json
219.9 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
181.8 KiB src/queries/validators.js
153.7 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
110.0 KiB ../packages/quill/packages/quill/dist/index.js
100.5 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/project-homepage/ProjectHomepage.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
279.9 KiB src/taxonomy/core-filter-definitions-by-group.json
219.9 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
181.8 KiB src/queries/validators.js
153.7 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
110.0 KiB ../packages/quill/packages/quill/dist/index.js
100.5 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/activity/explore/EventsScene.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
279.9 KiB src/taxonomy/core-filter-definitions-by-group.json
219.9 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
181.8 KiB src/queries/validators.js
153.7 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
110.0 KiB ../packages/quill/packages/quill/dist/index.js
100.5 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
Largest files eagerly shipped from src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
Size File
315.5 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
306.2 KiB ../node_modules/.pnpm/posthog-js@1.435.5_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
279.9 KiB src/taxonomy/core-filter-definitions-by-group.json
219.9 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
181.8 KiB src/queries/validators.js
153.7 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
110.0 KiB ../packages/quill/packages/quill/dist/index.js
100.5 KiB src/lib/api.ts
93.3 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/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.20 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.20 MiB · 19 files 🔺 +8.5 KiB (+0.4%) ████░░░░░░ 38.4% of 5.72 MiB
Deferred (lazy) 2.10 MiB · 44 files no change n/a — loads on demand
Loader dist/toolbar.js 1.2 KiB no change █░░░░░░░░░ 6.0% of 19.5 KiB
Largest eagerly-shipped chunks
Size File
832.8 KiB dist/toolbar/toolbar-app-Z7UAQKGH.css
657.2 KiB dist/toolbar/chunk-chunk-ADXIJEJ4.js
259.4 KiB dist/toolbar/chunk-chunk-XDFV6AJ3.js
138.2 KiB dist/toolbar/chunk-chunk-IJ6DVG7Q.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
75.2 KiB dist/toolbar/toolbar-app-XJWG3ZHP.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
35.6 KiB dist/toolbar/chunk-chunk-5CU3M5BS.js
21.0 KiB dist/toolbar/chunk-chunk-7DSGG7S4.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 — 🔺 +4.67 MiB (+0.5%)

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

Total: 966.77 MiB · 🔺 +4.67 MiB (+0.5%)

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

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

App JS CSS
debug 597.9 KB 199.4 KB
action 454.1 KB 199.4 KB
action-list 564.2 KB 199.4 KB
cohort 453.1 KB 199.4 KB
cohort-list 563.2 KB 199.4 KB
email-template 452.9 KB 199.4 KB
error-details 469.6 KB 199.4 KB
error-issue 454.5 KB 199.4 KB
error-issue-list 564.8 KB 199.4 KB
experiment 561.3 KB 199.4 KB
experiment-list 564.9 KB 199.4 KB
experiment-results 566.3 KB 199.4 KB
feature-flag 566.8 KB 199.4 KB
feature-flag-list 570.5 KB 199.4 KB
feature-flag-testing 457.3 KB 199.4 KB
inline-scan 453.6 KB 199.4 KB
insight-actors 562.3 KB 199.4 KB
invite-email-preview 452.3 KB 199.4 KB
llm-costs 559.3 KB 199.4 KB
session-recording 455.3 KB 199.4 KB
survey 454.7 KB 199.4 KB
survey-global-stats 561.9 KB 199.4 KB
survey-list 564.9 KB 199.4 KB
survey-stats 561.9 KB 199.4 KB
trace-span 453.5 KB 199.4 KB
trace-span-list 564.1 KB 199.4 KB
vision-observation-list 563.3 KB 199.4 KB
workflow 453.4 KB 199.4 KB
workflow-list 563.5 KB 199.4 KB
loops-review 457.8 KB 199.4 KB
query-results 774.1 KB 199.4 KB
render-ui 858.1 KB 199.4 KB
visual-review-snapshots 457.9 KB 199.4 KB
⚠️ Playwright — 1 flaky

🎭 Playwright report · View test results →

⚠️ 1 flaky test:

  • Creating a SQL insight with a variable and overriding it on a dashboard (chromium)

These issues are not necessarily caused by your changes.
Annoyed by this section? Help fix flakies and failures and it will go green!

⚠️ Backend coverage — 96.0% of changed backend lines covered — 2 uncovered

🧪 Backend test coverage

Patch coverage — changed backend lines (products + core): ███████████████████░ 96.0% (64 / 66)

File Patch Uncovered changed lines
posthog/settings/ingestion.py 60.0% 170–171

🤖 Agents: add a test only if an uncovered line exposes a realistic regression that existing tests miss. Otherwise explain why no new test is needed under "How did you test this code?". Gap list: the patch-coverage artifact on this run (gh run download 177721791857876 -n patch-coverage), or the coverage-data block at the end of this comment.

Per-product line coverage (touched products)
Product Coverage Lines
platform_features ██░░░░░░░░░░░░░░░░░░ 12.1% 7 / 58
demo ███████████░░░░░░░░░ 53.4% 1,445 / 2,707
data_tools ████████████░░░░░░░░ 61.2% 90 / 147
warehouse_sources_queue █████████████░░░░░░░ 65.9% 1,611 / 2,446
ai_gateway ███████████████░░░░░ 75.0% 9 / 12
aeo ███████████████░░░░░ 76.3% 617 / 809
batch_exports ████████████████░░░░ 81.2% 21,571 / 26,552
apm █████████████████░░░ 84.1% 1,306 / 1,553
cdp ██████████████████░░ 88.3% 4,559 / 5,164
ml_inference ██████████████████░░ 88.8% 539 / 607
mcp_analytics ██████████████████░░ 89.2% 5,038 / 5,651
product_tours ██████████████████░░ 89.3% 1,340 / 1,500
dashboards ██████████████████░░ 89.6% 6,924 / 7,727
notebooks ██████████████████░░ 90.2% 15,304 / 16,971
signals ██████████████████░░ 90.4% 59,704 / 66,064
cohorts ██████████████████░░ 90.5% 8,534 / 9,434
data_warehouse ██████████████████░░ 90.6% 14,375 / 15,860
streamlit_apps ██████████████████░░ 90.8% 2,684 / 2,956
managed_warehouse ██████████████████░░ 91.0% 10,252 / 11,263
today ██████████████████░░ 91.1% 891 / 978
data_modeling ██████████████████░░ 91.2% 10,562 / 11,584
tasks ██████████████████░░ 91.3% 79,044 / 86,610
exports ██████████████████░░ 91.7% 9,684 / 10,566
business_knowledge ██████████████████░░ 92.0% 8,447 / 9,181
engineering_analytics ██████████████████░░ 92.2% 11,497 / 12,475
ai_training ██████████████████░░ 92.2% 356 / 386
conversations ███████████████████░ 92.6% 29,137 / 31,467
early_access_features ███████████████████░ 92.6% 1,339 / 1,446
managed_migrations ███████████████████░ 92.7% 1,581 / 1,705
stamphog ███████████████████░ 92.8% 8,109 / 8,742
visual_review ███████████████████░ 92.9% 9,534 / 10,265
canvas ███████████████████░ 92.9% 7,155 / 7,703
approvals ███████████████████░ 93.0% 3,974 / 4,271
mcp_registry ███████████████████░ 93.1% 1,670 / 1,794
notifications ███████████████████░ 93.2% 1,144 / 1,228
error_tracking ███████████████████░ 93.2% 16,370 / 17,557
surveys ███████████████████░ 93.4% 6,644 / 7,113
slack_app ███████████████████░ 93.6% 14,560 / 15,557
autoresearch ███████████████████░ 93.6% 8,837 / 9,442
context_layer ███████████████████░ 93.8% 3,415 / 3,639
web_analytics ███████████████████░ 93.9% 23,680 / 25,229
billing_alerts ███████████████████░ 94.1% 2,094 / 2,226
mcp_store ███████████████████░ 94.3% 8,959 / 9,501
ai_observability ███████████████████░ 94.6% 24,974 / 26,409
wizard ███████████████████░ 94.7% 6,150 / 6,496
alerts ███████████████████░ 94.7% 9,321 / 9,845
workflows ███████████████████░ 94.7% 15,164 / 16,011
reminders ███████████████████░ 94.8% 760 / 802
review_hog ███████████████████░ 95.0% 11,750 / 12,362
annotations ███████████████████░ 95.1% 817 / 859
endpoints ███████████████████░ 95.1% 9,231 / 9,703
customer_analytics ███████████████████░ 95.2% 25,992 / 27,302
legal_documents ███████████████████░ 95.2% 2,311 / 2,427
marketing_analytics ███████████████████░ 95.3% 19,450 / 20,413
posthog_ai ███████████████████░ 95.3% 2,491 / 2,614
actions ███████████████████░ 95.5% 756 / 792
logs ███████████████████░ 95.5% 15,468 / 16,200
experiments ███████████████████░ 95.5% 32,937 / 34,486
data_catalog ███████████████████░ 95.6% 4,402 / 4,606
tracing ███████████████████░ 95.6% 3,536 / 3,699
replay_vision ███████████████████░ 95.6% 29,371 / 30,708
growth ███████████████████░ 95.7% 11,381 / 11,888
skills ███████████████████░ 95.8% 6,972 / 7,274
messaging ███████████████████░ 95.9% 3,824 / 3,989
product_analytics ███████████████████░ 96.0% 28,521 / 29,696
revenue_analytics ███████████████████░ 96.4% 1,889 / 1,959
user_interviews ███████████████████░ 96.5% 2,870 / 2,974
feature_flags ███████████████████░ 96.6% 26,904 / 27,856
access_control ███████████████████░ 96.7% 7,738 / 8,005
warehouse_sources ███████████████████░ 97.3% 468,810 / 481,716
data_quality ████████████████████ 97.5% 7,701 / 7,895
links ████████████████████ 97.9% 234 / 239
security ████████████████████ 98.0% 1,283 / 1,309
metrics ████████████████████ 98.0% 4,252 / 4,338
analytics_platform ████████████████████ 98.3% 2,784 / 2,833
pulse ████████████████████ 98.5% 2,046 / 2,078
live_debugger ████████████████████ 99.2% 626 / 631
field_notes ████████████████████ 99.4% 172 / 173

Report-only. Patch coverage = changed backend lines covered vs origin/master. Sorted lowest first.
Known gaps: lines covered only by Temporal tests show as uncovered; core line numbers may drift if master changed the same file.

resolvedExposureEvent now returns $experiment_exposure for an experiment with no
server-resolved event, instead of taking a per-call-site fallback. Such an experiment is
always a draft built locally, which would start after the cutoff, so the criteria panel no
longer has to override the shared helper while the metric form and the recordings
linkability check name the legacy event for the same draft.

Also hoists the duplicated default exposure config in get_exposure_config_params_for_builder
and corrects comments that still described the removed flag as a per-team rollout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 565b560e-1c84-4501-9b76-73bf9f72f117
@posthog

posthog Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up commit from the cleanup pass (eda1bc1):

  • resolvedExposureEvent now resolves the draft fallback itself ($experiment_exposure) instead of taking a per-call-site argument. The PR body's earlier note that the helper default stays $feature_flag_called is superseded: the fallback only ever fires for a locally-built draft (experiments from the API always carry the server-resolved field), and a draft starts after the cutoff. Keeping the old default meant the criteria panel and the metric form labelled the same draft differently.
  • Hoisted the duplicated default exposure config in get_exposure_config_params_for_builder.
  • Corrected comments that still described the removed flag as a per-team rollout.

Verified after this commit: frontend typescript:check, ruff check/format. The frontend Jest re-run and a backend re-run did not complete before this run ended, so CI is the check on those — the exposureContract unit test was updated for the new signature.

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 5f80941b-f0c6-4973-96a0-64daa0cb95cd

📥 Commits

Reviewing files that changed from the base of the PR and between 894e18e and 967a07b.

⛔ Files ignored due to path filters (1)
  • products/experiments/frontend/generated/api.schemas.ts is excluded by !**/generated/**
📒 Files selected for processing (4)
  • frontend/src/scenes/experiments/ExperimentForm/ExposureCriteriaPanel.tsx
  • frontend/src/scenes/experiments/exposureContract.test.ts
  • frontend/src/scenes/experiments/exposureContract.ts
  • services/mcp/src/api/generated.ts

Included review availability: Your plan provides up to 12 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

Default exposure-event selection now uses experiment start date rather than team feature-flag state. Backend callers and exposure configuration helpers no longer pass a team to resolve the default event. The frontend form and exposure contract use the updated event resolution, and tests cover cutoff-based selection and draft fallbacks.

Priority: ⚪ Not assessed

Merge Risk: ⚪ Minimal · up to 967a0

The API description now matches date-based exposure-event selection. No outstanding issue identified here prevents merging after normal checks.

Security Architecture Review

Security architecture risk: 🟡 Moderate · up to 967a0

Experiment results and the ability to freeze exposure could be affected if some teams do not receive the event that queries now select. Whether that configuration exists in production is unverified; no security vulnerability was established.

Retained concerns

  • Medium · reliability · inferred: Conditionally, date-only selection can outpace team-scoped production of $experiment_exposure. For a post-cutoff experiment without duplicated events, results would omit exposures and an otherwise valid freeze could fail closed, limiting exposure-containment operations.
Security review details

Security Blast Radius

  • inferred — The conditional mismatch can affect each post-cutoff experiment whose team lacks duplicated events; the reviewed paths show an analytics and freeze-containment consequence, not a demonstrated cross-tenant access path.

Trust Boundaries and Controls

  • observed — The freeze operation checks feature-flag access before scanning and rejects an empty snapshot before changing targeting; its later phase locks and rechecks the flag. No changed authorization control or attacker-directed sink was identified in the inspected paths.

Resilience and Maintainability Implications

  • inferred — Rejecting an empty freeze snapshot prevents an empty cohort from unenrolling everyone, but it cannot restore the ability to freeze an experiment whose real exposures are present only under the legacy event.

Hardening Proposals

  • proposed — Verify duplication settings and event presence across production ingestion deployments and relevant experiment windows before relying exclusively on the cutoff; retain a recovery path for teams with legacy-only exposures.
🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description is complete and stand-alone. It explains the problem, user-visible and mechanical changes, testing and known failures, release status, documentation impact, agent context, and key desi…
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Update the stale resolved_exposure_event help_text. · serializers.py:440-448

products/experiments/backend/presentation/serializers.py:440-448
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Update the stale resolved_exposure_event help_text.

This help_text still says $experiment_exposure applies "once the team is in the rollout and the experiment started at or after the cutoff." resolve_default_exposure_event no longer checks any team rollout state — it resolves purely from the start date (see line 580 and resolve_default_exposure_event in exposure_query_logic.py). This text feeds the generated OpenAPI schema and misleads API and MCP consumers about the field's actual semantics.

Drop the rollout clause so the text matches the date-only resolution.

📝 Proposed fix
     resolved_exposure_event = serializers.SerializerMethodField(
         help_text=(
             "The event exposures are actually counted on when the experiment doesn't configure a "
-            "custom one — `$feature_flag_called`, or `$experiment_exposure` once the team is in the "
-            "rollout and the experiment started at or after the cutoff. Resolved server-side so "
-            "clients display the same event the results queries read. For a draft, this is what the "
-            "experiment would resolve to if launched now."
+            "custom one — `$feature_flag_called`, or `$experiment_exposure` once the experiment "
+            "started at or after the cutoff. Resolved server-side so clients display the same event "
+            "the results queries read. For a draft, this is what the experiment would resolve to if "
+            "launched now."
         )
     )

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: PostHog/posthog/.coderabbit.yaml

Review profile: QUIET

Plan: Enterprise

Run ID: 94174ca6-5607-483e-bb9c-7cae6cbbc09b

📥 Commits

Reviewing files that changed from the base of the PR and between 7052550 and eda1bc1.

📒 Files selected for processing (25)
  • frontend/src/lib/constants.tsx
  • frontend/src/scenes/experiments/ExperimentForm/ExposureCriteriaPanel.tsx
  • frontend/src/scenes/experiments/exposureContract.test.ts
  • frontend/src/scenes/experiments/exposureContract.ts
  • frontend/src/types.ts
  • products/experiments/backend/experiment_service.py
  • products/experiments/backend/hogql_queries/experiment_exposures_query_runner.py
  • products/experiments/backend/hogql_queries/experiment_query_builder.py
  • products/experiments/backend/hogql_queries/experiment_query_runner.py
  • products/experiments/backend/hogql_queries/exposure_query_logic.py
  • products/experiments/backend/hogql_queries/test/experiment_query_runner/test_experiment_exposures_query_runner.py
  • products/experiments/backend/hogql_queries/test/test_experiment_exposure_preaggregation.py
  • products/experiments/backend/hogql_queries/test/test_experiment_funnel_metric_events_preaggregation.py
  • products/experiments/backend/hogql_queries/test/test_experiment_mean_metric_events_preaggregation.py
  • products/experiments/backend/hogql_queries/test/test_experiment_retention_metric_events_preaggregation.py
  • products/experiments/backend/presentation/serializers.py
  • products/experiments/backend/replay_linkage.py
  • products/experiments/backend/session_buckets.py
  • products/experiments/backend/session_event_deltas.py
  • products/experiments/backend/session_exposure.py
  • products/experiments/backend/setup_context.py
  • products/experiments/backend/test/test_freeze_exposure_clickhouse.py
  • products/experiments/backend/test/test_presentation_api.py
  • products/experiments/backend/test/test_session_buckets_api.py
  • products/experiments/backend/test/test_session_exposure.py
💤 Files with no reviewable changes (1)
  • frontend/src/lib/constants.tsx

Included review availability: Your plan provides up to 12 included reviews per hour; 2 remain after this review.

@trunk-io

trunk-io Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
ErrorTrackingWidget loads the declared renderer and falls back for the MCP issues-list response The test exceeded the maximum allowed time of 5000 milliseconds and timed out. Logs ↗︎
Scenes/Code review Default play-test The test failed because a logic component was not mounted when accessed, and there were unhandled network requests intercepted by the mock service... Logs ↗︎
Dashboards › Creating a SQL insight with a variable and overriding it on a dashboard The test failed because the page URL did not match the expected URL. Logs ↗︎
test_set_flag_evaluations_mode_moves_the_organizations_of_the_teams_2_allow_downgrade_lowers_the_organization_above The API returned a 500 error instead of the expected 200 OK response. Logs ↗︎

... and 8 more

View Full Report ↗︎ ⋅ Docs

Two Jest fixtures built an experiment without resolved_exposure_event, so they took the
draft fallback and now resolve $experiment_exposure. Real experiments from the API always
carry the field, so the fixtures now set it explicitly to the event their assertions mean.

Also drops the team-rollout clause from the resolved_exposure_event help_text, which fed the
generated OpenAPI schema, and regenerates the types.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 565b560e-1c84-4501-9b76-73bf9f72f117
@posthog

posthog Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

Pushed b84ec88.

CodeRabbit — fixed. The resolved_exposure_event help_text no longer mentions a team rollout; it now describes the date-only resolution. That string feeds the generated OpenAPI schema, so hogli build:openapi was re-run and products/experiments/frontend/generated/api.schemas.ts is updated in the same commit.

Jest (app-1, app-2) — fixed. Two fixtures (utils.test.ts experimentBase, experimentReplayTabLogic.test.ts EXPERIMENT) built an experiment with no resolved_exposure_event, so after the previous commit they took the draft fallback and resolved $experiment_exposure. Experiments from the API always carry the field, so both fixtures now set it explicitly to the event their assertions mean. utils.test.ts, experimentReplayTabLogic.test.ts and exposureContract.test.ts pass locally (205 tests).

Visual review (2 changed) — not from this PR. Both snapshots are scenes-app-signals-artefactloglist (narrow, light and dark) at 0.28–0.34% diff. This PR touches nothing under signals. No experiments story changed, so the exposure criteria panel label is not covered by a snapshot. Leaving these for a human to accept or re-run.

@posthog

posthog Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

👋 Visual changes detected for this PR.

Review and approve in PostHog Visual Review

If these changes are unexpected, they may be caused by a flaky test or a broken snapshot on master. Don't approve — rerun the job or wait for a fix.

posthog Bot and others added 3 commits September 25, 2026 06:46
resolvedExposureEvent fell back to $experiment_exposure for any payload missing
resolved_exposure_event. That assumed every such payload is a locally-built draft, but
ExperimentBasicSerializer omits the field, so a list-sourced experiment took the fallback
too — and a pre-cutoff experiment was then named on the wrong event.

The shared helper takes the legacy event again, and only the creation form, which knows its
experiment is new, passes $experiment_exposure explicitly.

This reverts the two test fixtures that were edited to accommodate the earlier fallback.
Three fixtures needing a server-resolved field they never carried was the signal that the
helper default was wrong, not the fixtures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Generated-By: PostHog Desktop
Task-Id: 565b560e-1c84-4501-9b76-73bf9f72f117
…ve-the-fully-ee3bbb

Generated-By: PostHog Desktop
Task-Id: 565b560e-1c84-4501-9b76-73bf9f72f117
…g/choreexperiments-remove-the-fully-ee3bbb

# Conflicts:
#	products/experiments/backend/hogql_queries/experiment_query_builder.py
#	products/experiments/backend/hogql_queries/exposure_query_logic.py
@andehen andehen assigned andehen and unassigned rubychilds Oct 1, 2026
Dropping the flag check moved every deployment to $experiment_exposure for
experiments started after the cutoff. Only PostHog Cloud ingests that event:
the plugin server copies $feature_flag_called into it for the teams in
EXPERIMENT_EXPOSURE_DUPLICATION_TEAMS, which is empty by default, and no SDK
sends it. A self-hosted install would count zero exposures, and freezing
enrollment would snapshot nobody.

The flag never resolved on self-hosted installs, so they always counted
$feature_flag_called. The new EXPERIMENT_EXPOSURE_EVENT_INGESTED setting keeps
that: it defaults to false in hobby run mode and true on Cloud, in local dev
and in tests. The creation form names the legacy event on hobby to match.
Local dev resolves post-cutoff experiments to $experiment_exposure like Cloud
does, but its ingestion did not make the event, so new local experiments
showed no exposures. Default EXPERIMENT_EXPOSURE_DUPLICATION_TEAMS to all
teams in the dev env, the same way PROPERTY_DEFS_CONSUMER_ENABLED_TEAMS does.
- Drop a feature_enabled mock that only existed for the removed flag. It now
  switched unrelated flags on for the test.
- Say "cutoff" instead of "rollout" in comments and the session replays skill.
- Correct why the session cache keys carry the resolved exposure event: it
  follows the start date and the EXPERIMENT_EXPOSURE_EVENT_INGESTED setting,
  which can change while an entry is warm.
@andehen
andehen marked this pull request as ready for review October 1, 2026 21:42
@parameterai

parameterai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Risk: No findings

This increment replaces the boolean EXPERIMENT_EXPOSURE_EVENT_INGESTED setting with a configurable EXPERIMENT_EXPOSURE_EVENT_CUTOFF datetime, and makes the nodejs ingestion duplicate $feature_flag_called into $experiment_exposure for all teams on every deployment instead of dev-only. I traced every surface in the delta — event resolution, HogQL builders, cache keys, the nodejs copy step, and the frontend contract — and found no security issue: the resolved event is always one of two fixed constants, cache keys still include it, and no attacker-controlled input reaches the resolution logic across a trust boundary.

Sentinel reviewed 104ba29 · Review settings

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team October 1, 2026 21:43
@pr-assigner-resolver-posthog

Copy link
Copy Markdown

👀 Auto-assigned reviewers

These soft owners were skipped because they only have minor changes here. Nothing blocks merge, so self-assign if you'd like a look:

  • @PostHog/team-ingestion (nodejs/src/owners.yaml)

Soft owners come from each directory's owners.yaml and each product's product.yaml (resolved nearest-file-wins). For a skipped owner, the locator is the file that decided it. Generated files and lockfiles are ignored when deciding ownership.

The cutoff moves from a constant to the EXPERIMENT_EXPOSURE_EVENT_CUTOFF
setting, an ISO 8601 value read from the environment. It defaults to
2026-09-01 UTC, when PostHog Cloud started copying $feature_flag_called into
$experiment_exposure, and Cloud does not override it. A self-hosted install
that starts the copy later sets it to that time, so its older experiments keep
counting $feature_flag_called.

This replaces EXPERIMENT_EXPOSURE_EVENT_INGESTED. Ingestion now makes the copy
on every deployment, so self-hosted installs no longer need a separate
switch, and the creation form names $experiment_exposure everywhere again.
EXPERIMENT_EXPOSURE_DUPLICATION_TEAMS now defaults to all teams on every
deployment, so self-hosted installs, local dev and tests make
$experiment_exposure the same way PostHog Cloud does.

generate_experiment_data drops its --exposure-event option. The data it
captures goes through ingestion, which now makes the copy, so the option
would count every exposure twice.
@scheduled-actions-posthog

Copy link
Copy Markdown
Contributor

This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, please remove the stale label – otherwise this will be closed in another week. If you want to permanently keep it open, use the waiting label.

@scheduled-actions-posthog

Copy link
Copy Markdown
Contributor

This PR was closed due to lack of activity. Feel free to reopen if it's still relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants