Skip to content

fix(error-tracking): install autocapture handler by default on first launch - #650

Open
ioannisj wants to merge 4 commits into
mainfrom
fix/error-tracking-first-launch-648
Open

fix(error-tracking): install autocapture handler by default on first launch#650
ioannisj wants to merge 4 commits into
mainfrom
fix/error-tracking-first-launch-648

Conversation

@ioannisj

Copy link
Copy Markdown
Contributor

💡 Motivation and Context

Fixes #648.

On the very first app launch there's no disk-cached remote config yet, so the native crash handler (PostHogErrorTrackingAutoCaptureIntegration, a Thread.UncaughtExceptionHandler) didn't install until the async /flags remote-config response arrived. Any uncaught exception in that window, from app start to the first config response, was lost. Offline made it worse: a failed first-launch fetch left autocapture off for the whole session.

This flips the gate to match the iOS fix. The handler now installs by default at setup when local errorTrackingConfig.autoCapture is on, and only skips when a remote config that actually exists (fetched this session or disk-cached from a prior launch) explicitly reports autocaptureExceptions: false. Remote config becomes a kill-switch rather than a prerequisite: if the freshly-loaded config disables autocapture, onRemoteConfig uninstalls the handler, and a failed fetch leaves the default install in place. Local autoCapture stays the primary gate, so a host that turned it off still gets no handler.

To tell "first launch, no config yet" apart from "config exists and says disabled", PostHogRemoteConfig gains hasCachedErrorTrackingConfig(), set at startup when the error-tracking config was already on disk. It's the Android analogue of iOS's hasCachedRemoteConfig.

This is the Android counterpart of iOS #551, ported from iOS #731. It also covers Flutter-on-Android, which maps captureNativeExceptions onto errorTrackingConfig.autoCapture and inherited the same gap.

💚 How did you test it?

Unit tests mirroring the iOS installation tests, in PostHogErrorTrackingAutoCaptureIntegrationTest:

  • installs by default on first launch, before remote config arrives
  • does not install when local autoCapture is off, even with no remote config
  • does not install when a cached or fetched remote config disables autocapture
  • installs when a cached remote config enables autocapture
  • onRemoteConfig uninstalls the default install when a freshly-loaded config disables autocapture, and keeps it when the config enables autocapture
  • a failed fetch (loaded = false) keeps the default install

Ran ./gradlew spotlessApply, :posthog:apiCheck, and the full :posthog:test suite, all green.

📝 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 self-assigned this Jul 22, 2026
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

posthog-android Compliance Report

Date: 2026-07-29 06:41:48 UTC
Duration: 118384ms

✅ All Tests Passed!

46/46 tests passed


Capture Tests

29/29 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields 368ms
Format Validation.Event Has Uuid 49ms
Format Validation.Event Has Lib Properties 39ms
Format Validation.Distinct Id Is String 27ms
Format Validation.Token Is Present 30ms
Format Validation.Custom Properties Preserved 29ms
Format Validation.Event Has Timestamp 36ms
Retry Behavior.Retries On 503 7042ms
Retry Behavior.Does Not Retry On 400 4023ms
Retry Behavior.Does Not Retry On 401 4023ms
Retry Behavior.Respects Retry After Header 7023ms
Retry Behavior.Implements Backoff 17020ms
Retry Behavior.Retries On 500 7017ms
Retry Behavior.Retries On 502 7015ms
Retry Behavior.Retries On 504 7020ms
Retry Behavior.Max Retries Respected 17034ms
Deduplication.Generates Unique Uuids 39ms
Deduplication.Preserves Uuid On Retry 7016ms
Deduplication.Preserves Uuid And Timestamp On Retry 12029ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 7019ms
Deduplication.No Duplicate Events In Batch 34ms
Deduplication.Different Events Have Different Uuids 21ms
Compression.Sends Gzip When Enabled 19ms
Batch Format.Uses Proper Batch Structure 17ms
Batch Format.Flush With No Events Sends Nothing 13ms
Batch Format.Multiple Events Batched Together 31ms
Error Handling.Does Not Retry On 403 4017ms
Error Handling.Does Not Retry On 413 4019ms
Error Handling.Retries On 408 5027ms

Feature_Flags Tests

17/17 tests passed

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

@ioannisj
ioannisj force-pushed the fix/error-tracking-first-launch-648 branch from bf14bc3 to 81ce5d7 Compare July 24, 2026 06:56
@ioannisj
ioannisj marked this pull request as ready for review July 27, 2026 12:39
@ioannisj
ioannisj requested a review from a team as a code owner July 27, 2026 12:39
@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor
Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
posthog/src/main/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegration.kt:45-50
**Cross-instance handler ownership breaks**

When two PostHog instances start with local autocapture enabled before remote configuration resolves, the first integration sets the process-wide installation flag and the second skips installation without saving the previous handler. Closing the second instance then observes the shared flag as installed, restores its null per-instance handler, and removes exception capture from the still-active first client while also discarding the original JVM handler.

Reviews (1): Last reviewed commit: "fix(error-tracking): cache disabled auto..." | Re-trigger Greptile

@turnipdabeets turnipdabeets 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.

LGTM, left some comments. Do you think we need to address RN as well?

Comment thread .changeset/error-tracking-first-launch-crashes.md
Comment thread .changeset/error-tracking-first-launch-crashes.md Outdated
Comment thread posthog/src/main/java/com/posthog/internal/PostHogRemoteConfig.kt Outdated
@ioannisj

Copy link
Copy Markdown
Contributor Author

LGTM, left some comments. Do you think we need to address RN as well?

Yeah and we'd probably have to gate flutter (dart side) exceptions as well I think

if (adapterExceptionHandler.getDefaultUncaughtExceptionHandler() === this) {
adapterExceptionHandler.setDefaultUncaughtExceptionHandler(defaultExceptionHandler)
}
integrationInstalled = false

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.

The guard does fix the clobbering — confirmed with a fake adapter, the app's handler now survives. But when the restore is skipped we still set integrationInstalled = false while we're actually still linked as that handler's delegate, and two things follow from it.

uncaughtException has no disabled check, so we keep capturing $exception for a project that just turned autocapture off — the kill-switch stops killing. And because the flag says uninstalled, a later onRemoteConfig re-enable runs install() again and sets defaultExceptionHandler to the app handler that delegates back to us; one crash then cycles (my probe re-entered the chain 21 times before I capped it, so in production that's a StackOverflowError inside the crash handler).

I think we need to separate "are we linked" from "are we capturing":

captureEnabled = false
if (adapterExceptionHandler.getDefaultUncaughtExceptionHandler() === this) {
    adapterExceptionHandler.setDefaultUncaughtExceptionHandler(defaultExceptionHandler)
    integrationInstalled = false // only uninstalled once we're actually out of the chain
}

with uncaughtException bailing when captureEnabled is false, and install() flipping it back on for the already-installed case.

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.

Bug: error tracking autocapture misses crashes on first launch before remote config resolves

2 participants