Skip to content

Commit ff2fcfd

Browse files
committed
ref: Extract handleRecordingEmit into util
1 parent af48921 commit ff2fcfd

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
});
212212
} catch (err) {
213213
this._handleException(err);
@@ -403,6 +403,25 @@ export class ReplayContainer implements ReplayContainerInterface {
403403
return false;
404404
}
405405

406+
/**
407+
* Capture some initial state that can change throughout the lifespan of the
408+
* replay. This is required because otherwise they would be captured at the
409+
* first flush.
410+
*/
411+
public setInitialState(): void {
412+
const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
413+
const url = `${WINDOW.location.origin}${urlPath}`;
414+
415+
this.performanceEvents = [];
416+
417+
// Reset _context as well
418+
this._clearContext();
419+
420+
this._context.initialUrl = url;
421+
this._context.initialTimestamp = new Date().getTime();
422+
this._context.urls.push(url);
423+
}
424+
406425
/** A wrapper to conditionally capture exceptions. */
407426
private _handleException(error: unknown): void {
408427
__DEBUG_BUILD__ && logger.error('[Replay]', error);
@@ -428,7 +447,7 @@ export class ReplayContainer implements ReplayContainerInterface {
428447
// If session was newly created (i.e. was not loaded from storage), then
429448
// enable flag to create the root replay
430449
if (type === 'new') {
431-
this._setInitialState();
450+
this.setInitialState();
432451
}
433452

434453
const currentSessionId = this.getSessionId();
@@ -446,25 +465,6 @@ export class ReplayContainer implements ReplayContainerInterface {
446465
return true;
447466
}
448467

449-
/**
450-
* Capture some initial state that can change throughout the lifespan of the
451-
* replay. This is required because otherwise they would be captured at the
452-
* first flush.
453-
*/
454-
private _setInitialState(): void {
455-
const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
456-
const url = `${WINDOW.location.origin}${urlPath}`;
457-
458-
this.performanceEvents = [];
459-
460-
// Reset _context as well
461-
this._clearContext();
462-
463-
this._context.initialUrl = url;
464-
this._context.initialTimestamp = new Date().getTime();
465-
this._context.urls.push(url);
466-
}
467-
468468
/**
469469
* Adds listeners to record events for the replay
470470
*/
@@ -516,72 +516,6 @@ export class ReplayContainer implements ReplayContainerInterface {
516516
}
517517
}
518518

519-
/**
520-
* Handler for recording events.
521-
*
522-
* Adds to event buffer, and has varying flushing behaviors if the event was a checkout.
523-
*/
524-
private _handleRecordingEmit: (event: RecordingEvent, isCheckout?: boolean) => void = (
525-
event: RecordingEvent,
526-
isCheckout?: boolean,
527-
) => {
528-
// If this is false, it means session is expired, create and a new session and wait for checkout
529-
if (!this.checkAndHandleExpiredSession()) {
530-
__DEBUG_BUILD__ && logger.warn('[Replay] Received replay event after session expired.');
531-
532-
return;
533-
}
534-
535-
this.addUpdate(() => {
536-
// The session is always started immediately on pageload/init, but for
537-
// error-only replays, it should reflect the most recent checkout
538-
// when an error occurs. Clear any state that happens before this current
539-
// checkout. This needs to happen before `addEvent()` which updates state
540-
// dependent on this reset.
541-
if (this.recordingMode === 'error' && event.type === 2) {
542-
this._setInitialState();
543-
}
544-
545-
// We need to clear existing events on a checkout, otherwise they are
546-
// incremental event updates and should be appended
547-
void addEvent(this, event, isCheckout);
548-
549-
// Different behavior for full snapshots (type=2), ignore other event types
550-
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
551-
if (event.type !== 2) {
552-
return false;
553-
}
554-
555-
// If there is a previousSessionId after a full snapshot occurs, then
556-
// the replay session was started due to session expiration. The new session
557-
// is started before triggering a new checkout and contains the id
558-
// of the previous session. Do not immediately flush in this case
559-
// to avoid capturing only the checkout and instead the replay will
560-
// be captured if they perform any follow-up actions.
561-
if (this.session && this.session.previousSessionId) {
562-
return true;
563-
}
564-
565-
// See note above re: session start needs to reflect the most recent
566-
// checkout.
567-
if (this.recordingMode === 'error' && this.session && this._context.earliestEvent) {
568-
this.session.started = this._context.earliestEvent;
569-
this._maybeSaveSession();
570-
}
571-
572-
// Flush immediately so that we do not miss the first segment, otherwise
573-
// it can prevent loading on the UI. This will cause an increase in short
574-
// replays (e.g. opening and closing a tab quickly), but these can be
575-
// filtered on the UI.
576-
if (this.recordingMode === 'session') {
577-
// We want to ensure the worker is ready, as otherwise we'd always send the first event uncompressed
578-
void this.flushImmediate();
579-
}
580-
581-
return true;
582-
});
583-
};
584-
585519
/**
586520
* Handle when visibility of the page content changes. Opening a new tab will
587521
* 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
@@ -313,6 +313,7 @@ export interface ReplayContainer {
313313
getOptions(): ReplayPluginOptions;
314314
getSessionId(): string | undefined;
315315
checkAndHandleExpiredSession(): boolean | void;
316+
setInitialState(): void;
316317
}
317318

318319
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)