diff --git a/packages/replay/src/index.ts b/packages/replay/src/index.ts index 6cc151f9dd81..1e852ea9e2de 100644 --- a/packages/replay/src/index.ts +++ b/packages/replay/src/index.ts @@ -1,7 +1,7 @@ /* eslint-disable max-lines */ // TODO: We might want to split this file up import type { BrowserClient, BrowserOptions } from '@sentry/browser'; import { addGlobalEventProcessor, getCurrentHub, Scope, setContext } from '@sentry/core'; -import { Breadcrumb, Client, Event, Integration } from '@sentry/types'; +import { Breadcrumb, Client, DataCategory, Event, EventDropReason, Integration } from '@sentry/types'; import { addInstrumentationHandler, createEnvelope, logger } from '@sentry/utils'; import debounce from 'lodash.debounce'; import { PerformanceObserverEntryList } from 'perf_hooks'; @@ -126,6 +126,11 @@ export class Replay implements Integration { */ private stopRecording: ReturnType | null = null; + /** + * We overwrite `client.recordDroppedEvent`, but store the original method here so we can restore it. + */ + private _originalRecordDroppedEvent?: Client['recordDroppedEvent']; + private context: InternalEventContext = { errorIds: new Set(), traceIds: new Set(), @@ -405,6 +410,9 @@ export class Replay implements Integration { WINDOW.addEventListener('blur', this.handleWindowBlur); WINDOW.addEventListener('focus', this.handleWindowFocus); + // We need to filter out dropped events captured by `addGlobalEventProcessor(this.handleGlobalEvent)` below + this._overwriteRecordDroppedEvent(); + // There is no way to remove these listeners, so ensure they are only added once if (!this.hasInitializedCoreListeners) { // Listeners from core SDK // @@ -467,6 +475,8 @@ export class Replay implements Integration { WINDOW.removeEventListener('blur', this.handleWindowBlur); WINDOW.removeEventListener('focus', this.handleWindowFocus); + this._restoreRecordDroppedEvent(); + if (this.performanceObserver) { this.performanceObserver.disconnect(); this.performanceObserver = null; @@ -1352,4 +1362,39 @@ export class Replay implements Integration { this.options.errorSampleRate = opt.replaysOnErrorSampleRate; } } + + private _overwriteRecordDroppedEvent(): void { + const client = getCurrentHub().getClient(); + + if (!client) { + return; + } + + const _originalCallback = client.recordDroppedEvent.bind(client); + + const recordDroppedEvent: Client['recordDroppedEvent'] = ( + reason: EventDropReason, + category: DataCategory, + event?: Event, + ): void => { + if (event && event.event_id) { + this.context.errorIds.delete(event.event_id); + } + + return _originalCallback(reason, category, event); + }; + + client.recordDroppedEvent = recordDroppedEvent; + this._originalRecordDroppedEvent = _originalCallback; + } + + private _restoreRecordDroppedEvent(): void { + const client = getCurrentHub().getClient(); + + if (!client || !this._originalRecordDroppedEvent) { + return; + } + + client.recordDroppedEvent = this._originalRecordDroppedEvent; + } } diff --git a/packages/replay/test/unit/index-handleGlobalEvent.test.ts b/packages/replay/test/unit/index-handleGlobalEvent.test.ts index e45a582c5c30..41b2e1bf13cb 100644 --- a/packages/replay/test/unit/index-handleGlobalEvent.test.ts +++ b/packages/replay/test/unit/index-handleGlobalEvent.test.ts @@ -1,3 +1,4 @@ +import { getCurrentHub } from '@sentry/core'; import { Error } from '@test/fixtures/error'; import { Transaction } from '@test/fixtures/transaction'; import { resetSdkMock } from '@test/mocks'; @@ -83,6 +84,27 @@ it('only tags errors with replay id, adds trace and error id to context for erro expect(replay.waitForError).toBe(false); }); +it('strips out dropped events from errorIds', async () => { + const error1 = Error({ event_id: 'err1' }); + const error2 = Error({ event_id: 'err2' }); + const error3 = Error({ event_id: 'err3' }); + + replay['_overwriteRecordDroppedEvent'](); + + const client = getCurrentHub().getClient()!; + + replay.handleGlobalEvent(error1); + replay.handleGlobalEvent(error2); + replay.handleGlobalEvent(error3); + + client.recordDroppedEvent('before_send', 'error', { event_id: 'err2' }); + + // @ts-ignore private + expect(Array.from(replay.context.errorIds)).toEqual(['err1', 'err3']); + + replay['_restoreRecordDroppedEvent'](); +}); + it('tags errors and transactions with replay id for session samples', async () => { ({ replay } = await resetSdkMock({ sessionSampleRate: 1.0,