Skip to content

Commit c2ba7be

Browse files
committed
feat(replay): Use new prepareEvent util & ensure dropping replays works
1 parent 3b1bcaf commit c2ba7be

File tree

7 files changed

+101
-87
lines changed

7 files changed

+101
-87
lines changed

packages/replay/jest.setup.ts

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,6 @@ jest.mock('./src/util/isBrowser', () => {
1515
};
1616
});
1717

18-
afterEach(() => {
19-
const hub = getCurrentHub();
20-
if (typeof hub?.getClient !== 'function') {
21-
// Potentially not a function due to partial mocks
22-
return;
23-
}
24-
25-
const client = hub?.getClient();
26-
// This can be weirded up by mocks/tests
27-
if (
28-
!client ||
29-
!client.getTransport ||
30-
typeof client.getTransport !== 'function' ||
31-
typeof client.getTransport()?.send !== 'function'
32-
) {
33-
return;
34-
}
35-
36-
(client.getTransport()?.send as MockTransport).mockClear();
37-
});
38-
3918
type EnvelopeHeader = {
4019
event_id: string;
4120
sent_at: string;

packages/replay/src/replay.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -942,7 +942,9 @@ export class ReplayContainer implements ReplayContainerInterface {
942942
const replayEvent = await getReplayEvent({ scope, client, replayId, event: baseEvent });
943943

944944
if (!replayEvent) {
945-
__DEBUG_BUILD__ && logger.error('[Replay] An event processor returned null, will not send replay.');
945+
// Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
946+
client.recordDroppedEvent('event_processor', 'replay_event', baseEvent);
947+
__DEBUG_BUILD__ && logger.log('An event processor returned `null`, will not send event.');
946948
return;
947949
}
948950

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Scope } from '@sentry/core';
1+
import { prepareEvent, Scope } from '@sentry/core';
22
import { Client, ReplayEvent } from '@sentry/types';
33

44
export async function getReplayEvent({
@@ -12,21 +12,27 @@ export async function getReplayEvent({
1212
replayId: string;
1313
event: ReplayEvent;
1414
}): Promise<ReplayEvent | null> {
15-
// XXX: This event does not trigger `beforeSend` in SDK
16-
// @ts-ignore private api
17-
const preparedEvent: ReplayEvent | null = await client._prepareEvent(event, { event_id }, scope);
15+
const preparedEvent = (await prepareEvent(client.getOptions(), event, { event_id }, scope)) as ReplayEvent | null;
1816

19-
if (preparedEvent) {
20-
// extract the SDK name because `client._prepareEvent` doesn't add it to the event
21-
const metadata = client.getOptions() && client.getOptions()._metadata;
22-
const { name } = (metadata && metadata.sdk) || {};
23-
24-
preparedEvent.sdk = {
25-
...preparedEvent.sdk,
26-
version: __SENTRY_REPLAY_VERSION__,
27-
name,
28-
};
17+
// If e.g. a global event processor returned null
18+
if (!preparedEvent) {
19+
return null;
2920
}
3021

22+
// This normally happens in browser client "_prepareEvent"
23+
// but since we do not use this private method from the client, but rather the plain import
24+
// we need to do this manually.
25+
preparedEvent.platform = preparedEvent.platform || 'javascript';
26+
27+
// extract the SDK name because `client._prepareEvent` doesn't add it to the event
28+
const metadata = client.getOptions() && client.getOptions()._metadata;
29+
const { name } = (metadata && metadata.sdk) || {};
30+
31+
preparedEvent.sdk = {
32+
...preparedEvent.sdk,
33+
version: __SENTRY_REPLAY_VERSION__,
34+
name,
35+
};
36+
3137
return preparedEvent;
3238
}

packages/replay/src/util/monkeyPatchRecordDroppedEvent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function overwriteRecordDroppedEvent(errorIds: Set<string>): void {
1717
category: DataCategory,
1818
event?: Event,
1919
): void => {
20-
if (event && event.event_id) {
20+
if (event && !event.type && event.event_id) {
2121
errorIds.delete(event.event_id);
2222
}
2323

packages/replay/test/unit/index.test.ts

Lines changed: 76 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { getCurrentHub } from '@sentry/core';
1+
import { getCurrentHub, Hub } from '@sentry/core';
2+
import { Event, Scope } from '@sentry/types';
23
import { EventType } from 'rrweb';
34

45
import {
@@ -747,54 +748,6 @@ describe('Replay', () => {
747748
});
748749
});
749750

750-
// TODO: ... this doesn't really test anything anymore since replay event and recording are sent in the same envelope
751-
it('does not create replay event if recording upload completely fails', async () => {
752-
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
753-
// Suppress console.errors
754-
const mockConsole = jest.spyOn(console, 'error').mockImplementation(jest.fn());
755-
// fail the first and second requests and pass the third one
756-
mockSendReplayRequest.mockImplementationOnce(() => {
757-
throw new Error('Something bad happened');
758-
});
759-
mockRecord._emitter(TEST_EVENT);
760-
761-
await advanceTimers(5000);
762-
763-
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
764-
765-
// Reset console.error mock to minimize the amount of time we are hiding
766-
// console messages in case an error happens after
767-
mockConsole.mockClear();
768-
expect(mockRecord.takeFullSnapshot).not.toHaveBeenCalled();
769-
770-
mockSendReplayRequest.mockImplementationOnce(() => {
771-
throw new Error('Something bad happened');
772-
});
773-
await advanceTimers(5000);
774-
expect(replay.sendReplayRequest).toHaveBeenCalledTimes(2);
775-
776-
// next tick should retry and fail
777-
mockConsole.mockClear();
778-
779-
mockSendReplayRequest.mockImplementationOnce(() => {
780-
throw new Error('Something bad happened');
781-
});
782-
await advanceTimers(10000);
783-
expect(replay.sendReplayRequest).toHaveBeenCalledTimes(3);
784-
785-
mockSendReplayRequest.mockImplementationOnce(() => {
786-
throw new Error('Something bad happened');
787-
});
788-
await advanceTimers(30000);
789-
expect(replay.sendReplayRequest).toHaveBeenCalledTimes(4);
790-
791-
// No activity has occurred, session's last activity should remain the same
792-
expect(replay.session?.lastActivity).toBeGreaterThanOrEqual(BASE_TIMESTAMP);
793-
expect(replay.session?.segmentId).toBe(1);
794-
795-
// TODO: Recording should stop and next event should do nothing
796-
});
797-
798751
it('has correct timestamps when there events earlier than initial timestamp', async function () {
799752
replay.clearSession();
800753
replay.loadSession({ expiry: 0 });
@@ -920,3 +873,77 @@ describe('Replay', () => {
920873
expect(replay.flush).toHaveBeenCalledTimes(1);
921874
});
922875
});
876+
877+
describe('eventProcessors', () => {
878+
let hub: Hub;
879+
let scope: Scope;
880+
881+
beforeEach(() => {
882+
hub = getCurrentHub();
883+
scope = hub.pushScope();
884+
});
885+
886+
afterEach(() => {
887+
hub.popScope();
888+
jest.resetAllMocks();
889+
});
890+
891+
it('handles event processors properly', async () => {
892+
const MUTATED_TIMESTAMP = BASE_TIMESTAMP + 3000;
893+
894+
const { mockRecord } = await resetSdkMock({
895+
replayOptions: {
896+
stickySession: false,
897+
},
898+
});
899+
900+
const client = hub.getClient()!;
901+
902+
jest.runAllTimers();
903+
const mockTransportSend = jest.spyOn(client.getTransport()!, 'send');
904+
mockTransportSend.mockReset();
905+
906+
const handler1 = jest.fn((event: Event): Event | null => {
907+
event.timestamp = MUTATED_TIMESTAMP;
908+
909+
return event;
910+
});
911+
912+
const handler2 = jest.fn((): Event | null => {
913+
return null;
914+
});
915+
916+
scope.addEventProcessor(handler1);
917+
918+
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
919+
920+
mockRecord._emitter(TEST_EVENT);
921+
jest.runAllTimers();
922+
jest.advanceTimersByTime(1);
923+
await new Promise(process.nextTick);
924+
925+
expect(mockTransportSend).toHaveBeenCalledTimes(1);
926+
927+
scope.addEventProcessor(handler2);
928+
929+
const TEST_EVENT2 = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
930+
931+
mockRecord._emitter(TEST_EVENT2);
932+
jest.runAllTimers();
933+
jest.advanceTimersByTime(1);
934+
await new Promise(process.nextTick);
935+
936+
expect(mockTransportSend).toHaveBeenCalledTimes(1);
937+
938+
expect(handler1).toHaveBeenCalledTimes(2);
939+
expect(handler2).toHaveBeenCalledTimes(1);
940+
941+
// This receives an envelope, which is a deeply nested array
942+
// We only care about the fact that the timestamp was mutated
943+
expect(mockTransportSend).toHaveBeenCalledWith(
944+
expect.arrayContaining([
945+
expect.arrayContaining([expect.arrayContaining([expect.objectContaining({ timestamp: MUTATED_TIMESTAMP })])]),
946+
]),
947+
);
948+
});
949+
});

packages/replay/test/utils/getDefaultBrowserClientOptions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { resolvedSyncPromise } from '@sentry/utils';
55
export function getDefaultBrowserClientOptions(options: Partial<ClientOptions> = {}): ClientOptions {
66
return {
77
integrations: [],
8+
dsn: 'https://username@domain/123',
89
transport: () => createTransport({ recordDroppedEvent: () => undefined }, _ => resolvedSyncPromise({})),
910
stackParser: () => [],
1011
...options,

rollup/plugins/bundlePlugins.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ export function makeTerserPlugin() {
108108
'_initStorage',
109109
'_support',
110110
// TODO: Get rid of these once we use the SDK to send replay events
111-
'_prepareEvent', // replay uses client._prepareEvent
112111
'_metadata', // replay uses client.getOptions()._metadata
113112
],
114113
},

0 commit comments

Comments
 (0)