Skip to content

Commit 3375302

Browse files
committed
ref: Extract handleRecordingEmit into util
1 parent f8b56b0 commit 3375302

File tree

5 files changed

+105
-93
lines changed

5 files changed

+105
-93
lines changed

packages/replay/src/replay.ts

Lines changed: 23 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import type {
1616
EventBuffer,
1717
InternalEventContext,
1818
PopEventContext,
19-
RecordingEvent,
2019
RecordingOptions,
2120
ReplayContainer as ReplayContainerInterface,
2221
ReplayPluginOptions,
@@ -30,6 +29,7 @@ import { createBreadcrumb } from './util/createBreadcrumb';
3029
import { createPerformanceEntries } from './util/createPerformanceEntries';
3130
import { createPerformanceSpans } from './util/createPerformanceSpans';
3231
import { debounce } from './util/debounce';
32+
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
3333
import { isExpired } from './util/isExpired';
3434
import { isSessionExpired } from './util/isSessionExpired';
3535
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent';
@@ -155,7 +155,7 @@ export class ReplayContainer implements ReplayContainerInterface {
155155
* _performanceObserver, Recording, Sentry SDK, etc)
156156
*/
157157
public start(): void {
158-
this._setInitialState();
158+
this.setInitialState();
159159

160160
if (!this._loadAndCheckSession()) {
161161
return;
@@ -207,7 +207,7 @@ export class ReplayContainer implements ReplayContainerInterface {
207207
// Without this, it would record forever, until an error happens, which we don't want
208208
// instead, we'll always keep the last 60 seconds of replay before an error happened
209209
...(this.recordingMode === 'error' && { checkoutEveryNms: ERROR_CHECKOUT_TIME }),
210-
emit: this._handleRecordingEmit,
210+
emit: getHandleRecordingEmit(this),
211211
onMutation: (mutations: unknown[]) => {
212212
if (this._options._experiments.captureMutationSize) {
213213
const count = mutations.length;
@@ -420,6 +420,25 @@ export class ReplayContainer implements ReplayContainerInterface {
420420
return false;
421421
}
422422

423+
/**
424+
* Capture some initial state that can change throughout the lifespan of the
425+
* replay. This is required because otherwise they would be captured at the
426+
* first flush.
427+
*/
428+
public setInitialState(): void {
429+
const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
430+
const url = `${WINDOW.location.origin}${urlPath}`;
431+
432+
this.performanceEvents = [];
433+
434+
// Reset _context as well
435+
this._clearContext();
436+
437+
this._context.initialUrl = url;
438+
this._context.initialTimestamp = new Date().getTime();
439+
this._context.urls.push(url);
440+
}
441+
423442
/** A wrapper to conditionally capture exceptions. */
424443
private _handleException(error: unknown): void {
425444
__DEBUG_BUILD__ && logger.error('[Replay]', error);
@@ -445,7 +464,7 @@ export class ReplayContainer implements ReplayContainerInterface {
445464
// If session was newly created (i.e. was not loaded from storage), then
446465
// enable flag to create the root replay
447466
if (type === 'new') {
448-
this._setInitialState();
467+
this.setInitialState();
449468
}
450469

451470
const currentSessionId = this.getSessionId();
@@ -463,25 +482,6 @@ export class ReplayContainer implements ReplayContainerInterface {
463482
return true;
464483
}
465484

466-
/**
467-
* Capture some initial state that can change throughout the lifespan of the
468-
* replay. This is required because otherwise they would be captured at the
469-
* first flush.
470-
*/
471-
private _setInitialState(): void {
472-
const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
473-
const url = `${WINDOW.location.origin}${urlPath}`;
474-
475-
this.performanceEvents = [];
476-
477-
// Reset _context as well
478-
this._clearContext();
479-
480-
this._context.initialUrl = url;
481-
this._context.initialTimestamp = new Date().getTime();
482-
this._context.urls.push(url);
483-
}
484-
485485
/**
486486
* Adds listeners to record events for the replay
487487
*/
@@ -533,72 +533,6 @@ export class ReplayContainer implements ReplayContainerInterface {
533533
}
534534
}
535535

536-
/**
537-
* Handler for recording events.
538-
*
539-
* Adds to event buffer, and has varying flushing behaviors if the event was a checkout.
540-
*/
541-
private _handleRecordingEmit: (event: RecordingEvent, isCheckout?: boolean) => void = (
542-
event: RecordingEvent,
543-
isCheckout?: boolean,
544-
) => {
545-
// If this is false, it means session is expired, create and a new session and wait for checkout
546-
if (!this.checkAndHandleExpiredSession()) {
547-
__DEBUG_BUILD__ && logger.warn('[Replay] Received replay event after session expired.');
548-
549-
return;
550-
}
551-
552-
this.addUpdate(() => {
553-
// The session is always started immediately on pageload/init, but for
554-
// error-only replays, it should reflect the most recent checkout
555-
// when an error occurs. Clear any state that happens before this current
556-
// checkout. This needs to happen before `addEvent()` which updates state
557-
// dependent on this reset.
558-
if (this.recordingMode === 'error' && event.type === 2) {
559-
this._setInitialState();
560-
}
561-
562-
// We need to clear existing events on a checkout, otherwise they are
563-
// incremental event updates and should be appended
564-
void addEvent(this, event, isCheckout);
565-
566-
// Different behavior for full snapshots (type=2), ignore other event types
567-
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
568-
if (event.type !== 2) {
569-
return false;
570-
}
571-
572-
// If there is a previousSessionId after a full snapshot occurs, then
573-
// the replay session was started due to session expiration. The new session
574-
// is started before triggering a new checkout and contains the id
575-
// of the previous session. Do not immediately flush in this case
576-
// to avoid capturing only the checkout and instead the replay will
577-
// be captured if they perform any follow-up actions.
578-
if (this.session && this.session.previousSessionId) {
579-
return true;
580-
}
581-
582-
// See note above re: session start needs to reflect the most recent
583-
// checkout.
584-
if (this.recordingMode === 'error' && this.session && this._context.earliestEvent) {
585-
this.session.started = this._context.earliestEvent;
586-
this._maybeSaveSession();
587-
}
588-
589-
// Flush immediately so that we do not miss the first segment, otherwise
590-
// it can prevent loading on the UI. This will cause an increase in short
591-
// replays (e.g. opening and closing a tab quickly), but these can be
592-
// filtered on the UI.
593-
if (this.recordingMode === 'session') {
594-
// We want to ensure the worker is ready, as otherwise we'd always send the first event uncompressed
595-
void this.flushImmediate();
596-
}
597-
598-
return true;
599-
});
600-
};
601-
602536
/**
603537
* Handle when visibility of the page content changes. Opening a new tab will
604538
* cause the state to change to hidden because of content of current page will

packages/replay/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ export interface ReplayContainer {
314314
getOptions(): ReplayPluginOptions;
315315
getSessionId(): string | undefined;
316316
checkAndHandleExpiredSession(): boolean | void;
317+
setInitialState(): void;
317318
}
318319

319320
export interface ReplayPerformanceEntry {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { logger } from '@sentry/utils';
2+
3+
import type { ReplayContainer } from '../replay';
4+
import { saveSession } from '../session/saveSession';
5+
import type { RecordingEvent } from '../types';
6+
import { addEvent } from './addEvent';
7+
8+
type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void;
9+
10+
/**
11+
* Handler for recording events.
12+
*
13+
* Adds to event buffer, and has varying flushing behaviors if the event was a checkout.
14+
*/
15+
export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCallback {
16+
return (event: RecordingEvent, isCheckout?: boolean) => {
17+
// If this is false, it means session is expired, create and a new session and wait for checkout
18+
if (!replay.checkAndHandleExpiredSession()) {
19+
__DEBUG_BUILD__ && logger.warn('[Replay] Received replay event after session expired.');
20+
21+
return;
22+
}
23+
24+
replay.addUpdate(() => {
25+
// The session is always started immediately on pageload/init, but for
26+
// error-only replays, it should reflect the most recent checkout
27+
// when an error occurs. Clear any state that happens before this current
28+
// checkout. This needs to happen before `addEvent()` which updates state
29+
// dependent on this reset.
30+
if (replay.recordingMode === 'error' && event.type === 2) {
31+
replay.setInitialState();
32+
}
33+
34+
// We need to clear existing events on a checkout, otherwise they are
35+
// incremental event updates and should be appended
36+
void addEvent(replay, event, isCheckout);
37+
38+
// Different behavior for full snapshots (type=2), ignore other event types
39+
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
40+
if (event.type !== 2) {
41+
return false;
42+
}
43+
44+
// If there is a previousSessionId after a full snapshot occurs, then
45+
// the replay session was started due to session expiration. The new session
46+
// is started before triggering a new checkout and contains the id
47+
// of the previous session. Do not immediately flush in this case
48+
// to avoid capturing only the checkout and instead the replay will
49+
// be captured if they perform any follow-up actions.
50+
if (replay.session && replay.session.previousSessionId) {
51+
return true;
52+
}
53+
54+
// See note above re: session start needs to reflect the most recent
55+
// checkout.
56+
if (replay.recordingMode === 'error' && replay.session) {
57+
const { earliestEvent } = replay.getContext();
58+
if (earliestEvent) {
59+
replay.session.started = earliestEvent;
60+
61+
if (replay.getOptions().stickySession) {
62+
saveSession(replay.session);
63+
}
64+
}
65+
}
66+
67+
// Flush immediately so that we do not miss the first segment, otherwise
68+
// it can prevent loading on the UI. This will cause an increase in short
69+
// replays (e.g. opening and closing a tab quickly), but these can be
70+
// filtered on the UI.
71+
if (replay.recordingMode === 'session') {
72+
// We want to ensure the worker is ready, as otherwise we'd always send the first event uncompressed
73+
void replay.flushImmediate();
74+
}
75+
76+
return true;
77+
});
78+
};
79+
}

packages/replay/test/integration/autoSaveSession.test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,9 @@ describe('Integration | autoSaveSession', () => {
1616
['with stickySession=true', true, 1],
1717
['with stickySession=false', false, 0],
1818
])('%s', async (_: string, stickySession: boolean, addSummand: number) => {
19-
let saveSessionSpy;
19+
const saveSessionSpy = jest.fn();
2020

2121
jest.mock('../../src/session/saveSession', () => {
22-
saveSessionSpy = jest.fn();
23-
2422
return {
2523
saveSession: saveSessionSpy,
2624
};

packages/replay/test/utils/setupReplayContainer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function setupReplayContainer({
2626
});
2727

2828
clearSession(replay);
29-
replay['_setInitialState']();
29+
replay.setInitialState();
3030
replay['_loadAndCheckSession']();
3131
replay['_isEnabled'] = true;
3232
replay.eventBuffer = createEventBuffer({

0 commit comments

Comments
 (0)