-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
Copy pathhandleRecordingEmit.ts
155 lines (133 loc) · 6.26 KB
/
handleRecordingEmit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
import { EventType } from '@sentry-internal/rrweb';
import { updateClickDetectorForRecordingEvent } from '../coreHandlers/handleClick';
import { DEBUG_BUILD } from '../debug-build';
import { saveSession } from '../session/saveSession';
import type { RecordingEvent, ReplayContainer, ReplayOptionFrameEvent } from '../types';
import { addEventSync } from './addEvent';
import { logger } from './logger';
type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void;
/**
* Handler for recording events.
*
* Adds to event buffer, and has varying flushing behaviors if the event was a checkout.
*/
export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCallback {
let hadFirstEvent = false;
return (event: RecordingEvent, _isCheckout?: boolean) => {
// If this is false, it means session is expired, create and a new session and wait for checkout
if (!replay.checkAndHandleExpiredSession()) {
DEBUG_BUILD && logger.warn('Received replay event after session expired.');
return;
}
// `_isCheckout` is only set when the checkout is due to `checkoutEveryNms`
// We also want to treat the first event as a checkout, so we handle this specifically here
const isCheckout = _isCheckout || !hadFirstEvent;
hadFirstEvent = true;
if (replay.clickDetector) {
updateClickDetectorForRecordingEvent(replay.clickDetector, event);
}
// The handler returns `true` if we do not want to trigger debounced flush, `false` if we want to debounce flush.
replay.addUpdate(() => {
// The session is always started immediately on pageload/init, but for
// error-only replays, it should reflect the most recent checkout
// when an error occurs. Clear any state that happens before this current
// checkout. This needs to happen before `addEvent()` which updates state
// dependent on this reset.
if (replay.recordingMode === 'buffer' && isCheckout) {
replay.setInitialState();
}
// If the event is not added (e.g. due to being paused, disabled, or out of the max replay duration),
// Skip all further steps
if (!addEventSync(replay, event, isCheckout)) {
// Return true to skip scheduling a debounced flush
return true;
}
// Different behavior for full snapshots (type=2), ignore other event types
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
if (!isCheckout) {
return false;
}
const session = replay.session;
// Additionally, create a meta event that will capture certain SDK settings.
// In order to handle buffer mode, this needs to either be done when we
// receive checkout events or at flush time. We have an experimental mode
// to perform multiple checkouts a session (the idea is to improve
// seeking during playback), so also only include if segmentId is 0
// (handled in `addSettingsEvent`).
//
// `isCheckout` is always true, but want to be explicit that it should
// only be added for checkouts
addSettingsEvent(replay, isCheckout);
// When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
// this should usually be the timestamp of the checkout event, but to be safe...
if (replay.recordingMode === 'buffer' && session && replay.eventBuffer) {
const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
if (earliestEvent) {
DEBUG_BUILD &&
logger.info(`Updating session start time to earliest event in buffer to ${new Date(earliestEvent)}`);
session.started = earliestEvent;
if (replay.getOptions().stickySession) {
saveSession(session);
}
}
}
// If there is a previousSessionId after a full snapshot occurs, then
// the replay session was started due to session expiration. The new session
// is started before triggering a new checkout and contains the id
// of the previous session. Do not immediately flush in this case
// to avoid capturing only the checkout and instead the replay will
// be captured if they perform any follow-up actions.
if (session?.previousSessionId) {
return true;
}
if (replay.recordingMode === 'session') {
// If the full snapshot is due to an initial load, we will not have
// a previous session ID. In this case, we want to buffer events
// for a set amount of time before flushing. This can help avoid
// capturing replays of users that immediately close the window.
// This should never reject
// eslint-disable-next-line @typescript-eslint/no-floating-promises
void replay.flush();
}
return true;
});
};
}
/**
* Exported for tests
*/
export function createOptionsEvent(replay: ReplayContainer): ReplayOptionFrameEvent {
const options = replay.getOptions();
return {
type: EventType.Custom,
timestamp: Date.now(),
data: {
tag: 'options',
payload: {
shouldRecordCanvas: replay.isRecordingCanvas(),
sessionSampleRate: options.sessionSampleRate,
errorSampleRate: options.errorSampleRate,
useCompressionOption: options.useCompression,
blockAllMedia: options.blockAllMedia,
maskAllText: options.maskAllText,
maskAllInputs: options.maskAllInputs,
useCompression: replay.eventBuffer ? replay.eventBuffer.type === 'worker' : false,
networkDetailHasUrls: options.networkDetailAllowUrls.length > 0,
networkCaptureBodies: options.networkCaptureBodies,
networkRequestHasHeaders: options.networkRequestHeaders.length > 0,
networkResponseHasHeaders: options.networkResponseHeaders.length > 0,
},
},
};
}
/**
* Add a "meta" event that contains a simplified view on current configuration
* options. This should only be included on the first segment of a recording.
*/
function addSettingsEvent(replay: ReplayContainer, isCheckout?: boolean): void {
// Only need to add this event when sending the first segment
if (!isCheckout || !replay.session || replay.session.segmentId !== 0) {
return;
}
addEventSync(replay, createOptionsEvent(replay), false);
}