Skip to content

Commit 29a7213

Browse files
committed
feat(replay): Use new prepareEvent util & ensure dropping replays works
1 parent e4ef56e commit 29a7213

File tree

8 files changed

+101
-79
lines changed

8 files changed

+101
-79
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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,13 @@ export class ReplayContainer implements ReplayContainerInterface {
940940

941941
const replayEvent = await getReplayEvent({ scope, client, replayId, event: baseEvent });
942942

943+
if (!replayEvent) {
944+
// Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
945+
client.recordDroppedEvent('event_processor', 'replay_event', baseEvent);
946+
__DEBUG_BUILD__ && logger.log('An event processor returned `null`, will not send event.');
947+
return;
948+
}
949+
943950
replayEvent.tags = {
944951
...replayEvent.tags,
945952
sessionSampleRate: this._options.sessionSampleRate,

packages/replay/src/util/getReplayEvent.ts

Lines changed: 13 additions & 5 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, Event } from '@sentry/types';
33

44
import { REPLAY_SDK_INFO } from '../constants';
@@ -13,10 +13,18 @@ export async function getReplayEvent({
1313
scope: Scope;
1414
replayId: string;
1515
event: Event;
16-
}): Promise<Event> {
17-
// XXX: This event does not trigger `beforeSend` in SDK
18-
// @ts-ignore private api
19-
const preparedEvent: Event = await client._prepareEvent(event, { event_id }, scope);
16+
}): Promise<Event | null> {
17+
const preparedEvent = await prepareEvent(client.getOptions(), event, { event_id }, scope);
18+
19+
// If e.g. a global event processor returned null
20+
if (!preparedEvent) {
21+
return null;
22+
}
23+
24+
// This normally happens in browser client "_prepareEvent"
25+
// but since we do not use this private method from the client, but rather the plain import
26+
// we need to do this manually.
27+
preparedEvent.platform = preparedEvent.platform || 'javascript';
2028

2129
preparedEvent.sdk = {
2230
...preparedEvent.sdk,

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 { MAX_SESSION_LIFE, REPLAY_SESSION_KEY, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from '../../src/constants';
@@ -741,54 +742,6 @@ describe('Replay', () => {
741742
});
742743
});
743744

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

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,

packages/types/src/datacategory.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,6 @@ export type DataCategory =
1919
// SDK internal event, like client_reports
2020
| 'internal'
2121
// Profile event type
22-
| 'profile';
22+
| 'profile'
23+
// Replay event type
24+
| 'replay_event';

rollup/plugins/bundlePlugins.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,6 @@ export function makeTerserPlugin() {
104104
'_driver',
105105
'_initStorage',
106106
'_support',
107-
// TODO: Get rid of these once we use the SDK to send replay events
108-
'_prepareEvent', // replay uses client._prepareEvent
109107
],
110108
},
111109
},

0 commit comments

Comments
 (0)