Skip to content

Commit bee8d52

Browse files
authored
ref(replay): Extract some more handlers out into functional style (#6893)
1 parent e66a0c0 commit bee8d52

File tree

9 files changed

+166
-134
lines changed

9 files changed

+166
-134
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { Breadcrumb } from '@sentry/types';
2+
import { EventType } from 'rrweb';
3+
4+
import type { ReplayContainer } from '../types';
5+
import { addEvent } from '../util/addEvent';
6+
7+
/**
8+
* Add a breadcrumb event to replay.
9+
*/
10+
export function addBreadcrumbEvent(replay: ReplayContainer, breadcrumb: Breadcrumb): void {
11+
if (breadcrumb.category === 'sentry.transaction') {
12+
return;
13+
}
14+
15+
if (breadcrumb.category === 'ui.click') {
16+
replay.triggerUserActivity();
17+
} else {
18+
replay.checkAndHandleExpiredSession();
19+
}
20+
21+
replay.addUpdate(() => {
22+
void addEvent(replay, {
23+
type: EventType.Custom,
24+
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
25+
// but maybe we should just keep them as milliseconds
26+
timestamp: (breadcrumb.timestamp || 0) * 1000,
27+
data: {
28+
tag: 'breadcrumb',
29+
payload: breadcrumb,
30+
},
31+
});
32+
33+
// Do not flush after console log messages
34+
return breadcrumb.category === 'console';
35+
});
36+
}

packages/replay/src/coreHandlers/breadcrumbHandler.ts

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

packages/replay/src/coreHandlers/handleDom.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,35 @@ import type { Breadcrumb } from '@sentry/types';
22
import { htmlTreeAsString } from '@sentry/utils';
33
import { record } from 'rrweb';
44

5+
import type { ReplayContainer } from '../types';
56
import { createBreadcrumb } from '../util/createBreadcrumb';
7+
import { addBreadcrumbEvent } from './addBreadcrumbEvent';
68

7-
export interface DomHandlerData {
9+
interface DomHandlerData {
810
name: string;
911
event: Node | { target: Node };
1012
}
1113

14+
export const handleDomListener: (replay: ReplayContainer) => (handlerData: DomHandlerData) => void =
15+
(replay: ReplayContainer) =>
16+
(handlerData: DomHandlerData): void => {
17+
if (!replay.isEnabled()) {
18+
return;
19+
}
20+
21+
const result = handleDom(handlerData);
22+
23+
if (!result) {
24+
return;
25+
}
26+
27+
addBreadcrumbEvent(replay, result);
28+
};
29+
1230
/**
1331
* An event handler to react to DOM events.
1432
*/
15-
export function handleDom(handlerData: DomHandlerData): Breadcrumb | null {
33+
function handleDom(handlerData: DomHandlerData): Breadcrumb | null {
1634
// Taken from https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/integrations/breadcrumbs.ts#L112
1735
let target;
1836
let targetNode;

packages/replay/src/coreHandlers/handleScope.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,27 @@
11
import type { Breadcrumb, Scope } from '@sentry/types';
22

3+
import type { ReplayContainer } from '../types';
34
import { createBreadcrumb } from '../util/createBreadcrumb';
5+
import { addBreadcrumbEvent } from './addBreadcrumbEvent';
46

57
let _LAST_BREADCRUMB: null | Breadcrumb = null;
68

9+
export const handleScopeListener: (replay: ReplayContainer) => (scope: Scope) => void =
10+
(replay: ReplayContainer) =>
11+
(scope: Scope): void => {
12+
if (!replay.isEnabled()) {
13+
return;
14+
}
15+
16+
const result = handleScope(scope);
17+
18+
if (!result) {
19+
return;
20+
}
21+
22+
addBreadcrumbEvent(replay, result);
23+
};
24+
725
/**
826
* An event handler to handle scope changes.
927
*/

packages/replay/src/replay.ts

Lines changed: 46 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
/* eslint-disable max-lines */ // TODO: We might want to split this file up
2-
import { addGlobalEventProcessor, captureException, getCurrentHub } from '@sentry/core';
2+
import { captureException } from '@sentry/core';
33
import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types';
44
import type { RateLimits } from '@sentry/utils';
5-
import { addInstrumentationHandler, disabledUntil, logger } from '@sentry/utils';
5+
import { disabledUntil, logger } from '@sentry/utils';
66
import { EventType, record } from 'rrweb';
77

88
import { MAX_SESSION_LIFE, SESSION_IDLE_DURATION, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from './constants';
9-
import { breadcrumbHandler } from './coreHandlers/breadcrumbHandler';
10-
import { handleFetchSpanListener } from './coreHandlers/handleFetch';
11-
import { handleGlobalEventListener } from './coreHandlers/handleGlobalEvent';
12-
import { handleHistorySpanListener } from './coreHandlers/handleHistory';
13-
import { handleXhrSpanListener } from './coreHandlers/handleXhr';
149
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
1510
import { createEventBuffer } from './eventBuffer';
1611
import { getSession } from './session/getSession';
@@ -20,7 +15,6 @@ import type {
2015
AddUpdateCallback,
2116
AllPerformanceEntry,
2217
EventBuffer,
23-
InstrumentationTypeBreadcrumb,
2418
InternalEventContext,
2519
PopEventContext,
2620
RecordingEvent,
@@ -30,6 +24,7 @@ import type {
3024
Session,
3125
} from './types';
3226
import { addEvent } from './util/addEvent';
27+
import { addGlobalListeners } from './util/addGlobalListeners';
3328
import { addMemoryEntry } from './util/addMemoryEntry';
3429
import { createBreadcrumb } from './util/createBreadcrumb';
3530
import { createPerformanceEntries } from './util/createPerformanceEntries';
@@ -318,7 +313,7 @@ export class ReplayContainer implements ReplayContainerInterface {
318313
}
319314

320315
// Otherwise... recording was never suspended, continue as normalish
321-
this._checkAndHandleExpiredSession();
316+
this.checkAndHandleExpiredSession();
322317

323318
this._updateSessionActivity();
324319
}
@@ -340,6 +335,44 @@ export class ReplayContainer implements ReplayContainerInterface {
340335
return this.session && this.session.id;
341336
}
342337

338+
/**
339+
* Checks if recording should be stopped due to user inactivity. Otherwise
340+
* check if session is expired and create a new session if so. Triggers a new
341+
* full snapshot on new session.
342+
*
343+
* Returns true if session is not expired, false otherwise.
344+
* @hidden
345+
*/
346+
public checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void {
347+
const oldSessionId = this.getSessionId();
348+
349+
// Prevent starting a new session if the last user activity is older than
350+
// MAX_SESSION_LIFE. Otherwise non-user activity can trigger a new
351+
// session+recording. This creates noisy replays that do not have much
352+
// content in them.
353+
if (this._lastActivity && isExpired(this._lastActivity, MAX_SESSION_LIFE)) {
354+
// Pause recording
355+
this.pause();
356+
return;
357+
}
358+
359+
// --- There is recent user activity --- //
360+
// This will create a new session if expired, based on expiry length
361+
this._loadSession({ expiry });
362+
363+
// Session was expired if session ids do not match
364+
const expired = oldSessionId !== this.getSessionId();
365+
366+
if (!expired) {
367+
return true;
368+
}
369+
370+
// Session is expired, trigger a full snapshot (which will create a new session)
371+
this._triggerFullSnapshot();
372+
373+
return false;
374+
}
375+
343376
/** A wrapper to conditionally capture exceptions. */
344377
private _handleException(error: unknown): void {
345378
__DEBUG_BUILD__ && logger.error('[Replay]', error);
@@ -409,19 +442,7 @@ export class ReplayContainer implements ReplayContainerInterface {
409442

410443
// There is no way to remove these listeners, so ensure they are only added once
411444
if (!this._hasInitializedCoreListeners) {
412-
// Listeners from core SDK //
413-
const scope = getCurrentHub().getScope();
414-
if (scope) {
415-
scope.addScopeListener(this._handleCoreBreadcrumbListener('scope'));
416-
}
417-
addInstrumentationHandler('dom', this._handleCoreBreadcrumbListener('dom'));
418-
addInstrumentationHandler('fetch', handleFetchSpanListener(this));
419-
addInstrumentationHandler('xhr', handleXhrSpanListener(this));
420-
addInstrumentationHandler('history', handleHistorySpanListener(this));
421-
422-
// Tag all (non replay) events that get sent to Sentry with the current
423-
// replay ID so that we can reference them later in the UI
424-
addGlobalEventProcessor(handleGlobalEventListener(this));
445+
addGlobalListeners(this);
425446

426447
this._hasInitializedCoreListeners = true;
427448
}
@@ -468,7 +489,7 @@ export class ReplayContainer implements ReplayContainerInterface {
468489
isCheckout?: boolean,
469490
) => {
470491
// If this is false, it means session is expired, create and a new session and wait for checkout
471-
if (!this._checkAndHandleExpiredSession()) {
492+
if (!this.checkAndHandleExpiredSession()) {
472493
__DEBUG_BUILD__ && logger.error('[Replay] Received replay event after session expired.');
473494

474495
return;
@@ -563,51 +584,6 @@ export class ReplayContainer implements ReplayContainerInterface {
563584
this._doChangeToForegroundTasks(breadcrumb);
564585
};
565586

566-
/**
567-
* Handler for Sentry Core SDK events.
568-
*
569-
* These events will create breadcrumb-like objects in the recording.
570-
*/
571-
private _handleCoreBreadcrumbListener: (type: InstrumentationTypeBreadcrumb) => (handlerData: unknown) => void =
572-
(type: InstrumentationTypeBreadcrumb) =>
573-
(handlerData: unknown): void => {
574-
if (!this._isEnabled) {
575-
return;
576-
}
577-
578-
const result = breadcrumbHandler(type, handlerData);
579-
580-
if (result === null) {
581-
return;
582-
}
583-
584-
if (result.category === 'sentry.transaction') {
585-
return;
586-
}
587-
588-
if (result.category === 'ui.click') {
589-
this.triggerUserActivity();
590-
} else {
591-
this._checkAndHandleExpiredSession();
592-
}
593-
594-
this.addUpdate(() => {
595-
void addEvent(this, {
596-
type: EventType.Custom,
597-
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
598-
// but maybe we should just keep them as milliseconds
599-
timestamp: (result.timestamp || 0) * 1000,
600-
data: {
601-
tag: 'breadcrumb',
602-
payload: result,
603-
},
604-
});
605-
606-
// Do not flush after console log messages
607-
return result.category === 'console';
608-
});
609-
};
610-
611587
/**
612588
* Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
613589
*/
@@ -636,7 +612,7 @@ export class ReplayContainer implements ReplayContainerInterface {
636612
return;
637613
}
638614

639-
const isSessionActive = this._checkAndHandleExpiredSession({
615+
const isSessionActive = this.checkAndHandleExpiredSession({
640616
expiry: VISIBILITY_CHANGE_TIMEOUT,
641617
});
642618

@@ -707,43 +683,6 @@ export class ReplayContainer implements ReplayContainerInterface {
707683
return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries)));
708684
}
709685

710-
/**
711-
* Checks if recording should be stopped due to user inactivity. Otherwise
712-
* check if session is expired and create a new session if so. Triggers a new
713-
* full snapshot on new session.
714-
*
715-
* Returns true if session is not expired, false otherwise.
716-
*/
717-
private _checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void {
718-
const oldSessionId = this.getSessionId();
719-
720-
// Prevent starting a new session if the last user activity is older than
721-
// MAX_SESSION_LIFE. Otherwise non-user activity can trigger a new
722-
// session+recording. This creates noisy replays that do not have much
723-
// content in them.
724-
if (this._lastActivity && isExpired(this._lastActivity, MAX_SESSION_LIFE)) {
725-
// Pause recording
726-
this.pause();
727-
return;
728-
}
729-
730-
// --- There is recent user activity --- //
731-
// This will create a new session if expired, based on expiry length
732-
this._loadSession({ expiry });
733-
734-
// Session was expired if session ids do not match
735-
const expired = oldSessionId !== this.getSessionId();
736-
737-
if (!expired) {
738-
return true;
739-
}
740-
741-
// Session is expired, trigger a full snapshot (which will create a new session)
742-
this._triggerFullSnapshot();
743-
744-
return false;
745-
}
746-
747686
/**
748687
* Only flush if `this.recordingMode === 'session'`
749688
*/
@@ -862,7 +801,7 @@ export class ReplayContainer implements ReplayContainerInterface {
862801
return;
863802
}
864803

865-
if (!this._checkAndHandleExpiredSession()) {
804+
if (!this.checkAndHandleExpiredSession()) {
866805
__DEBUG_BUILD__ && logger.error('[Replay] Attempting to finish replay event after session expired.');
867806
return;
868807
}

packages/replay/src/types.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@ export interface SendReplayData {
1818
options: ReplayPluginOptions;
1919
}
2020

21-
export type InstrumentationTypeBreadcrumb = 'dom' | 'scope';
22-
2321
/**
2422
* The request payload to worker
2523
*/
@@ -251,6 +249,7 @@ export interface ReplayContainer {
251249
addUpdate(cb: AddUpdateCallback): void;
252250
getOptions(): ReplayPluginOptions;
253251
getSessionId(): string | undefined;
252+
checkAndHandleExpiredSession(): boolean | void;
254253
}
255254

256255
export interface ReplayPerformanceEntry {

0 commit comments

Comments
 (0)