Repository navigation
fix: remove the RCTFatalException default from ignoredExceptionTypes - #894
Conversation
React Native names the NSException it raises for a fatal JS error "RCTFatalException: <message>", and ignoredExceptionTypes matches exception types exactly, so the default never matched a real React Native crash and filtered nothing. Default the option to [] and describe only what it does. The React Native plugin deduplicates native re-reports of fatal JS errors itself in beforeSend. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
posthog-ios Compliance ReportDate: 2026-10-02 14:20:47 UTC
|
| Test | Status | Duration |
|---|---|---|
| Format Validation.Event Has Required Fields | ✅ | 2732ms |
| Format Validation.Event Has Uuid | ✅ | 2725ms |
| Format Validation.Event Has Lib Properties | ✅ | 2760ms |
| Format Validation.Distinct Id Is String | ✅ | 347ms |
| Format Validation.Token Is Present | ✅ | 2705ms |
| Format Validation.Custom Properties Preserved | ✅ | 2750ms |
| Format Validation.Event Has Timestamp | ✅ | 2750ms |
| Retry Behavior.Retries On 503 | ✅ | 11459ms |
| Retry Behavior.Does Not Retry On 400 | ✅ | 4756ms |
| Retry Behavior.Does Not Retry On 401 | ✅ | 2348ms |
| Retry Behavior.Respects Retry After Header | ✅ | 7671ms |
| Retry Behavior.Implements Backoff | ✅ | 21333ms |
| Retry Behavior.Retries On 500 | ✅ | 5469ms |
| Retry Behavior.Retries On 502 | ✅ | 9347ms |
| Retry Behavior.Retries On 504 | ✅ | 8768ms |
| Retry Behavior.Max Retries Respected | ❌ | 21301ms |
| Deduplication.Generates Unique Uuids | ✅ | 2725ms |
| Deduplication.Preserves Uuid On Retry | ✅ | 9217ms |
| Deduplication.Preserves Uuid And Timestamp On Retry | ✅ | 16685ms |
| Deduplication.Preserves Uuid And Timestamp On Batch Retry | ✅ | 7670ms |
| Deduplication.No Duplicate Events In Batch | ✅ | 2932ms |
| Deduplication.Different Events Have Different Uuids | ✅ | 2674ms |
| Compression.Sends Gzip When Enabled | ✅ | 2750ms |
| Batch Format.Uses Proper Batch Structure | ✅ | 375ms |
| Batch Format.Flush With No Events Sends Nothing | ✅ | 290ms |
| Batch Format.Multiple Events Batched Together | ✅ | 2781ms |
| Error Handling.Does Not Retry On 403 | ✅ | 4767ms |
| Error Handling.Does Not Retry On 413 | ✅ | 4737ms |
| Error Handling.Retries On 408 | ✅ | 9200ms |
Failures
retry_behavior.max_retries_respected
Expected 4 requests, got 6
Feature_Flags Tests
✅ 16/16 tests passed
View Details
| Test | Status | Duration |
|---|---|---|
| Request Payload.Request With Person Properties Device Id | ✅ | 2809ms |
| Request Payload.Flags Request Uses V2 Query Param | ✅ | 2782ms |
| Request Payload.Flags Request Hits Flags Path Not Decide | ✅ | 2766ms |
| Request Payload.Flags Request Omits Authorization Header | ✅ | 2738ms |
| Request Payload.Token In Flags Body Matches Init | ✅ | 2763ms |
| Request Payload.Groups Round Trip | ✅ | 2737ms |
| Request Payload.Groups Default To Empty Object | ✅ | 2699ms |
| Request Payload.Person Properties Distinct Id Auto Populated When Caller Omits It | ✅ | 2696ms |
| Request Payload.Disable Geoip False Propagates As Geoip Disable False | ✅ | 2780ms |
| Request Payload.Disable Geoip Omitted Defaults To False | ✅ | 2765ms |
| Request Payload.Flag Keys To Evaluate Contains Only Requested Key | ✅ | 2758ms |
| Request Lifecycle.No Flags Request On Init Alone | ✅ | 60ms |
| Request Lifecycle.No Flags Request On Normal Capture | ✅ | 2643ms |
| Request Lifecycle.Two Flag Calls Produce Two Remote Requests | ✅ | 5371ms |
| Request Lifecycle.Mock Response Value Is Returned To Caller | ✅ | 2657ms |
| Side Effect Events.Get Feature Flag Captures Feature Flag Called Event | ✅ | 2994ms |
There was a problem hiding this comment.
Yeah I think this makes sense. I ran a device repro to check and try to validate the claim
- New arch on RN 0.76.9, 0.79.6 and 0.81.6: the native report is a bare
SIGABRT, so the duplicates do ship - RN 0.87.1: the
NSExceptionsurvives (react/react-native#56265) asRCTFatalException: Unhandled JS Exception: …. That still isn't an exact match, and the plugin'sbeforeSenddrops it - With the plugin's dedup turned off (to simulate older plugin but latest posthog-ios), the old default still didn't catch it
We'd probably need to address the duplicate handling plugin side though on older new arch versions. Maybe hooking RCTSetFatalHandler there could close it?
| 'posthog-ios': patch | ||
| --- | ||
|
|
||
| Remove the `RCTFatalException` default from `errorTrackingConfig.ignoredExceptionTypes` and correct its documentation. The default never matched the exception React Native raises for a fatal JS error, which is named `"RCTFatalException: <message>"`, so it filtered nothing. The option now defaults to `[]`. Deduplicating React Native crashes is done by the PostHog React Native plugin, not by this option. If your app relied on filtering an exception named exactly `RCTFatalException`, add it back with `config.errorTrackingConfig.ignoredExceptionTypes = ["RCTFatalException"]`. |
There was a problem hiding this comment.
[suggestion] Could we qualify the plugin line here? It only dedups on the old arch and on new arch RN 0.83.5+ / 0.85+.
On new arch 0.76 to 0.83.4 and 0.84.x, RN converts the NSException into a jsi::JSError (fixed upstream in react/react-native#56265), so the native report is a bare SIGABRT and the plugin has nothing to match. I reproduced the native duplicate on device on 0.76.9, 0.79.6 and 0.81.6; on 0.87.1 the plugin dropped it.
Maybe something like: "The PostHog React Native plugin removes these duplicates where it can (old architecture, and new architecture on RN 0.83.5+ / 0.85+). On earlier new-architecture versions a native SIGABRT duplicate can still appear."
Same nuance for the PR body's "doesn't raise an NSException at all": RCTFatal does raise one, older RN just converts it before it reaches the crash reporter.
| /// than only the outermost entry so a wrapped exception whose underlying | ||
| /// cause is, e.g., `RCTFatalException` is still suppressed. Match is | ||
| /// cause has an ignored type is still suppressed. Match is | ||
| /// case-sensitive and exact (the field is a class name, not free text). |
There was a problem hiding this comment.
[nit] This still says "(the field is a class name, not free text)", and the test label at PostHogTests/PostHogErrorTrackingIgnoredTypesTest.swift:73 says "NSException class names are stable identifiers". That's the assumption that made the old default dead: an NSException's type is its name, and RCTFatal puts free text in it. Worth rewording to "for an NSException this is its name, which can embed free text" and relabelling the test to "match is case-sensitive"?
There was a problem hiding this comment.
Done in ad5e018. The doc comment now says an NSException's type is its name, which can embed free text, and the test label is just "match is case-sensitive".
you sure? why did we open the issues and merged the PRs then? i recall asking you to check dupe reports |
|
@marandaneto You're right that the duplicates were real. They were fixed by the plugin, not by this default. Timeline:
The real exception is named We checked this on devices three times: my simulator and iPhone runs, and @ioannisj's run with the plugin's dedup turned off. In all of them, What's still open: on the new architecture, RN 0.76 to 0.83.4, the native report is a bare |
The changeset now says which React Native versions the plugin dedups on. The matcher doc and test label no longer claim an NSException type is a stable class name; its name can embed free text. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
@ioannisj I’ll wait to merge until you get a change to see the comments. |
Yeah I think this looks okay to merge. From my runs looks like Btw the reason I check with dedup off plugin-side is to check what happens if an old plugin version is installed (without the beforeSend hook) witht he latest pothog-ios. Even then, the duplicate goes through so ignoredExceptionTypes never really matched anything |
💡 Motivation and Context
PostHogErrorTrackingConfig.ignoredExceptionTypeshas defaulted to["RCTFatalException"]since 3.64.0 (#666). The docs say this deduplicates React Native crashes. It doesn't. It never matches a real React Native crash, so it filters nothing.RCTFataldoesn't name theNSExceptionRCTFatalException. It names it"RCTFatalException: <error description>"(React/Base/RCTAssert.m:133:[NSString stringWithFormat:@"%@: %@", RCTFatalExceptionName, error.localizedDescription]). React Native itself matches this name by prefix.ignoredExceptionTypesmatches exactly.exceptionListMatchesIgnoredTypesdoes aSet.containson each$exception_list[*].type, andcaptureInternalapplies the same list. A crash report'stypeis the raw exception name.NSExceptionat all. The app aborts on a C++facebook::jsi::JSError, and the crash report's type isSIGABRT.ignoredExceptionTypesconfig to skip RN-duplicate native crashes (closes #653) #666 used synthetic events typed exactly"RCTFatalException", so they passed without testing a real crash.beforeSenddrops$exceptionevents that mentionUnhandled JS Exception,ExceptionsManager.reportExceptionorfacebook::jsi::JSError. It never setsignoredExceptionTypeson iOS.ignoredExceptionTypesdefaults to empty, and the RN plugin addsJavascriptExceptionthere itself. No sdk-specs spec defines this option.This PR:
[]$exceptionevents whose$exception_list[*].type(anNSException'sname) exactly matches an entry, case-sensitivelyEffect: none expected. The only apps affected are ones that raise or report an exception named exactly
RCTFatalException, and we don't know of any. They can add it back:config.errorTrackingConfig.ignoredExceptionTypes = ["RCTFatalException"].💚 How did you test it?
Unit tests and builds
[].genericCaptureDropsIgnoredTyperelied on the default. It now sets the list explicitly.make testpassed (919 tests in 148 suites),make buildSdkpassed,make apiCheckpassed (snapshot up to date; only the default value changed),make lintpassed (only existing warnings).React Native app on the iOS simulator
First on an iPhone 17 simulator (iOS 26.4), then on a physical iPhone (next section).
Setup:
example-rn-native-pluginapp from posthog-js main (RN 0.79.6, Hermes, Release build)posthog-react-native4.77.0 witherrorTracking.autocapture: { uncaughtExceptions: true, nativeCrashes: true }origin/main(default["RCTFatalException"]) and once at this branch (default[])Each run throws an error from a
setTimeout, waits for the crash, then relaunches so the crash report gets sent.What I observed:
$exception_list[*].type$exceptionsentfacebook::jsi::JSError(libc++abi abort)SIGABRTError)SIGABRTError)RCT_NEW_ARCH_ENABLED=0)NSExceptionRCTFatalException: Unhandled JS Exception: Error: …, js engine: hermesRCTFatalException: Unhandled JS Exception: Error: …, js engine: hermesError)beforeSendRCT_NEW_ARCH_ENABLED=0)Error)beforeSendmainshows the crash report reachingbeforeSendwithignoredTypes=["RCTFatalException"]. It says "Crash report processed and captured", never "Crash report skipped: exception type is in errorTrackingConfig.ignoredExceptionTypes". So the old default didn't fire on a real React Native fatal; the plugin's marker match dropped it.beforeSendin a local copy. That change isn't in any repo.mainand this branch sent the same requests.React Native app on a physical iPhone
I reran the same app on an iPhone 14 Pro Max (iOS 26.5.2, connected over USB), signed with a development team. All four builds were Release, built once at
origin/mainand once at this branch, for each architecture.$exception_list[*].type$exceptionSIGABRTSIGABRTRCTFatalException: Unhandled JS Exception: Error: …, js engine: hermesbeforeSendbeforeSendmain, the crash report reachedbeforeSendwithignoredTypes=["RCTFatalException"]. It was "processed and captured", not "skipped", and the plugin's marker match then dropped it. Same as on the simulator.NSURLErrorDomain -1009,unsatisfied (Local network prohibited).xcrun devicectl device copy from), together with the SDK's debug log ("Queued event" / "was dropped").$exceptionpayloads frommain(on-disk queue) and this branch (received by the mock server). They have the same property keys and the same types, values, mechanism and stack-frame modules. They differ only in run-specific details: addresses, build path, the crashing thread's id, debug images and session timing.What I didn't test:
Other wrappers (from reading
origin/mainsource)ignoredExceptionTypes. iOS native crash capture isconfig.errorTrackingConfig.autoCapture = captureNativeExceptions.ignoredExceptionTypes(Kotlin classsimpleName/qualifiedName) to the native default. So a KMP iOS app's list loses theRCTFatalExceptionentry. That only matters for a Kotlin class named exactlyRCTFatalException. KMP's own default isemptyList(), and no KMP test checks the merged native list.@posthog/react-native-pluginandposthog-react-native): on iOS,RCTFatalExceptionappears only in a plugin comment. Neither package setsignoredExceptionTypes. The Android plugin addsJavascriptException. Native crash capture on React Native is opt-in (errorTracking.autocapture.nativeCrashes).📝 Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
I used Claude Code to read the source of posthog-ios, react-native, the posthog-js React Native packages, posthog-android, posthog-flutter and posthog-kmp, and to make the change. It also ran the before/after React Native check on the simulator and a physical iPhone.
I chose to make the default empty rather than switch to prefix matching. Prefix matching would still miss the new architecture's
SIGABRT, and the React Native plugin already handles deduplication where it can.🤖 Generated with Claude Code