Skip to content

fix(errortracking): stamp native crashes on the SDK's clock - #813

Merged
turnipdabeets merged 4 commits into
mainfrom
fix/native-crash-timestamp-clock
Sep 25, 2026
Merged

turnipdabeets merged 4 commits into
mainfrom
fix/native-crash-timestamp-clock

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Native (NDK) crashes recovered from ApplicationExitInfo can land at the wrong time in PostHog, by however far the device's wall clock and network time disagree.

  • PostHogNativeCrashIntegration stamps the event with exitInfo.timestamp, which is wall-clock time.
  • On API 33+, PostHogAndroidDateProvider fills the batch sent_at from network time.
  • Ingestion's clock-skew correction stores roughly received time + (timestamp - sent_at). With the two values on different clocks, the gap between the clocks goes straight into the stored time.

Seen on an emulator whose network clock lagged the wall clock by about 36 minutes after the host slept: every recovered NDK crash was stored 36 minutes in the future. A real device with a manually set or drifted clock would be off by its clock error in the same way. Found while testing React Native NDK crash capture (PostHog/posthog-js#5062).

The fix records each run's offset between dateProvider.currentTimeMillis() and the wall clock (NativeCrashClockOffsetStore, last 16 runs, keyed by pid). The offset is recorded at startup and again on every ACTION_TIME_CHANGED, so it stays right if the wall clock is set while the app runs. Each recovered crash's exit timestamp is then shifted by the offset of the run it crashed in, found by the exit record's pid, so the event is on the same clock as sent_at even if the clock changed between the crash and its recovery. Runs are keyed by pid, not start time, because the wall clock can move backwards. The current offset is only used when no run was recorded yet, for example on the first launch after upgrading. The watermark still stores the raw exitInfo.timestamp, so deduplication is unchanged. Events captured in-process (JVM crashes and everything else) already use the date provider and are unaffected.

💚 How did you test it?

  • Unit tests: the crash uses the offset recorded by its own run (also after the wall clock moved back), the current run's offset is recorded and re-recorded on a time change, the current offset is the fallback, plus NativeCrashClockOffsetStoreTest. Each fails without its part of the fix. All :posthog-android:testDebugUnitTest tests pass, and spotlessApply made no changes.
  • On device: API 34 arm64 emulator whose network clock lagged the wall clock by about 36 minutes. React Native example app (posthog-js example-expo-57) with this build forced in place of posthog-android 3.66.0, release build, and a real JNI SIGSEGV. The crash time is the OS exit-record time; the stored time comes from PostHog.
Build Crash (exit record) Stored Off by
3.66.0 (before) 15:40:33 16:17:12 +36 min
3.66.0 (before) 15:52:06 16:28:40 +36.5 min
This PR 16:43:23.97 16:43:30.42 +6.4 s (first request of a cold launch)
This PR 16:46:12.05 16:46:13.55 +1.5 s
This PR 16:46:57.99 16:46:59.31 +1.3 s
This PR, sent 22 s later after the network came back 16:48:11.47 16:48:12.99 +1.5 s
This PR, per-run offset 18:10:34.66 18:10:36.30 +1.6 s
This PR, per-run offset, wall clock moved +1 h before relaunch 18:11:26.23 18:11:31.63 +5.4 s (the per-scan version would be about −1 h)
This PR (5a14e3b), launched with wall clock +1 h, set back while running, then kill -SEGV (posthog-android sample app) 13:49:08.34 UTC 13:49:11.08 UTC +2.7 s (the startup-offset version would be about −1 h)
This PR, Pixel 10a (Android 16), clock +1 h at launch, set back while running, SIGSEGV 14:47:51.22 UTC 14:47:52.49 UTC +1.3 s
This PR, Pixel 10a, clock moved back between runs, crash after the earlier run's start 14:51:02.38 UTC 14:51:04.97 UTC +2.6 s (the start-time lookup would be about −2 min)

The seconds that remain are network delay between building the batch and the server receiving it, which the skew correction adds to every event.

Regression checks on the same build:

  • A further relaunch after a capture sends nothing, so deduplication is unchanged.
  • A JVM crash captured in-process is stored within 0.3 s of the crash.

📝 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: Human-driven (agent-assisted)

Found and fixed with Claude Code while testing posthog-js#5062 on device. The root cause was traced by reading the queued event file on a rooted emulator, which showed a correct crash timestamp with a network-time sent_at. The emulator's NTP state confirmed the clock gap.

Rejected alternatives:

  • Changing PostHogAndroidDateProvider: it uses network time on purpose.
  • Changing the server's skew correction: that would affect every SDK.

🤖 Generated with Claude Code

Exit records carry wall-clock time, but on API 33+ the batch sent_at comes
from the network-corrected date provider. Ingestion shifts each event by
timestamp - sent_at, so a crash moved by however far the two clocks
disagreed. Shift the exit timestamp into the date provider's clock; the
watermark keeps the raw exit timestamp.
@turnipdabeets
turnipdabeets requested a review from a team as a code owner September 24, 2026 16:52
@turnipdabeets turnipdabeets self-assigned this Sep 24, 2026
@greptile-apps

greptile-apps Bot commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Retrigger

The PR should not merge until recovered crashes remain correctly timed across clock-source changes and changes between crash and recovery.

Reviews (1) · Last reviewed commit: "fix(errortracking): stamp native crashes..."

…shes

Record each run's date-provider-to-wall-clock offset and apply the offset of
the run a crash happened in, so a clock change between the crash and its
recovery no longer shifts the event.

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two reproduced timestamp-correction regressions from the review of d40a8ae.

@marandaneto
marandaneto requested a review from a team September 25, 2026 08:08
@marandaneto

marandaneto commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

suggestion: Non-blocking follow-up on the clock-offset approach — This PR addresses the mismatch between wall-clock crash timestamps and the SDK’s network-time clock, but an offset saved when the app starts may be wrong when it crashes if the device time changes during that run.

Libraries such as TrueTime Android may be useful references for persisting clock information and maintaining a network-based time estimate across device-time changes. That is a reference to investigate, not a recommendation to add a dependency.

I’m not sure the extra complexity is worth it here: wall clock and network time generally agree, and when they differ the gap may be milliseconds, seconds, or minutes. We should weigh the practical impact against the implementation and maintenance cost before expanding this fix. This comment is non-blocking; documenting the limitation may be sufficient.

curious what @ioannisj say?

… them on time changes

Runs were matched to crashes by wall-clock start time, which breaks when the
clock moves backwards. The exit record carries the pid, so key runs by pid.

A run's startup offset also went stale when the wall clock changed during the
run, so the offset is now re-recorded on ACTION_TIME_CHANGED.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ioannisj

Copy link
Copy Markdown
Contributor

suggestion: Non-blocking follow-up on the clock-offset approach — This PR addresses the mismatch between wall-clock crash timestamps and the SDK’s network-time clock, but an offset saved when the app starts may be wrong when it crashes if the device time changes during that run.

Libraries such as TrueTime Android may be useful references for persisting clock information and maintaining a network-based time estimate across device-time changes. That is a reference to investigate, not a recommendation to add a dependency.

I’m not sure the extra complexity is worth it here: wall clock and network time generally agree, and when they differ the gap may be milliseconds, seconds, or minutes. We should weigh the practical impact against the implementation and maintenance cost before expanding this fix. This comment is non-blocking; documenting the limitation may be sufficient.

curious what @ioannisj say?

Yeah not sure if the complexity is needed here, documenting should be enough until we have a stronger signal?

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

Agreed on keeping this small. Keying by pid and refreshing on time changes fixes both regressions with no new dependency. The case Greptile raised is still open: a run that never got network time records 0. That applies to every event captured before the date provider gets network time, so I've left it out of this PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@turnipdabeets
turnipdabeets enabled auto-merge (squash) September 25, 2026 14:53
@turnipdabeets
turnipdabeets merged commit 11c1121 into main Sep 25, 2026
17 checks passed
@turnipdabeets
turnipdabeets deleted the fix/native-crash-timestamp-clock branch September 25, 2026 15:09
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.

3 participants