Skip to content

fix(replay): gate event-trigger starts behind the replay checks - #758

Draft
posthog[bot] wants to merge 1 commit into
posthog-self-driving/fixreplay-stop-android-replay-when-its-fea916from
posthog-self-driving/fixreplay-gate-android-event-trigger-3831bf
Draft

fix(replay): gate event-trigger starts behind the replay checks#758
posthog[bot] wants to merge 1 commit into
posthog-self-driving/fixreplay-stop-android-replay-when-its-fea916from
posthog-self-driving/fixreplay-gate-android-event-trigger-3831bf

Conversation

@posthog

@posthog posthog Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

  • Who is hurt: Android apps that gate session replay behind their own feature flag, on any project whose settings configure event triggers. They record the users the flag excludes, so this is privacy-shaped rather than cosmetic.
  • Root cause: a matched event trigger called start() directly. That skipped every gate the other start paths apply, and never read config.sessionReplay at all.
  • It then stuck: start() sets startedWithAutomaticDisabled, so the recording counted as manually started and survived rotation and every later check until an explicit stopSessionReplay().
Gate startSessionReplay onSessionIdChanged Event trigger, before Event trigger, after
config.sessionReplay master switch
Project flag (isSessionReplayFlagActive)
Sampling decision
flowchart LR
  E["captured event<br/>matches a trigger"] --> A["record the activation"]
  A --> G{"master switch,<br/>project flag,<br/>sampling"}
  G -- "all allow" --> S["start recording"]
  G -- "any rejects" --> N["do not start<br/>(new)"]
Loading

Stacked on #757, which added the reevaluateRecordingState() / isRecordingPermittedForCurrentSession() policy this reuses, and touches the same files.

Changes

  • The event-trigger path now asks isRecordingPermittedForCurrentSession() before it starts, so a matched trigger only lifts the event-trigger gate and the rest of the policy still decides.
  • start() now remembers an explicit start asked for while automatic replay is off. The trigger gate defers such a start, so without this the manual intent is lost and the deferred recording would be refused as an automatic one once the trigger matched.
  • The trigger activation is still recorded before the gate runs. The trigger genuinely fired, so turning the master switch back on records the rest of the session instead of waiting for a trigger that already passed.
  • KDoc on PostHogConfig.sessionReplay corrected: an event trigger is not a manual start.

Note

Behavior change for one configuration: a project with event triggers whose app never sets sessionReplay = true and never calls startSessionReplay() no longer records. That is the reported symptom.

💚 How did you test it?

Ran locally on JDK 17 with the Android SDK: make test (all modules), make checkFormat, and apiCheck all pass. No public API change, so the .api dumps are unchanged.

Three new tests in PostHogReplayIntegrationTest, next to the existing trigger tests and reusing their fixture:

  • Each rejecting gate in turn — master switch off, project flag off, sampled out — leaves recording inactive after a matching event.
  • A refused trigger start is not a manual recording, so a later remote config delivery still does not record; writing sessionReplay = true then does record, which proves the activation was kept.
  • A recording that start() asked for while the master switch is off still starts once the trigger matches.
Non-vacuity check

Each test was confirmed to fail with its half of the fix removed: the first two fail without the gate, the third fails without the manual-intent line.

One existing test (trigger started replay does not resume on a session the trigger has not activated) set sessionReplay = false and asserted the trigger started recording, which is the defect. It now sets sessionReplay = true and asserts the same outcome for its own subject, the trigger gate blocking a resume after rotation.

No emulator run: the symptom is behavioral, and the tests drive the real integration through the trigger, rotation, remote config, and master switch paths.

📝 Checklist

  • I reviewed the submitted code.
  • I added tests to verify the changes.
  • I updated the docs if needed.
  • No breaking change or entry added to the changelog.

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Fully autonomous

  • Authored by PostHog Desktop (Claude) from an inbox report, with Read/Grep/Bash and the repo's own Gradle tasks. No human drove it, so it is left unassigned for the owning team to triage. Agent-authored, so it needs human review.
  • Rejected: mirroring onSessionIdChanged gate by gate. The report suggested that shape. isRecordingPermittedForCurrentSession() already encodes the same decision and fix(replay): honor PostHogConfig.sessionReplay writes after setup #757 made it the single source, so duplicating the three checks inline would have been a second copy to keep in sync.
  • Rejected: posting the trigger start to the main thread. onSessionIdChanged and the resume path both post. Doing that here would delay a trigger start and widen the diff past the missing gate, and the gate check plus start() run on one thread as it is.
  • Found while fixing, and kept in scope. The gate alone would have regressed one case: an app that calls startSessionReplay() while sessionReplay is false, on a project with triggers. The trigger gate defers that start and the intent was never recorded, so the gate would refuse it later. Hence the second change, with its own test.
  • Verified the flags-service path named in the report as a possible cause is unrelated and intact.

Created with PostHog Desktop from this inbox report.

A matching event trigger called start() directly, so it skipped the master
switch, the project flag and the sampling decision that every other start
path applies. An app that gates replay behind its own feature flag kept
recording the users the flag excludes. start() then set
startedWithAutomaticDisabled, so the recording counted as manually started
and survived every later check.

onEvent now reuses isRecordingPermittedForCurrentSession() before it starts,
after it records the trigger activation, so the trigger only lifts the
event-trigger gate.

start() remembers an explicit start asked for while automatic replay is off.
The trigger gate defers that start, so without this the manual intent is lost
and the deferred recording is refused once the trigger matches.

Generated-By: PostHog Desktop
Task-Id: d4a23d7a-bccc-4e65-9061-9223a7937811
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.

0 participants