Skip to content

Commit 65245f4

Browse files
authored
ref(replay): Extract some functions out from class (#6448)
1 parent 5de6e43 commit 65245f4

34 files changed

+520
-417
lines changed

packages/replay/jest.setup.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22
import { getCurrentHub } from '@sentry/core';
33
import { Transport } from '@sentry/types';
44

5-
import { ReplayContainer } from './src/replay';
6-
import { Session } from './src/session/Session';
5+
import type { ReplayContainer, Session } from './src/types';
76

87
// @ts-ignore TS error, this is replaced in prod builds bc of rollup
98
global.__SENTRY_REPLAY_VERSION__ = 'version:Test';

packages/replay/src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
99
export const REPLAY_SESSION_KEY = 'sentryReplaySession';
1010
export const REPLAY_EVENT_NAME = 'replay_event';
1111
export const RECORDING_EVENT_NAME = 'replay_recording';
12+
export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';
1213

1314
// The idle limit for a session
1415
export const SESSION_IDLE_DURATION = 300_000; // 5 minutes in ms

packages/replay/src/coreHandlers/breadcrumbHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Breadcrumb, Scope } from '@sentry/types';
22

3-
import { InstrumentationTypeBreadcrumb } from '../types';
3+
import type { InstrumentationTypeBreadcrumb } from '../types';
44
import { DomHandlerData, handleDom } from './handleDom';
55
import { handleScope } from './handleScope';
66

packages/replay/src/coreHandlers/handleFetch.ts

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { ReplayPerformanceEntry } from '../createPerformanceEntry';
1+
import type { ReplayPerformanceEntry } from '../createPerformanceEntry';
2+
import type { ReplayContainer } from '../types';
3+
import { createPerformanceSpans } from '../util/createPerformanceSpans';
24
import { isIngestHost } from '../util/isIngestHost';
35

4-
export interface FetchHandlerData {
6+
interface FetchHandlerData {
57
args: Parameters<typeof fetch>;
68
fetchData: {
79
method: string;
@@ -18,6 +20,7 @@ export interface FetchHandlerData {
1820
endTimestamp?: number;
1921
}
2022

23+
/** only exported for tests */
2124
export function handleFetch(handlerData: FetchHandlerData): null | ReplayPerformanceEntry {
2225
if (!handlerData.endTimestamp) {
2326
return null;
@@ -41,3 +44,28 @@ export function handleFetch(handlerData: FetchHandlerData): null | ReplayPerform
4144
},
4245
};
4346
}
47+
48+
/**
49+
* Returns a listener to be added to `addInstrumentationHandler('fetch', listener)`.
50+
*/
51+
export function handleFetchSpanListener(replay: ReplayContainer): (handlerData: FetchHandlerData) => void {
52+
return (handlerData: FetchHandlerData) => {
53+
if (!replay.isEnabled()) {
54+
return;
55+
}
56+
57+
const result = handleFetch(handlerData);
58+
59+
if (result === null) {
60+
return;
61+
}
62+
63+
replay.addUpdate(() => {
64+
createPerformanceSpans(replay, [result]);
65+
// Returning true will cause `addUpdate` to not flush
66+
// We do not want network requests to cause a flush. This will prevent
67+
// recurring/polling requests from keeping the replay session alive.
68+
return true;
69+
});
70+
};
71+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Event } from '@sentry/types';
2+
3+
import { REPLAY_EVENT_NAME, UNABLE_TO_SEND_REPLAY } from '../constants';
4+
import type { ReplayContainer } from '../types';
5+
import { addInternalBreadcrumb } from '../util/addInternalBreadcrumb';
6+
7+
/**
8+
* Returns a listener to be added to `addGlobalEventProcessor(listener)`.
9+
*/
10+
export function handleGlobalEventListener(replay: ReplayContainer): (event: Event) => Event {
11+
return (event: Event) => {
12+
// Do not apply replayId to the root event
13+
if (
14+
// @ts-ignore new event type
15+
event.type === REPLAY_EVENT_NAME
16+
) {
17+
// Replays have separate set of breadcrumbs, do not include breadcrumbs
18+
// from core SDK
19+
delete event.breadcrumbs;
20+
return event;
21+
}
22+
23+
// Only tag transactions with replayId if not waiting for an error
24+
// @ts-ignore private
25+
if (event.type !== 'transaction' || !replay._waitForError) {
26+
event.tags = { ...event.tags, replayId: replay.session?.id };
27+
}
28+
29+
// Collect traceIds in _context regardless of `_waitForError` - if it's true,
30+
// _context gets cleared on every checkout
31+
if (event.type === 'transaction' && event.contexts && event.contexts.trace && event.contexts.trace.trace_id) {
32+
replay.getContext().traceIds.add(event.contexts.trace.trace_id as string);
33+
return event;
34+
}
35+
36+
// no event type means error
37+
if (!event.type) {
38+
replay.getContext().errorIds.add(event.event_id as string);
39+
}
40+
41+
const exc = event.exception?.values?.[0];
42+
addInternalBreadcrumb({
43+
message: `Tagging event (${event.event_id}) - ${event.message} - ${exc?.type || 'Unknown'}: ${
44+
exc?.value || 'n/a'
45+
}`,
46+
});
47+
48+
// Need to be very careful that this does not cause an infinite loop
49+
if (
50+
// @ts-ignore private
51+
replay._waitForError &&
52+
event.exception &&
53+
event.message !== UNABLE_TO_SEND_REPLAY // ignore this error because otherwise we could loop indefinitely with trying to capture replay and failing
54+
) {
55+
setTimeout(async () => {
56+
// Allow flush to complete before resuming as a session recording, otherwise
57+
// the checkout from `startRecording` may be included in the payload.
58+
// Prefer to keep the error replay as a separate (and smaller) segment
59+
// than the session replay.
60+
await replay.flushImmediate();
61+
62+
if (replay.stopRecording()) {
63+
// Reset all "capture on error" configuration before
64+
// starting a new recording
65+
// @ts-ignore private
66+
replay._waitForError = false;
67+
replay.startRecording();
68+
}
69+
});
70+
}
71+
72+
return event;
73+
};
74+
}

packages/replay/src/coreHandlers/handleHistory.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { ReplayPerformanceEntry } from '../createPerformanceEntry';
2+
import type { ReplayContainer } from '../types';
3+
import { createPerformanceSpans } from '../util/createPerformanceSpans';
24

3-
export interface HistoryHandlerData {
5+
interface HistoryHandlerData {
46
from: string;
57
to: string;
68
}
79

8-
export function handleHistory(handlerData: HistoryHandlerData): ReplayPerformanceEntry {
10+
function handleHistory(handlerData: HistoryHandlerData): ReplayPerformanceEntry {
911
const { from, to } = handlerData;
1012

1113
const now = new Date().getTime() / 1000;
@@ -20,3 +22,30 @@ export function handleHistory(handlerData: HistoryHandlerData): ReplayPerformanc
2022
},
2123
};
2224
}
25+
26+
/**
27+
* Returns a listener to be added to `addInstrumentationHandler('history', listener)`.
28+
*/
29+
export function handleHistorySpanListener(replay: ReplayContainer): (handlerData: HistoryHandlerData) => void {
30+
return (handlerData: HistoryHandlerData) => {
31+
if (!replay.isEnabled()) {
32+
return;
33+
}
34+
35+
const result = handleHistory(handlerData);
36+
37+
if (result === null) {
38+
return;
39+
}
40+
41+
// Need to collect visited URLs
42+
replay.getContext().urls.push(result.name);
43+
replay.triggerUserActivity();
44+
45+
replay.addUpdate(() => {
46+
createPerformanceSpans(replay, [result]);
47+
// Returning false to flush
48+
return false;
49+
});
50+
};
51+
}

packages/replay/src/coreHandlers/handleXhr.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ReplayPerformanceEntry } from '../createPerformanceEntry';
2+
import type { ReplayContainer } from '../types';
3+
import { createPerformanceSpans } from '../util/createPerformanceSpans';
24
import { isIngestHost } from '../util/isIngestHost';
35

46
// From sentry-javascript
@@ -19,14 +21,14 @@ interface SentryWrappedXMLHttpRequest extends XMLHttpRequest {
1921
__sentry_own_request__?: boolean;
2022
}
2123

22-
export interface XhrHandlerData {
24+
interface XhrHandlerData {
2325
args: [string, string];
2426
xhr: SentryWrappedXMLHttpRequest;
2527
startTimestamp: number;
2628
endTimestamp?: number;
2729
}
2830

29-
export function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null {
31+
function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null {
3032
if (handlerData.xhr.__sentry_own_request__) {
3133
// Taken from sentry-javascript
3234
// Only capture non-sentry requests
@@ -61,3 +63,28 @@ export function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry |
6163
},
6264
};
6365
}
66+
67+
/**
68+
* Returns a listener to be added to `addInstrumentationHandler('xhr', listener)`.
69+
*/
70+
export function handleXhrSpanListener(replay: ReplayContainer): (handlerData: XhrHandlerData) => void {
71+
return (handlerData: XhrHandlerData) => {
72+
if (!replay.isEnabled()) {
73+
return;
74+
}
75+
76+
const result = handleXhr(handlerData);
77+
78+
if (result === null) {
79+
return;
80+
}
81+
82+
replay.addUpdate(() => {
83+
createPerformanceSpans(replay, [result]);
84+
// Returning true will cause `addUpdate` to not flush
85+
// We do not want network requests to cause a flush. This will prevent
86+
// recurring/polling requests from keeping the replay session alive.
87+
return true;
88+
});
89+
};
90+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import type { AllPerformanceEntry, ReplayContainer } from '../types';
2+
import { dedupePerformanceEntries } from '../util/dedupePerformanceEntries';
3+
4+
/**
5+
* Sets up a PerformanceObserver to listen to all performance entry types.
6+
*/
7+
export function setupPerformanceObserver(replay: ReplayContainer): PerformanceObserver {
8+
const performanceObserverHandler = (list: PerformanceObserverEntryList): void => {
9+
// For whatever reason the observer was returning duplicate navigation
10+
// entries (the other entry types were not duplicated).
11+
const newPerformanceEntries = dedupePerformanceEntries(
12+
replay.performanceEvents,
13+
list.getEntries() as AllPerformanceEntry[],
14+
);
15+
replay.performanceEvents = newPerformanceEntries;
16+
};
17+
18+
const performanceObserver = new PerformanceObserver(performanceObserverHandler);
19+
20+
[
21+
'element',
22+
'event',
23+
'first-input',
24+
'largest-contentful-paint',
25+
'layout-shift',
26+
'longtask',
27+
'navigation',
28+
'paint',
29+
'resource',
30+
].forEach(type => {
31+
try {
32+
performanceObserver.observe({
33+
type,
34+
buffered: true,
35+
});
36+
} catch {
37+
// This can throw if an entry type is not supported in the browser.
38+
// Ignore these errors.
39+
}
40+
});
41+
42+
return performanceObserver;
43+
}

packages/replay/src/coreHandlers/spanHandler.ts

Lines changed: 0 additions & 17 deletions
This file was deleted.

packages/replay/src/createPerformanceEntry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { browserPerformanceTimeOrigin } from '@sentry/utils';
22
import { record } from 'rrweb';
33

44
import { WINDOW } from './constants';
5-
import { AllPerformanceEntry, PerformanceNavigationTiming, PerformancePaintTiming } from './types';
5+
import type { AllPerformanceEntry, PerformanceNavigationTiming, PerformancePaintTiming } from './types';
66
import { isIngestHost } from './util/isIngestHost';
77

88
export interface ReplayPerformanceEntry {

packages/replay/src/eventBuffer.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { captureException } from '@sentry/core';
55
import { logger } from '@sentry/utils';
66

7-
import { RecordingEvent, WorkerRequest, WorkerResponse } from './types';
7+
import type { EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from './types';
88
import workerString from './worker/worker.js';
99

1010
interface CreateEventBufferParams {
@@ -35,13 +35,6 @@ export function createEventBuffer({ useCompression }: CreateEventBufferParams):
3535
return new EventBufferArray();
3636
}
3737

38-
export interface EventBuffer {
39-
readonly length: number;
40-
destroy(): void;
41-
addEvent(event: RecordingEvent, isCheckout?: boolean): void;
42-
finish(): Promise<string | Uint8Array>;
43-
}
44-
4538
class EventBufferArray implements EventBuffer {
4639
private _events: RecordingEvent[];
4740

0 commit comments

Comments
 (0)