feat(errortracking): ignoredExceptionTypes config to skip RN-duplicate JS errors#569
feat(errortracking): ignoredExceptionTypes config to skip RN-duplicate JS errors#569tsushanth wants to merge 2 commits into
Conversation
Prompt To Fix All With AIFix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
posthog/src/main/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegration.kt:88-92
When the cause chain is what triggered the skip (not the direct throwable), this log reports the outer wrapper's class name — e.g. `java.lang.RuntimeException` — even though that class is not in `ignoredExceptionTypes`. A developer looking at these logs would wonder why `RuntimeException` was suppressed, since they only configured `com.facebook.react.common.JavascriptException`. Logging `throwable.javaClass.name` is only accurate when the outermost exception matched; the message should be phrased to cover the cause-chain case too.
```suggestion
} else {
config.logger.log(
"Skipping autocapture: ${throwable.javaClass.name} (or a cause in its chain) matches ignoredExceptionTypes",
)
}
```
### Issue 2 of 3
posthog/src/test/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegrationTest.kt:157-226
Prefer parameterised tests — the three new tests share identical setup (`remoteConfigHolder`, `isAutocaptureExceptionsEnabled`, `getDefaultUncaughtExceptionHandler`) and verify the same contract with different inputs. At minimum, the "skips capture" (direct match) and "still captures" (no match) cases are a natural `@ParameterizedTest` pair: the only axes that vary are the throwable type, the ignored-list contents, and whether `captureException` is expected to be called. Collapsing them reduces boilerplate and makes it immediately clear when a new edge case is added that it's intentionally distinct from existing ones. The cause-chain test is structurally different enough to stay standalone.
### Issue 3 of 3
posthog/src/test/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegrationTest.kt:228-234
The comment says "a local class named the same way", but the stub's actual JVM name is `com.posthog.errortracking.PostHogErrorTrackingAutoCaptureIntegrationTest$ReactNativeJavascriptExceptionStub` — nothing like `com.facebook.react.common.JavascriptException`. The tests work correctly because they pass `ReactNativeJavascriptExceptionStub::class.java.name` as the filter value, not the real RN FQCN. The comment should reflect the actual design: a stub with an arbitrary local name, registered under that same local name, so the name-based matching code path is exercised correctly without pulling in the RN dependency.
```suggestion
/**
* Local stand-in for `com.facebook.react.common.JavascriptException`. The real type
* lives in React Native, which isn't (and shouldn't be) a test dependency of the
* SDK. The ignored-exception filter is purely class-name based: tests register this
* stub's own JVM name as the ignored FQCN, which exercises the matching logic without
* requiring RN on the classpath. The stub name intentionally differs from the real RN
* class name; the code path is the same either way.
*/
private class ReactNativeJavascriptExceptionStub(message: String) : RuntimeException(message)
```
Reviews (1): Last reviewed commit: "feat(errortracking): ignoredExceptionTyp..." | Re-trigger Greptile |
| } else { | ||
| config.logger.log( | ||
| "Skipping autocapture for ignored exception type: ${throwable.javaClass.name}", | ||
| ) | ||
| } |
There was a problem hiding this comment.
When the cause chain is what triggered the skip (not the direct throwable), this log reports the outer wrapper's class name — e.g.
java.lang.RuntimeException — even though that class is not in ignoredExceptionTypes. A developer looking at these logs would wonder why RuntimeException was suppressed, since they only configured com.facebook.react.common.JavascriptException. Logging throwable.javaClass.name is only accurate when the outermost exception matched; the message should be phrased to cover the cause-chain case too.
| } else { | |
| config.logger.log( | |
| "Skipping autocapture for ignored exception type: ${throwable.javaClass.name}", | |
| ) | |
| } | |
| } else { | |
| config.logger.log( | |
| "Skipping autocapture: ${throwable.javaClass.name} (or a cause in its chain) matches ignoredExceptionTypes", | |
| ) | |
| } |
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/src/main/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegration.kt
Line: 88-92
Comment:
When the cause chain is what triggered the skip (not the direct throwable), this log reports the outer wrapper's class name — e.g. `java.lang.RuntimeException` — even though that class is not in `ignoredExceptionTypes`. A developer looking at these logs would wonder why `RuntimeException` was suppressed, since they only configured `com.facebook.react.common.JavascriptException`. Logging `throwable.javaClass.name` is only accurate when the outermost exception matched; the message should be phrased to cover the cause-chain case too.
```suggestion
} else {
config.logger.log(
"Skipping autocapture: ${throwable.javaClass.name} (or a cause in its chain) matches ignoredExceptionTypes",
)
}
```
How can I resolve this? If you propose a fix, please make it concise.| fun `uncaughtException skips capture when throwable class is in ignoredExceptionTypes`() { | ||
| whenever(mockConfig.remoteConfigHolder).thenReturn(mockRemoteConfig) | ||
| whenever(mockRemoteConfig.isAutocaptureExceptionsEnabled()).thenReturn(true) | ||
| whenever(mockAdapter.getDefaultUncaughtExceptionHandler()).thenReturn(mockExceptionHandler) | ||
|
|
||
| val thread = Thread.currentThread() | ||
| // Simulates the React Native scenario: posthog-js has already captured the | ||
| // fatal JS error; React Native rethrows it natively as JavascriptException, | ||
| // and the SDK should not emit a duplicate $exception event. | ||
| val throwable = ReactNativeJavascriptExceptionStub("Unhandled JS Exception: ReferenceError") | ||
|
|
||
| val integration = | ||
| getSut(ignoredExceptionTypes = listOf(ReactNativeJavascriptExceptionStub::class.java.name)) | ||
| integration.install(mockPostHog) | ||
|
|
||
| integration.uncaughtException(thread, throwable) | ||
|
|
||
| verify(mockPostHog, never()).captureException(any<PostHogThrowable>(), anyOrNull()) | ||
| // The downstream handler still runs so the process terminates / RN's red-box | ||
| // appears as it would without PostHog installed. | ||
| verify(mockExceptionHandler).uncaughtException(thread, throwable) | ||
|
|
||
| integration.uninstall() | ||
| } | ||
|
|
||
| @Test | ||
| fun `uncaughtException skips capture when ignored class is anywhere in the cause chain`() { | ||
| whenever(mockConfig.remoteConfigHolder).thenReturn(mockRemoteConfig) | ||
| whenever(mockRemoteConfig.isAutocaptureExceptionsEnabled()).thenReturn(true) | ||
| whenever(mockAdapter.getDefaultUncaughtExceptionHandler()).thenReturn(mockExceptionHandler) | ||
|
|
||
| val thread = Thread.currentThread() | ||
| // The outermost type is RuntimeException, not the ignored type, but the | ||
| // cause chain contains the ignored type. Real RN apps wrap the JS exception | ||
| // inside platform-level wrappers, so walking the chain matters. | ||
| val inner = ReactNativeJavascriptExceptionStub("inner") | ||
| val outer = RuntimeException("outer", inner) | ||
|
|
||
| val integration = | ||
| getSut(ignoredExceptionTypes = listOf(ReactNativeJavascriptExceptionStub::class.java.name)) | ||
| integration.install(mockPostHog) | ||
|
|
||
| integration.uncaughtException(thread, outer) | ||
|
|
||
| verify(mockPostHog, never()).captureException(any<PostHogThrowable>(), anyOrNull()) | ||
| verify(mockExceptionHandler).uncaughtException(thread, outer) | ||
|
|
||
| integration.uninstall() | ||
| } | ||
|
|
||
| @Test | ||
| fun `uncaughtException still captures when throwable class is not in ignoredExceptionTypes`() { | ||
| whenever(mockConfig.remoteConfigHolder).thenReturn(mockRemoteConfig) | ||
| whenever(mockRemoteConfig.isAutocaptureExceptionsEnabled()).thenReturn(true) | ||
| whenever(mockAdapter.getDefaultUncaughtExceptionHandler()).thenReturn(mockExceptionHandler) | ||
|
|
||
| val thread = Thread.currentThread() | ||
| val throwable = RuntimeException("Genuine native crash") | ||
|
|
||
| val integration = | ||
| getSut(ignoredExceptionTypes = listOf("com.facebook.react.common.JavascriptException")) | ||
| integration.install(mockPostHog) | ||
|
|
||
| integration.uncaughtException(thread, throwable) | ||
|
|
||
| verify(mockPostHog).captureException(any<PostHogThrowable>(), anyOrNull()) | ||
| verify(mockExceptionHandler).uncaughtException(thread, throwable) | ||
|
|
||
| integration.uninstall() | ||
| } |
There was a problem hiding this comment.
Prefer parameterised tests — the three new tests share identical setup (
remoteConfigHolder, isAutocaptureExceptionsEnabled, getDefaultUncaughtExceptionHandler) and verify the same contract with different inputs. At minimum, the "skips capture" (direct match) and "still captures" (no match) cases are a natural @ParameterizedTest pair: the only axes that vary are the throwable type, the ignored-list contents, and whether captureException is expected to be called. Collapsing them reduces boilerplate and makes it immediately clear when a new edge case is added that it's intentionally distinct from existing ones. The cause-chain test is structurally different enough to stay standalone.
Context Used: Do not attempt to comment on incorrect alphabetica... (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/src/test/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegrationTest.kt
Line: 157-226
Comment:
Prefer parameterised tests — the three new tests share identical setup (`remoteConfigHolder`, `isAutocaptureExceptionsEnabled`, `getDefaultUncaughtExceptionHandler`) and verify the same contract with different inputs. At minimum, the "skips capture" (direct match) and "still captures" (no match) cases are a natural `@ParameterizedTest` pair: the only axes that vary are the throwable type, the ignored-list contents, and whether `captureException` is expected to be called. Collapsing them reduces boilerplate and makes it immediately clear when a new edge case is added that it's intentionally distinct from existing ones. The cause-chain test is structurally different enough to stay standalone.
**Context Used:** Do not attempt to comment on incorrect alphabetica... ([source](https://app.greptile.com/review/custom-context?memory=instruction-0))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| /** | ||
| * Local stand-in for `com.facebook.react.common.JavascriptException`. The real type | ||
| * lives in React Native, which isn't (and shouldn't be) a test dependency of the | ||
| * SDK. The ignored-exception filter is purely class-name based, so a local class | ||
| * named the same way exercises the same code path without dragging in RN. | ||
| */ | ||
| private class ReactNativeJavascriptExceptionStub(message: String) : RuntimeException(message) |
There was a problem hiding this comment.
The comment says "a local class named the same way", but the stub's actual JVM name is
com.posthog.errortracking.PostHogErrorTrackingAutoCaptureIntegrationTest$ReactNativeJavascriptExceptionStub — nothing like com.facebook.react.common.JavascriptException. The tests work correctly because they pass ReactNativeJavascriptExceptionStub::class.java.name as the filter value, not the real RN FQCN. The comment should reflect the actual design: a stub with an arbitrary local name, registered under that same local name, so the name-based matching code path is exercised correctly without pulling in the RN dependency.
| /** | |
| * Local stand-in for `com.facebook.react.common.JavascriptException`. The real type | |
| * lives in React Native, which isn't (and shouldn't be) a test dependency of the | |
| * SDK. The ignored-exception filter is purely class-name based, so a local class | |
| * named the same way exercises the same code path without dragging in RN. | |
| */ | |
| private class ReactNativeJavascriptExceptionStub(message: String) : RuntimeException(message) | |
| /** | |
| * Local stand-in for `com.facebook.react.common.JavascriptException`. The real type | |
| * lives in React Native, which isn't (and shouldn't be) a test dependency of the | |
| * SDK. The ignored-exception filter is purely class-name based: tests register this | |
| * stub's own JVM name as the ignored FQCN, which exercises the matching logic without | |
| * requiring RN on the classpath. The stub name intentionally differs from the real RN | |
| * class name; the code path is the same either way. | |
| */ | |
| private class ReactNativeJavascriptExceptionStub(message: String) : RuntimeException(message) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/src/test/java/com/posthog/errortracking/PostHogErrorTrackingAutoCaptureIntegrationTest.kt
Line: 228-234
Comment:
The comment says "a local class named the same way", but the stub's actual JVM name is `com.posthog.errortracking.PostHogErrorTrackingAutoCaptureIntegrationTest$ReactNativeJavascriptExceptionStub` — nothing like `com.facebook.react.common.JavascriptException`. The tests work correctly because they pass `ReactNativeJavascriptExceptionStub::class.java.name` as the filter value, not the real RN FQCN. The comment should reflect the actual design: a stub with an arbitrary local name, registered under that same local name, so the name-based matching code path is exercised correctly without pulling in the RN dependency.
```suggestion
/**
* Local stand-in for `com.facebook.react.common.JavascriptException`. The real type
* lives in React Native, which isn't (and shouldn't be) a test dependency of the
* SDK. The ignored-exception filter is purely class-name based: tests register this
* stub's own JVM name as the ignored FQCN, which exercises the matching logic without
* requiring RN on the classpath. The stub name intentionally differs from the real RN
* class name; the code path is the same either way.
*/
private class ReactNativeJavascriptExceptionStub(message: String) : RuntimeException(message)
```
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Address two of three Greptile review notes on PR PostHog#569: 1. The "skipping autocapture" log message previously read as if the throwable's outermost class was always the matched ignoredType. A cause-chain match would report e.g. `java.lang.RuntimeException` as suppressed even when only `JavascriptException` was in the ignore list. Reworded to surface that "the throwable or a cause in its chain" matched. 2. The `ReactNativeJavascriptExceptionStub` doc comment claimed it was "named the same way" as the real RN class — it isn't; the stub's JVM name lives under the test class. Restated the actual design: the stub's own FQCN is what tests register as the ignored value, exercising the same code path without needing RN on the classpath. Greptile's third note (parameterising the new tests) is intentionally deferred — the individual test names ("skips capture when…", "still captures when…", etc.) document distinct behaviours and read more clearly than a `@ParameterizedTest` table would. Happy to revisit if the maintainer prefers the table form.
|
Thanks @greptile-apps — pushed
Issue 2 (parameterising the new tests): intentionally deferred. The individual test names — All 17 tests still pass after the changes. |
|
Both fixes look correct. Issue 1: The log now reads Issue 3: The stub doc comment correctly describes the design: tests register the stub's own JVM name as the ignored FQCN, exercising the name-based matching without pulling RN onto the classpath. Issue 2 (deferred): The rationale holds. These aren't symmetric variants — the cause-chain test threads LGTM on Tip: You can customize Greptile's behavior for this repo with |
|
what i'd do differently is to drop after capturing the error, where we do the beforeSend as well |
|
Good call — pushed d11c33d restructuring the filter to live at the What moved
Tests
Full posthog module suite passes locally: (The greptile "non-parameterised tests" P2 from the earlier review still applies to the |
|
|
||
| val list = event.properties?.get("\$exception_list") as? List<*> ?: return event | ||
| for (entry in list) { | ||
| val type = (entry as? Map<*, *>)?.get("type") as? String ?: continue |
There was a problem hiding this comment.
that wont work, types might be minified/not available, we have to match the type with the instance type
| * | ||
| * Defaults to empty. | ||
| */ | ||
| public val ignoredExceptionTypes: MutableList<String> = mutableListOf(), |
There was a problem hiding this comment.
I think I agree with @marandaneto on this, string matching will probably not work because of minification. So this should probably take a Class not a String and check via isInstance. This means that we can't use beforeSend to run these checks.
I believe we can drop from
which is called for both manual and automatic error captureswe'll want to keep the cause-chain walk too cause we'll need that for PostHogThrowable
…icate native crashes (closes PostHog#653) React Native rethrows fatal JS errors via `RCTFatal(...)` as an uncaught `NSException` named `RCTFatalException`. When the `@posthog/react-native-plugin` enables native crash autocapture (`errorTracking.autocapture.nativeCrashes`), the iOS crash reporter captures that as a separate `$exception` event with a native stack trace — duplicating the event the JS layer has already captured with its own JS stack trace. The result is two `$exception` events for one logical error, with the duplicate carrying less useful context. sentry-react-native solves the equivalent on its iOS side with `addIgnoredExceptionForType(...)`; posthog-android added a parallel `errorTrackingConfig.ignoredExceptionTypes` filter (PostHog/posthog-android#569). This commit brings the same lever to posthog-ios so the JS-side event can be the single source of truth without forcing the React Native plugin to disable native crash autocapture entirely. Implementation: - PostHog/ErrorTracking/PostHogErrorTrackingConfig.swift: new `@objc public var ignoredExceptionTypes: [String] = []` documenting the RN use case and pointing at the parity rule in sentry-react-native + posthog-android. - PostHog/ErrorTracking/PostHogErrorTrackingAutoCaptureIntegration.swift: after `PostHogCrashReportProcessor.processReport(...)` runs and before `captureInternal("$exception", ...)`, walk the produced `$exception_list` for any `type` that matches an entry in `ignoredExceptionTypes` and short-circuit the capture if it does. Walks the full list (not just the outermost entry) so a wrapped RCTFatalException underneath an outer NSException wrapper is still suppressed. - PostHogTests/PostHogErrorTrackingIgnoredTypesTest.swift: new Swift Testing suite with 7 cases — empty list is a no-op, outer-type match suppresses, inner-chain match suppresses, mismatched types pass through, missing/empty `$exception_list` are safe, match is case-sensitive (NSException class names are stable identifiers), and the config field defaults to empty so the change is fully backwards-compatible. `swift build` clean. Could not run `swift test` to completion locally because the main branch's `PostHogSessionManagerTest` has pre-existing unrelated compile errors (`value of type 'PostHogSDK' has no member 'getSessionManager'`); CI should handle the test target correctly. Pairs with: PostHog/posthog-android#567 (the Android side of the same issue, fixed by PostHog/posthog-android#569).
…rowable> Per @marandaneto and @ioannisj's reviews on PostHog#569: string matching against fully-qualified class names is unsafe under R8 / ProGuard minification — the consumer app's class names get renamed at build time while the SDK-side ignore list stays as the unminified source names, so the filter silently stops matching. Switch the public API to `MutableList<Class<out Throwable>>` and move the check from a `PostHogBeforeSend` hook into `PostHog.captureException` itself, where the original `Throwable` is still in scope and `Class.isInstance` can compare by reference. The check walks the cause chain (bounded by `MAX_CAUSE_DEPTH = 32` to guard against cyclic chains), so a wrapped throwable — including the `PostHogThrowable` created by the uncaught handler — still matches when its inner cause does. Drop the now-dead `PostHogIgnoredExceptionTypesBeforeSend` class and its register/unregister wiring on `PostHogErrorTrackingAutoCaptureIntegration`. Tests pivot accordingly: `PostHogIgnoredExceptionTypesBeforeSendTest` goes away, two BeforeSend-shaped tests on `PostHogErrorTrackingAutoCaptureIntegrationTest` go away, and three new tests on `PostHogTest` cover the direct-match drop, the cause-chain drop, and the keep-when-not-matching path. 696 tests pass under `./gradlew :posthog:test`; spotless format + api dump up to date.
|
Thanks @marandaneto and @ioannisj. Refactored to take
Ready for another look whenever you have a moment. |
There was a problem hiding this comment.
Thanx @tsushanth, this is almost there. Left some last few comments + some merge conflicts to resolve
| val ignored = config?.errorTrackingConfig?.ignoredExceptionTypes | ||
| if (!ignored.isNullOrEmpty()) { | ||
| val match = findIgnoredTypeInCauseChain(throwable, ignored) | ||
| if (match != null) { | ||
| config?.logger?.log( | ||
| "Skipping \$exception: ${match.name} (or a cause in its chain) matches ignoredExceptionTypes", | ||
| ) | ||
| return | ||
| } | ||
| } |
There was a problem hiding this comment.
Let's move this inside try-catch below?
| * Setting | ||
| * `ignoredExceptionTypes = mutableListOf(JavascriptException::class.java)` | ||
| * suppresses the native duplicate so the JS-captured event is the | ||
| * single source of truth. Mirrors `sentry-android`'s |
There was a problem hiding this comment.
Let's remove reference in public docs and move to header attribution if indeed necessary
| ### Minor Changes | ||
|
|
||
| - Error tracking: add `errorTrackingConfig.ignoredExceptionTypes` (a | ||
| `MutableList<Class<out Throwable>>`) so callers can suppress duplicate | ||
| captures by type. The list is consulted by `PostHog.captureException` — | ||
| both the autocapture handler and direct calls — and walks the cause | ||
| chain, so a wrapped throwable still matches. Matching is by `Class` | ||
| reference, not by name, so R8 / ProGuard renaming can't silently break | ||
| the filter. Primary use case is dropping the native rethrow of fatal JS | ||
| errors when the React Native plugin already captures them on the JS | ||
| side. Closes #567. |
There was a problem hiding this comment.
Lets not edit changelog directly but provide a changeset entry. Refer to RELEASING.md
| * Throwable classes to skip during capture (both autocapture and manual | ||
| * [PostHog.captureException] calls). | ||
| * | ||
| * For every capture, the throwable and every entry of its cause chain | ||
| * are tested with [Class.isInstance]; if any link matches an entry in | ||
| * this list, the SDK will not emit a `$exception` event. The next | ||
| * exception handler in the chain is still invoked, so the process | ||
| * termination / RN red-box / etc. behaves as before. | ||
| * | ||
| * Matching is done by [Class] reference (not by name) so that R8 / | ||
| * ProGuard minification renaming a class can't silently break the | ||
| * filter — the call site holds the same `Class` object the SDK | ||
| * compares against. | ||
| * | ||
| * The primary use case is React Native apps that already capture | ||
| * fatal JS errors via `@posthog/react-native-plugin`'s JS-side | ||
| * autocapture: React Native rethrows the same fatal JS error on the | ||
| * native side as `com.facebook.react.common.JavascriptException`, | ||
| * and posthog-android would otherwise emit a second `$exception` | ||
| * event for the same logical error. | ||
| * | ||
| * Setting | ||
| * `ignoredExceptionTypes = mutableListOf(JavascriptException::class.java)` | ||
| * suppresses the native duplicate so the JS-captured event is the | ||
| * single source of truth. Mirrors `sentry-android`'s |
There was a problem hiding this comment.
Let's simplify this a little but, too chatty
…s by class Adds a `MutableList<Class<out Throwable>>` on `PostHogErrorTrackingConfig` that `PostHog.captureException` consults before building the `$exception` event. The throwable and every entry in its cause chain are tested with `Class.isInstance` (so R8 / ProGuard renames don't break the filter); on a match the SDK skips the event and the downstream uncaught-exception handler still runs. The cause-chain walk is bounded by `MAX_CAUSE_DEPTH = 32` to guard against cyclic chains. The single-source-of-truth check lives inside `captureException`'s existing try-catch, so manual `PostHog.captureException(...)` callers and the autocapture path both go through it. Three new tests on `PostHogTest` cover the direct-match drop, the cause-chain drop, and the keep-when-not-matching path. Snapshot under `posthog/api/posthog.api` regenerated to exercise the new public getter; `apiDump` + `spotlessCheck` are green. Closes PostHog#567.
6370460 to
4abc99f
Compare
|
Thanks @ioannisj. Squashed the branch onto current
706 tests pass under |
| sut.close() | ||
| } | ||
|
|
||
| private fun exceptionStepMessages(event: com.posthog.PostHogEvent): List<Any?> { |
There was a problem hiding this comment.
These tests should not be removed here
| exceptionStepsEnabled: Boolean = true, | ||
| exceptionStepsMaxBytes: Int = 32768, |
There was a problem hiding this comment.
Same, probably stale commits from main
| this.errorTrackingConfig.exceptionSteps.enabled = exceptionStepsEnabled | ||
| this.errorTrackingConfig.exceptionSteps.maxBytes = exceptionStepsMaxBytes |
There was a problem hiding this comment.
ioannisj
left a comment
There was a problem hiding this comment.
Need to rebase on main to remove some unrelated changes (e.g exceptionSteps + tests). Left a comment, not a blocker since this does not sit directly on RN path/case but the config option would be more complete if we gated generic capture() method as well
| } | ||
| } | ||
|
|
||
| override fun captureException( |
There was a problem hiding this comment.
Now that we have the concrete types, I think it should be possible to also gate the generic capture("$exception") path as well
Summary
Closes #567.
In React Native apps that enable native crash autocapture, a fatal JS error is captured twice:
uncaughtExceptionsautocapture (with the JS stack trace), andcom.facebook.react.common.JavascriptException. posthog-android's uncaught-exception handler then captures it as a second$exceptionevent (with a native stack trace).The result is two
$exceptionevents for one logical error.API
New optional field on
PostHogErrorTrackingConfig:Setup in an RN app:
Whenever the uncaught-exception handler fires, the throwable and every cause in its chain are checked against the list by class name:
\$exception, but still chains to the next handler (so the process termination / RN red-box / etc. behave the same way as before).Mirrors
sentry-android'saddIgnoredExceptionForType(...).Implementation notes
Class<*>::isInstance. This is deliberate: matching by name means apps that don't have React Native (or any other optional dependency) on their classpath are not affected at startup or call time. The SDK never has to load or even know about RN's types — the consumer just passes the FQCN string.causeuntil either a match is found or a seen-set short-circuits a self-referential chain.defaultExceptionHandleris always invoked, ignored or not. The contract ofThread.UncaughtExceptionHandler(process termination, RN's red-box, etc.) shouldn't change based on whether we emit a\$exceptionevent.ignoredExceptionTypessee zero behavior difference.Tests
Three new tests in
PostHogErrorTrackingAutoCaptureIntegrationTest:uncaughtException skips capture when throwable class is in ignoredExceptionTypes— direct match.uncaughtException skips capture when ignored class is anywhere in the cause chain—RuntimeException("outer", cause = JsException). The outermost type is not in the list, but the chain match still suppresses.uncaughtException still captures when throwable class is not in ignoredExceptionTypes— guards against the strip widening to all throwables when the list is non-empty.The RN exception type is stubbed locally (
ReactNativeJavascriptExceptionStub) so the test doesn't drag React Native into the SDK's test classpath — the name-based check exercises the same code path either way.All 17 tests pass (14 existing + 3 new):
Test plan
./gradlew :posthog:test --tests "com.posthog.errortracking.*"— 17/17 pass./gradlew :posthog:compileDebugKotlin(via the test task) — cleanignoredExceptionTypes = [], all existing tests behave identically (no behavior change to the autocapture path).Sibling issue
A matching fix is needed on the iOS side per #653 (RN rethrows fatal JS as
RCTFatalException). Happy to ship that next as a separate PR if this shape is what you want — the iOS surface would bePostHogErrorTrackingConfig.ignoredExceptionNames: [String]with the same cause-chain walk.Notes for review
@marandaneto — you suggested
beforeSendas a "for now" workaround on the issue thread; this PR proposes the structural fix. Naming and scope are intentionally narrow: just the FQCN list, no Class-object overload, no auto-detection of RN. Happy to refactor if you'd rather keep the surface even smaller (e.g. hardcode the JavascriptException class internally and gate on an RN-detection bool) — let me know which shape you'd prefer.