Skip to content

fix(ingestion): skip $experiment_exposure duplication for v2 flag rules calls - #125449

Merged
trunk-io[bot] merged 1 commit into
masterfrom
feat/ingestion-skip-v2-exposure-clone
Oct 29, 2026
Merged

trunk-io[bot] merged 1 commit into
masterfrom
feat/ingestion-skip-v2-exposure-clone

Conversation

@andehen

@andehen andehen commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

Problem

For teams in EXPERIMENT_EXPOSURE_DUPLICATION_TEAMS, ingestion duplicates every multivariate $feature_flag_called event as an $experiment_exposure event (variant inferred from a non-boolean string $feature_flag_response).

Flags on v2 rules work differently: SDKs mark their $feature_flag_called events with $feature_flag_config_version: 2 and send $experiment_exposure themselves. Duplicating those calls would record each exposure twice.

Changes

  • create-event-step.ts: a $feature_flag_called event with $feature_flag_config_version equal to 2 is no longer duplicated. It passes through unchanged. SDKs send the number; the string "2" is matched too, because a double-counted exposure is worse than a skipped duplicate.
  • Everything else stays as it is: the team allowlist, the multivariate inference, and the deterministic UUIDv5 of the duplicate (so Kafka redeliveries still collapse in ClickHouse). Events without the property, or with 1, keep the current behaviour.
  • A directly captured $experiment_exposure was already never duplicated (the check requires the $feature_flag_called event name). A test now covers it.

createCreateEventStep is the only place that duplicates. The analytics event subpipeline is the only caller that passes the allowlist (the AI and error-tracking pipelines call it without one).

How did you test this code?

  • New unit tests in create-event-step.test.ts:
    • $feature_flag_config_version 2 or "2": no duplicate, original event unchanged (uuid, name, properties).
    • Missing, 1 or "1": duplicated as before, with the UUID pinned to the existing UUIDv5 derivation.
    • A direct $experiment_exposure: not duplicated.
  • The two v2 cases fail without the guard.
  • pnpm exec jest src/ingestion/common/steps/event-processing/create-event-step.test.ts: 33 passed.
  • prettier, eslint and oxlint are clean on the changed files.

Rollback

Revert. Only safe while no SDK sends $experiment_exposure directly for v2 rules flags; after that, a revert brings back double-counted exposures.

🤖 Generated with Claude Code

…es calls

SDKs mark $feature_flag_called events for flags on v2 rules with
$feature_flag_config_version: 2 and send $experiment_exposure directly.
Duplicating those calls would record each exposure twice.
@trunk-io

trunk-io Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

😎 Merged successfully - details.

@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

🤖 CI report

✅ Trunk lane — non-backend lane (node:ingestion)

This PR is assigned to the non-backend lane (node:ingestion). It does not run backend Python tests and may merge in parallel with PRs in other lanes.

✅ Duplication (Python) — clean

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.

✅ 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 — 5% of added code lines are comments (4 of 78)

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
nodejs/src/ingestion/common/steps/event-processing/create-event-step.ts 3 8
nodejs/src/ingestion/common/steps/event-processing/create-event-step.test.ts 1 70

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

@andehen
andehen marked this pull request as ready for review October 29, 2026 12:52
@parameterai

parameterai Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Risk: No findings

This change stops duplicating multivariate $feature_flag_called events into $experiment_exposure when the event carries $feature_flag_config_version 2 (number or string), since SDKs on v2 flag rules send the exposure event themselves; tests pin both the new pass-through and the unchanged duplication path. The only security-relevant observation is that the gating property is client-supplied, but its effect is limited to the client's own project's analytics, which they can already control by other means, so no vulnerability is introduced.

Sentinel reviewed 7d7b95a · Review settings

@pr-assigner-resolver-posthog
pr-assigner-resolver-posthog Bot requested a review from a team October 29, 2026 12:53
@trunk-io

trunk-io Bot commented Oct 29, 2026

Copy link
Copy Markdown

Static Badge   Static Badge   Static Badge

Failed Test Failure Summary Logs
ingestion general server should not error on startup - ingestion_v2_combined The test expected the mock function to be called with 0, but it was called once. Logs ↗︎
ingestion general server should not error on startup - ingestion_v2 A mock function was expected not to be called, but it was called once. Logs ↗︎

View Full Report ↗︎ ⋅ Docs

@trunk-io
trunk-io Bot merged commit 4f0291a into master Oct 29, 2026
285 checks passed
@trunk-io
trunk-io Bot deleted the feat/ingestion-skip-v2-exposure-clone branch October 29, 2026 15:09
@deployment-status-posthog

deployment-status-posthog Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-10-29 15:45 UTC Run
prod-us ✅ Deployed 2026-10-29 16:22 UTC Run
prod-eu ✅ Deployed 2026-10-29 16:15 UTC Run

andehen added a commit to PostHog/posthog-sdk-test-harness that referenced this pull request Oct 30, 2026
Follow PostHog/sdk-specs#162. An SDK sends $experiment_exposure only for
a config version 2 experiment split, so the config version, rule type,
reason code and source marker were constants on it. Require exactly
$feature_flag, $feature_flag_response, $feature_flag_variant,
$feature_flag_rule_id and locally_evaluated, and drop the other four
properties so the closed schema rejects them. The rule ID identifies
the experiment rule and marks the exposure as SDK-sent: ingestion no
longer copies a $feature_flag_called that carries config version 2
(PostHog/posthog#125449), and a copy never carries a rule ID.

A config version 2 $feature_flag_called carries $feature_flag_reason_code
in place of $feature_flag_reason. Declare $feature_flag_reason in the
called context and reject it in the version 2 branch; version 1 calls
keep it with any value.

The event schemas have no fixture sets, so meta-tests build the
projections: each exposure property missing, each forbidden exposure
property present, and $feature_flag_reason on every presence-matrix
row's version 2 call. Contract package 8.0.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants