Skip to content

fix(replay): pare down debug properties on captured events - #833

Open
ioannisj wants to merge 2 commits into
mainfrom
feat/pare-down-replay-debug-properties
Open

ioannisj wants to merge 2 commits into
mainfrom
feat/pare-down-replay-debug-properties

Conversation

@ioannisj

@ioannisj ioannisj commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Ports PostHog/posthog-ios#859, which is itself a port of PostHog/posthog-js#5144 plus a few mobile-only decisions. Since #782 every captured event carries the full replay debug bundle, which repeats a lot of properties for diagnostics that only need an occasional sample.

Required on every event (when available): $recording_status, $sdk_debug_replay_event_trigger_status, $sdk_debug_replay_linked_flag_trigger_status and $sdk_debug_replay_internal_buffer_length. $sdk_debug_pending_queue_size stays ungated, and SDK values still overwrite a same-named registered super property.

The rest of the bundle ($sdk_debug_session_start, $sdk_debug_replay_capture_mode, $sdk_debug_replay_flush_hold_reason, $sdk_debug_replay_pending_trigger_conditions) only goes on SDK events, meaning names starting with $ other than $feature_flag_called and $snapshot. Eligibility uses the name before beforeSend, and custom events never get it.

It's attached at most once per 30s of wall clock:

  • Only one claim can be outstanding, so a capture from inside beforeSend can't also take it. A claim older than 30s counts as leaked.
  • The window starts once the claiming event is handed to the queue. A claimer dropped by beforeSend, routed to the replay queue as $snapshot, or failing mid-capture releases the claim instead.
  • State resets on close().

Removes $sdk_debug_current_session_duration and $sdk_debug_replay_throttle_delay_ms.

Every event goes through PostHog.capture, so whether an event claimed the bundle is tracked on that call rather than on the event. That survives beforeSend renaming or replacing the event without a marker in the properties or a new field on PostHogEvent. captureStateless now delegates to an internal helper that reports whether the event reached the queue. No public API change (make api diff is empty).

Where Android differs from iOS:

  • beforeSend already runs twice per stateful capture (once in PostHog.capture, again in captureStateless). That's pre-existing and not changed here; the window commits after both passes. Worth a follow-up to run it once.
  • Person-properties dedup happens before capture, so a deduplicated $set never claims.
  • There's no read-only crash-context property build on Android, so nothing to exempt from the gate.
  • Keeps the existing Android rule that an event backdated before the current session started carries no debug keys.

The published session-replay-debug-properties spec still requires the removed keys and per-event attachment. The spec update is in flight.

💚 How did you test it?

New PostHogTest cases cover:

  • throttle at 0/29/30s with the required keys on every event
  • wall clock vs a future event timestamp, and a clock moving backwards (the window stays shut until it catches up, same as iOS)
  • $feature_flag_called and custom events carrying only the required keys and not arming the window
  • a claimer dropped by beforeSend releasing, and an event without the bundle not consuming the window when the interval elapses mid-capture
  • a deduplicated identify $set not arming
  • a capture from inside beforeSend not double-claiming, and renames in both directions
  • the window starting at acceptance with the clock advanced inside beforeSend
  • a snapshot-routed claimer releasing, no internal state reaching the queued event, and close() resetting the window

I updated the tests that asserted the full bundle on custom events or the removed keys, and re-recorded the Android batch event-shape snapshot. 13 of the 14 new tests fail against main; the one that passes there is the no-internal-state guard, which holds by construction.

./gradlew :posthog:test and :posthog-android:testDebugUnitTest pass, and make checkFormat and make api are clean.

📝 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

DRI: @ioannisj
Autonomy: Human-driven (agent-assisted)

@ioannisj
ioannisj requested a review from a team as a code owner October 2, 2026 19:20
@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Retrigger

[Medium risk] Adjusts which debug properties attach to replay events.

The PR appears safe to merge, with a non-blocking issue that can allow extra debug bundles during overlapping captures.

Reviews (1) · Last reviewed commit: "fix(replay): keep the replay debug windo..."

Comment on lines +871 to +874
private fun releaseReplayDebugPropertiesClaim() {
synchronized(replayDebugPropertiesLock) {
outstandingReplayDebugClaimAt = null
}

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.

P2 Older captures clear newer claims

releaseReplayDebugPropertiesClaim() clears the current claim without checking who owns it. If capture A stays in beforeSend for over 30 seconds, capture B can replace its expired claim. If A then returns null, its finally clears B's claim. Capture C can now claim while B is still running, letting B and C queue the full debug bundle within 30 seconds.

Give each claim an owner token and release it only when that token still owns the claim. Add a test for this overlap.

Knowledge Base Used: Event capture and delivery

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/src/main/java/com/posthog/PostHog.kt
Line: 871-874

Comment:
**Older captures clear newer claims**

`releaseReplayDebugPropertiesClaim()` clears the current claim without checking who owns it. If capture A stays in `beforeSend` for over 30 seconds, capture B can replace its expired claim. If A then returns `null`, its `finally` clears B's claim. Capture C can now claim while B is still running, letting B and C queue the full debug bundle within 30 seconds.

Give each claim an owner token and release it only when that token still owns the claim. Add a test for this overlap.

**Knowledge Base Used:** [Event capture and delivery](https://app.greptile.com/posthog-org-19734/-/custom-context/knowledge-base/posthog/posthog-android/-/docs/event-capture-and-delivery.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@github-actions

github-actions Bot commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-10-02 19:27:25 UTC
Duration: 118020ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

✅ 29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields ✅ 271ms
Format Validation.Event Has Uuid ✅ 30ms
Format Validation.Event Has Lib Properties ✅ 19ms
Format Validation.Distinct Id Is String ✅ 20ms
Format Validation.Token Is Present ✅ 18ms
Format Validation.Custom Properties Preserved ✅ 21ms
Format Validation.Event Has Timestamp ✅ 17ms
Retry Behavior.Retries On 503 ✅ 7024ms
Retry Behavior.Does Not Retry On 400 ✅ 4026ms
Retry Behavior.Does Not Retry On 401 ✅ 4025ms
Retry Behavior.Respects Retry After Header ✅ 7027ms
Retry Behavior.Implements Backoff ✅ 17035ms
Retry Behavior.Retries On 500 ✅ 7022ms
Retry Behavior.Retries On 502 ✅ 7020ms
Retry Behavior.Retries On 504 ✅ 7020ms
Retry Behavior.Max Retries Respected ✅ 17035ms
Deduplication.Generates Unique Uuids ✅ 38ms
Deduplication.Preserves Uuid On Retry ✅ 7017ms
Deduplication.Preserves Uuid And Timestamp On Retry ✅ 12022ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry ✅ 7020ms
Deduplication.No Duplicate Events In Batch ✅ 38ms
Deduplication.Different Events Have Different Uuids ✅ 21ms
Compression.Sends Gzip When Enabled ✅ 14ms
Batch Format.Uses Proper Batch Structure ✅ 14ms
Batch Format.Flush With No Events Sends Nothing ✅ 10ms
Batch Format.Multiple Events Batched Together ✅ 27ms
Error Handling.Does Not Retry On 403 ✅ 4015ms
Error Handling.Does Not Retry On 413 ✅ 4023ms
Error Handling.Retries On 408 ✅ 5029ms

Feature_Flags Tests

✅ 17/17 tests passed

View Details
Test Status Duration
Request Payload.Request With Person Properties Device Id ✅ 33ms
Request Payload.Flags Request Uses V2 Query Param ✅ 26ms
Request Payload.Flags Request Hits Flags Path Not Decide ✅ 19ms
Request Payload.Flags Request Omits Authorization Header ✅ 19ms
Request Payload.Token In Flags Body Matches Init ✅ 16ms
Request Payload.Groups Round Trip ✅ 15ms
Request Payload.Groups Default To Empty Object ✅ 17ms
Request Payload.Disable Geoip False Propagates As Geoip Disable False ✅ 15ms
Request Payload.Disable Geoip Omitted Defaults To False ✅ 17ms
Request Payload.Flag Keys To Evaluate Contains Only Requested Key ✅ 16ms
Request Lifecycle.No Flags Request On Init Alone ✅ 10ms
Request Lifecycle.No Flags Request On Normal Capture ✅ 17ms
Request Lifecycle.Two Flag Calls Produce Two Remote Requests ✅ 30ms
Request Lifecycle.Mock Response Value Is Returned To Caller ✅ 17ms
Retry Behavior.Retries Flags On 502 ✅ 320ms
Retry Behavior.Retries Flags On 504 ✅ 320ms
Side Effect Events.Get Feature Flag Captures Feature Flag Called Event ✅ 19ms

This branch has not been deployed

No deployments
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.

1 participant