Skip to content

Commit e42f283

Browse files
committed
ref: Extract handleRecordingEmit into util
1 parent 45158f4 commit e42f283

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,
@@ -29,6 +28,7 @@ import { createBreadcrumb } from './util/createBreadcrumb';
2928
import { createPerformanceEntries } from './util/createPerformanceEntries';
3029
import { createPerformanceSpans } from './util/createPerformanceSpans';
3130
import { debounce } from './util/debounce';
31+
import { getHandleRecordingEmit } from './util/handleRecordingEmit';
3232
import { isExpired } from './util/isExpired';
3333
import { isSessionExpired } from './util/isSessionExpired';
3434
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent';
@@ -145,7 +145,7 @@ export class ReplayContainer implements ReplayContainerInterface {
145145
* _performanceObserver, Recording, Sentry SDK, etc)
146146
*/
147147
public start(): void {
148-
this._setInitialState();
148+
this.setInitialState();
149149

150150
if (!this._loadAndCheckSession()) {
151151
return;
@@ -197,7 +197,7 @@ export class ReplayContainer implements ReplayContainerInterface {
197197
// Without this, it would record forever, until an error happens, which we don't want
198198
// instead, we'll always keep the last 60 seconds of replay before an error happened
199199
...(this.recordingMode === 'error' && { checkoutEveryNms: ERROR_CHECKOUT_TIME }),
200-
emit: this._handleRecordingEmit,
200+
emit: getHandleRecordingEmit(this),
201201
});
202202
} catch (err) {
203203
this._handleException(err);
@@ -393,6 +393,25 @@ export class ReplayContainer implements ReplayContainerInterface {
393393
return false;
394394
}
395395

396+
/**
397+
* Capture some initial state that can change throughout the lifespan of the
398+
* replay. This is required because otherwise they would be captured at the
399+
* first flush.
400+
*/
401+
public setInitialState(): void {
402+
const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
403+
const url = `${WINDOW.location.origin}${urlPath}`;
404+
405+
this.performanceEvents = [];
406+
407+
// Reset _context as well
408+
this._clearContext();
409+
410+
this._context.initialUrl = url;
411+
this._context.initialTimestamp = new Date().getTime();
412+
this._context.urls.push(url);
413+
}
414+
396415
/** A wrapper to conditionally capture exceptions. */
397416
private _handleException(error: unknown): void {
398417
__DEBUG_BUILD__ && logger.error('[Replay]', error);
@@ -418,7 +437,7 @@ export class ReplayContainer implements ReplayContainerInterface {
418437
// If session was newly created (i.e. was not loaded from storage), then
419438
// enable flag to create the root replay
420439
if (type === 'new') {
421-
this._setInitialState();
440+
this.setInitialState();
422441
}
423442

424443
const currentSessionId = this.getSessionId();
@@ -436,25 +455,6 @@ export class ReplayContainer implements ReplayContainerInterface {
436455
return true;
437456
}
438457

439-
/**
440-
* Capture some initial state that can change throughout the lifespan of the
441-
* replay. This is required because otherwise they would be captured at the
442-
* first flush.
443-
*/
444-
private _setInitialState(): void {
445-
const urlPath = `${WINDOW.location.pathname}${WINDOW.location.hash}${WINDOW.location.search}`;
446-
const url = `${WINDOW.location.origin}${urlPath}`;
447-
448-
this.performanceEvents = [];
449-
450-
// Reset _context as well
451-
this._clearContext();
452-
453-
this._context.initialUrl = url;
454-
this._context.initialTimestamp = new Date().getTime();
455-
this._context.urls.push(url);
456-
}
457-
458458
/**
459459
* Adds listeners to record events for the replay
460460
*/
@@ -506,72 +506,6 @@ export class ReplayContainer implements ReplayContainerInterface {
506506
}
507507
}
508508

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

309310
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.error('[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)