Skip to content

Commit 0a19df2

Browse files
authored
feat: ignore extension exceptions (#2089)
1 parent 087c116 commit 0a19df2

File tree

7 files changed

+70
-10
lines changed

7 files changed

+70
-10
lines changed

.changeset/tame-doors-design.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'posthog-js': minor
3+
---
4+
5+
feat: exclude exceptions autocaptured by extensions

packages/browser/src/__tests__/__snapshots__/config-snapshot.test.ts.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,11 @@ exports[`config snapshot for PostHogConfig 1`] = `
324324
]
325325
},
326326
\\"error_tracking\\": {
327+
\\"captureExtensionExceptions\\": [
328+
\\"undefined\\",
329+
\\"false\\",
330+
\\"true\\"
331+
],
327332
\\"__exceptionRateLimiterRefillRate\\": [
328333
\\"undefined\\",
329334
\\"number\\"

packages/browser/src/__tests__/posthog-exceptions.test.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { ERROR_TRACKING_SUPPRESSION_RULES } from '../constants'
2-
import { PostHog } from '../posthog-core'
2+
import { defaultConfig, PostHog } from '../posthog-core'
33
import { PostHogExceptions } from '../posthog-exceptions'
44
import { PostHogPersistence } from '../posthog-persistence'
55
import {
6-
FlagsResponse,
76
ErrorTrackingSuppressionRule,
87
ErrorTrackingSuppressionRuleValue,
98
PostHogConfig,
109
Property,
10+
RemoteConfig,
1111
} from '../types'
1212

1313
function createSuppressionRule(
@@ -37,7 +37,7 @@ describe('PostHogExceptions', () => {
3737
let config: PostHogConfig
3838

3939
beforeEach(() => {
40-
config = { persistence: 'memory' } as unknown as PostHogConfig
40+
config = { ...defaultConfig(), persistence: 'memory' }
4141

4242
const postHogPersistence = new PostHogPersistence(config)
4343
postHogPersistence.clear()
@@ -67,8 +67,8 @@ describe('PostHogExceptions', () => {
6767
describe('onRemoteConfig', () => {
6868
it('persists the suppression rules', () => {
6969
const suppressionRule = createSuppressionRule()
70-
const flagsResponse: Partial<FlagsResponse> = { errorTracking: { suppressionRules: [suppressionRule] } }
71-
exceptions.onRemoteConfig(flagsResponse as FlagsResponse)
70+
const remoteResponse: Partial<RemoteConfig> = { errorTracking: { suppressionRules: [suppressionRule] } }
71+
exceptions.onRemoteConfig(remoteResponse as RemoteConfig)
7272
expect(exceptions['_suppressionRules']).toEqual([suppressionRule])
7373
})
7474
})
@@ -84,21 +84,21 @@ describe('PostHogExceptions', () => {
8484
['GenericError', 'This is a message that contains a ReactMinified error'],
8585
])('drops the event if a suppression rule matches', (type, value) => {
8686
const suppressionRule = createSuppressionRule('OR')
87-
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as FlagsResponse)
87+
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as RemoteConfig)
8888
exceptions.sendExceptionEvent({ $exception_list: [{ type, value }] })
8989
expect(captureMock).not.toBeCalled()
9090
})
9191

9292
it('captures an exception if no $exception_list property exists', () => {
9393
const suppressionRule = createSuppressionRule('AND')
94-
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as FlagsResponse)
94+
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as RemoteConfig)
9595
exceptions.sendExceptionEvent({ custom_property: true })
9696
expect(captureMock).toBeCalled()
9797
})
9898

9999
it('captures an exception if all rule conditions do not match', () => {
100100
const suppressionRule = createSuppressionRule('AND')
101-
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as FlagsResponse)
101+
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as RemoteConfig)
102102
exceptions.sendExceptionEvent({ $exception_list: [{ type: 'TypeError', value: 'This is a type error' }] })
103103
expect(captureMock).toBeCalled()
104104
})
@@ -112,9 +112,24 @@ describe('PostHogExceptions', () => {
112112
type: 'error_tracking_issue_property',
113113
},
114114
])
115-
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as FlagsResponse)
115+
exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as RemoteConfig)
116116
exceptions.sendExceptionEvent({ $exception_list: [{ type: 'TypeError', value: 'This is a type error' }] })
117117
expect(captureMock).toBeCalled()
118118
})
119+
120+
it('does not capture exceptions with frames from extensions by default', () => {
121+
const frame = { filename: 'chrome-extension://', platform: 'javascript:web' }
122+
const exception = { stacktrace: { frames: [frame], type: 'raw' } }
123+
exceptions.sendExceptionEvent({ $exception_list: [exception] })
124+
expect(captureMock).not.toBeCalledWith('$exception', { $exception_list: [exception] }, expect.anything())
125+
})
126+
127+
it('captures extension exceptions when enabled', () => {
128+
exceptions.onRemoteConfig({ errorTracking: { captureExtensionExceptions: true } } as RemoteConfig)
129+
const frame = { filename: 'chrome-extension://', platform: 'javascript:web' }
130+
const exception = { stacktrace: { frames: [frame], type: 'raw' } }
131+
exceptions.sendExceptionEvent({ $exception_list: [exception] })
132+
expect(captureMock).toBeCalledWith('$exception', { $exception_list: [exception] }, expect.anything())
133+
})
119134
})
120135
})

packages/browser/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const AUTOCAPTURE_DISABLED_SERVER_SIDE = '$autocapture_disabled_server_si
1414
export const HEATMAPS_ENABLED_SERVER_SIDE = '$heatmaps_enabled_server_side'
1515
export const EXCEPTION_CAPTURE_ENABLED_SERVER_SIDE = '$exception_capture_enabled_server_side'
1616
export const ERROR_TRACKING_SUPPRESSION_RULES = '$error_tracking_suppression_rules'
17+
export const ERROR_TRACKING_CAPTURE_EXTENSION_EXCEPTIONS = '$error_tracking_capture_extension_exceptions'
1718
export const WEB_VITALS_ENABLED_SERVER_SIDE = '$web_vitals_enabled_server_side'
1819
export const DEAD_CLICKS_ENABLED_SERVER_SIDE = '$dead_clicks_enabled_server_side'
1920
export const WEB_VITALS_ALLOWED_METRICS = '$web_vitals_allowed_metrics'

packages/browser/src/posthog-exceptions.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ERROR_TRACKING_SUPPRESSION_RULES } from './constants'
1+
import { ERROR_TRACKING_CAPTURE_EXTENSION_EXCEPTIONS, ERROR_TRACKING_SUPPRESSION_RULES } from './constants'
2+
import { Exception } from './extensions/exception-autocapture/error-conversion'
23
import { PostHog } from './posthog-core'
34
import { CaptureResult, ErrorTrackingSuppressionRule, Properties, RemoteConfig } from './types'
45
import { createLogger } from './utils/logger'
@@ -18,23 +19,36 @@ export class PostHogExceptions {
1819

1920
onRemoteConfig(response: RemoteConfig) {
2021
const suppressionRules = response.errorTracking?.suppressionRules ?? []
22+
const captureExtensionExceptions = response.errorTracking?.captureExtensionExceptions
2123

2224
// store this in-memory in case persistence is disabled
2325
this._suppressionRules = suppressionRules
2426

2527
if (this._instance.persistence) {
2628
this._instance.persistence.register({
2729
[ERROR_TRACKING_SUPPRESSION_RULES]: this._suppressionRules,
30+
[ERROR_TRACKING_CAPTURE_EXTENSION_EXCEPTIONS]: captureExtensionExceptions,
2831
})
2932
}
3033
}
3134

35+
private get _captureExtensionExceptions() {
36+
const enabled_server_side = !!this._instance.get_property(ERROR_TRACKING_CAPTURE_EXTENSION_EXCEPTIONS)
37+
const enabled_client_side = this._instance.config.error_tracking.captureExtensionExceptions
38+
return enabled_client_side ?? enabled_server_side ?? false
39+
}
40+
3241
sendExceptionEvent(properties: Properties): CaptureResult | undefined {
3342
if (this._matchesSuppressionRule(properties)) {
3443
logger.info('Skipping exception capture because a suppression rule matched')
3544
return
3645
}
3746

47+
if (!this._captureExtensionExceptions && this._isExtensionException(properties)) {
48+
logger.info('Skipping exception capture because it was thrown by an extension')
49+
return
50+
}
51+
3852
return this._instance.capture('$exception', properties, {
3953
_noTruncate: true,
4054
_batchKey: 'exceptionEvent',
@@ -74,4 +88,15 @@ export class PostHogExceptions {
7488
return rule.type === 'OR' ? results.some(Boolean) : results.every(Boolean)
7589
})
7690
}
91+
92+
private _isExtensionException(properties: Properties): boolean {
93+
const exceptionList = properties.$exception_list
94+
95+
if (!exceptionList || !isArray(exceptionList)) {
96+
return false
97+
}
98+
99+
const frames = (exceptionList as Exception[]).flatMap((e) => e.stacktrace?.frames ?? [])
100+
return frames.some((f) => f.filename && f.filename.startsWith('chrome-extension://'))
101+
}
77102
}

packages/browser/src/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1010,6 +1010,12 @@ export interface PostHogConfig {
10101010
}
10111011

10121012
export interface ErrorTrackingOptions {
1013+
/**
1014+
* Decide whether exceptions thrown by browser extensions should be captured
1015+
*
1016+
* @default false
1017+
*/
1018+
captureExtensionExceptions?: boolean
10131019
/**
10141020
* ADVANCED: alters the refill rate for the token bucket mutation throttling
10151021
* Normally only altered alongside posthog support guidance.
@@ -1385,6 +1391,7 @@ export interface RemoteConfig {
13851391
*/
13861392
errorTracking?: {
13871393
autocaptureExceptions?: boolean
1394+
captureExtensionExceptions?: boolean
13881395
suppressionRules?: ErrorTrackingSuppressionRule[]
13891396
}
13901397

packages/browser/terser-mangled-names.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"_capture",
3232
"_captureDeadClick",
3333
"_captureEvent",
34+
"_captureExtensionExceptions",
3435
"_captureInitialPageview",
3536
"_capturePageview",
3637
"_captureSnapshot",
@@ -124,6 +125,7 @@
124125
"_internalFlagCheckSatisfied",
125126
"_isConsoleLogCaptureEnabled",
126127
"_isDisabledServerSide",
128+
"_isExtensionException",
127129
"_isFetchingSurveys",
128130
"_isIdle",
129131
"_isInitializingSurveys",

0 commit comments

Comments
 (0)