@@ -13,7 +13,7 @@ import type { CaptureResult } from 'posthog-js'
1313 * Neither can be acted on: there is no defect to fix and no user impact, but
1414 * both fire often enough per session to bury real crashes in the issue list.
1515 */
16- const CANCELLATION_EXCEPTION_TYPES = new Set ( [ 'AbortError' , 'Canceled' ] )
16+ const CANCELLATION_ERROR_NAMES = new Set ( [ 'AbortError' , 'Canceled' ] )
1717
1818/**
1919 * Exception messages that carry no diagnosable content.
@@ -41,13 +41,50 @@ const UNDIAGNOSABLE_EXCEPTION_MESSAGES = [
4141interface CapturedException {
4242 type ?: unknown
4343 value ?: unknown
44+ mechanism ?: { handled ?: unknown }
4445}
4546
46- function isNoise ( exception : CapturedException ) : boolean {
47- if ( typeof exception . type === 'string' && CANCELLATION_EXCEPTION_TYPES . has ( exception . type ) ) {
47+ /**
48+ * Whether the browser raised this itself, rather than us reporting it on purpose.
49+ *
50+ * PostHog's `window.onerror` and `unhandledrejection` wrappers both build their
51+ * exception with `mechanism.handled: false`, while `captureException` — the call
52+ * our error boundaries make — builds with `true`. Only the browser's own reports
53+ * are eligible for filtering: a deliberate report means someone decided the
54+ * failure was worth knowing about, and dropping it would repeat the silent loss
55+ * this filter's sibling gate exists to prevent.
56+ *
57+ * Read from the first entry only. When an error carries a `cause`, PostHog
58+ * appends the chained links with `handled: true` regardless of how the original
59+ * was raised, so the head of the list is the one that reflects the source.
60+ */
61+ function isBrowserRaised ( exceptions : CapturedException [ ] ) : boolean {
62+ return exceptions [ 0 ] ?. mechanism ?. handled === false
63+ }
64+
65+ /**
66+ * Whether this is a cancellation, testing both shapes PostHog can produce.
67+ *
68+ * Which field carries the error's `name` depends on which coercer ran. A plain
69+ * `Error` keeps its `name` as `type`, so Monaco's `CancellationError` arrives as
70+ * type `Canceled`. A `DOMException` — what `fetch` rejects with when its signal
71+ * fires — always coerces to type `DOMException`, with the name folded into the
72+ * front of the value as `"AbortError: signal is aborted without reason"`.
73+ * Matching on `type` alone therefore misses every real aborted request.
74+ */
75+ function isCancellation ( exception : CapturedException ) : boolean {
76+ if ( typeof exception . type === 'string' && CANCELLATION_ERROR_NAMES . has ( exception . type ) ) {
4877 return true
4978 }
5079
80+ if ( typeof exception . value !== 'string' ) return false
81+
82+ return CANCELLATION_ERROR_NAMES . has ( exception . value . split ( ':' , 1 ) [ 0 ] )
83+ }
84+
85+ function isNoise ( exception : CapturedException ) : boolean {
86+ if ( isCancellation ( exception ) ) return true
87+
5188 if ( typeof exception . value !== 'string' ) return false
5289 const message = exception . value . trim ( )
5390
@@ -57,8 +94,9 @@ function isNoise(exception: CapturedException): boolean {
5794/**
5895 * `before_send` hook that drops browser noise from error tracking.
5996 *
60- * Fails open in every direction: anything that is not a `$exception`, and any
61- * `$exception` whose list is missing or unrecognizable, passes through
97+ * Fails open in every direction: anything that is not a `$exception`, any
98+ * `$exception` whose list is missing or unrecognizable, and anything we
99+ * reported deliberately rather than caught from the browser, all pass through
62100 * untouched. This runs on **every** captured event, so a filter that guessed
63101 * wrong would silently delete product analytics rather than merely over-report.
64102 *
@@ -75,10 +113,13 @@ export function dropUnactionableExceptions(event: CaptureResult | null): Capture
75113 const exceptions : unknown = event . properties ?. $exception_list
76114 if ( ! Array . isArray ( exceptions ) || exceptions . length === 0 ) return event
77115
78- const allNoise = exceptions . every (
79- ( exception ) =>
80- typeof exception === 'object' && exception !== null && isNoise ( exception as CapturedException )
116+ const entries : CapturedException [ ] = exceptions . filter (
117+ ( exception ) : exception is CapturedException =>
118+ typeof exception === 'object' && exception !== null
81119 )
120+ if ( entries . length !== exceptions . length ) return event
121+
122+ if ( ! isBrowserRaised ( entries ) ) return event
82123
83- return allNoise ? null : event
124+ return entries . every ( isNoise ) ? null : event
84125}
0 commit comments