Skip to content

ref(replay): Extract some more handlers out into functional style #6893

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions packages/replay/src/coreHandlers/addBreadcrumbEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Breadcrumb } from '@sentry/types';
import { EventType } from 'rrweb';

import type { ReplayContainer } from '../types';
import { addEvent } from '../util/addEvent';

/**
* Add a breadcrumb event to replay.
*/
export function addBreadcrumbEvent(replay: ReplayContainer, breadcrumb: Breadcrumb): void {
if (breadcrumb.category === 'sentry.transaction') {
return;
}

if (breadcrumb.category === 'ui.click') {
replay.triggerUserActivity();
} else {
replay.checkAndHandleExpiredSession();
}

replay.addUpdate(() => {
void addEvent(replay, {
type: EventType.Custom,
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
// but maybe we should just keep them as milliseconds
timestamp: (breadcrumb.timestamp || 0) * 1000,
data: {
tag: 'breadcrumb',
payload: breadcrumb,
},
});

// Do not flush after console log messages
return breadcrumb.category === 'console';
});
}
17 changes: 0 additions & 17 deletions packages/replay/src/coreHandlers/breadcrumbHandler.ts

This file was deleted.

22 changes: 20 additions & 2 deletions packages/replay/src/coreHandlers/handleDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,35 @@ import type { Breadcrumb } from '@sentry/types';
import { htmlTreeAsString } from '@sentry/utils';
import { record } from 'rrweb';

import type { ReplayContainer } from '../types';
import { createBreadcrumb } from '../util/createBreadcrumb';
import { addBreadcrumbEvent } from './addBreadcrumbEvent';

export interface DomHandlerData {
interface DomHandlerData {
name: string;
event: Node | { target: Node };
}

export const handleDomListener: (replay: ReplayContainer) => (handlerData: DomHandlerData) => void =
(replay: ReplayContainer) =>
(handlerData: DomHandlerData): void => {
if (!replay.isEnabled()) {
return;
}

const result = handleDom(handlerData);

if (!result) {
return;
}

addBreadcrumbEvent(replay, result);
};

/**
* An event handler to react to DOM events.
*/
export function handleDom(handlerData: DomHandlerData): Breadcrumb | null {
function handleDom(handlerData: DomHandlerData): Breadcrumb | null {
// Taken from https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/integrations/breadcrumbs.ts#L112
let target;
let targetNode;
Expand Down
18 changes: 18 additions & 0 deletions packages/replay/src/coreHandlers/handleScope.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,27 @@
import type { Breadcrumb, Scope } from '@sentry/types';

import type { ReplayContainer } from '../types';
import { createBreadcrumb } from '../util/createBreadcrumb';
import { addBreadcrumbEvent } from './addBreadcrumbEvent';

let _LAST_BREADCRUMB: null | Breadcrumb = null;

export const handleScopeListener: (replay: ReplayContainer) => (scope: Scope) => void =
(replay: ReplayContainer) =>
(scope: Scope): void => {
if (!replay.isEnabled()) {
return;
}

const result = handleScope(scope);

if (!result) {
return;
}

addBreadcrumbEvent(replay, result);
};

/**
* An event handler to handle scope changes.
*/
Expand Down
153 changes: 46 additions & 107 deletions packages/replay/src/replay.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
/* eslint-disable max-lines */ // TODO: We might want to split this file up
import { addGlobalEventProcessor, captureException, getCurrentHub } from '@sentry/core';
import { captureException } from '@sentry/core';
import type { Breadcrumb, ReplayRecordingMode } from '@sentry/types';
import type { RateLimits } from '@sentry/utils';
import { addInstrumentationHandler, disabledUntil, logger } from '@sentry/utils';
import { disabledUntil, logger } from '@sentry/utils';
import { EventType, record } from 'rrweb';

import { MAX_SESSION_LIFE, SESSION_IDLE_DURATION, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from './constants';
import { breadcrumbHandler } from './coreHandlers/breadcrumbHandler';
import { handleFetchSpanListener } from './coreHandlers/handleFetch';
import { handleGlobalEventListener } from './coreHandlers/handleGlobalEvent';
import { handleHistorySpanListener } from './coreHandlers/handleHistory';
import { handleXhrSpanListener } from './coreHandlers/handleXhr';
import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
import { createEventBuffer } from './eventBuffer';
import { getSession } from './session/getSession';
Expand All @@ -20,7 +15,6 @@ import type {
AddUpdateCallback,
AllPerformanceEntry,
EventBuffer,
InstrumentationTypeBreadcrumb,
InternalEventContext,
PopEventContext,
RecordingEvent,
Expand All @@ -30,6 +24,7 @@ import type {
Session,
} from './types';
import { addEvent } from './util/addEvent';
import { addGlobalListeners } from './util/addGlobalListeners';
import { addMemoryEntry } from './util/addMemoryEntry';
import { createBreadcrumb } from './util/createBreadcrumb';
import { createPerformanceEntries } from './util/createPerformanceEntries';
Expand Down Expand Up @@ -318,7 +313,7 @@ export class ReplayContainer implements ReplayContainerInterface {
}

// Otherwise... recording was never suspended, continue as normalish
this._checkAndHandleExpiredSession();
this.checkAndHandleExpiredSession();

this._updateSessionActivity();
}
Expand All @@ -340,6 +335,44 @@ export class ReplayContainer implements ReplayContainerInterface {
return this.session && this.session.id;
}

/**
* Checks if recording should be stopped due to user inactivity. Otherwise
* check if session is expired and create a new session if so. Triggers a new
* full snapshot on new session.
*
* Returns true if session is not expired, false otherwise.
* @hidden
*/
public checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void {
const oldSessionId = this.getSessionId();

// Prevent starting a new session if the last user activity is older than
// MAX_SESSION_LIFE. Otherwise non-user activity can trigger a new
// session+recording. This creates noisy replays that do not have much
// content in them.
if (this._lastActivity && isExpired(this._lastActivity, MAX_SESSION_LIFE)) {
// Pause recording
this.pause();
return;
}

// --- There is recent user activity --- //
// This will create a new session if expired, based on expiry length
this._loadSession({ expiry });

// Session was expired if session ids do not match
const expired = oldSessionId !== this.getSessionId();

if (!expired) {
return true;
}

// Session is expired, trigger a full snapshot (which will create a new session)
this._triggerFullSnapshot();

return false;
}

/** A wrapper to conditionally capture exceptions. */
private _handleException(error: unknown): void {
__DEBUG_BUILD__ && logger.error('[Replay]', error);
Expand Down Expand Up @@ -409,19 +442,7 @@ export class ReplayContainer implements ReplayContainerInterface {

// There is no way to remove these listeners, so ensure they are only added once
if (!this._hasInitializedCoreListeners) {
// Listeners from core SDK //
const scope = getCurrentHub().getScope();
if (scope) {
scope.addScopeListener(this._handleCoreBreadcrumbListener('scope'));
}
addInstrumentationHandler('dom', this._handleCoreBreadcrumbListener('dom'));
addInstrumentationHandler('fetch', handleFetchSpanListener(this));
addInstrumentationHandler('xhr', handleXhrSpanListener(this));
addInstrumentationHandler('history', handleHistorySpanListener(this));

// Tag all (non replay) events that get sent to Sentry with the current
// replay ID so that we can reference them later in the UI
addGlobalEventProcessor(handleGlobalEventListener(this));
addGlobalListeners(this);

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

return;
Expand Down Expand Up @@ -563,51 +584,6 @@ export class ReplayContainer implements ReplayContainerInterface {
this._doChangeToForegroundTasks(breadcrumb);
};

/**
* Handler for Sentry Core SDK events.
*
* These events will create breadcrumb-like objects in the recording.
*/
private _handleCoreBreadcrumbListener: (type: InstrumentationTypeBreadcrumb) => (handlerData: unknown) => void =
(type: InstrumentationTypeBreadcrumb) =>
(handlerData: unknown): void => {
if (!this._isEnabled) {
return;
}

const result = breadcrumbHandler(type, handlerData);

if (result === null) {
return;
}

if (result.category === 'sentry.transaction') {
return;
}

if (result.category === 'ui.click') {
this.triggerUserActivity();
} else {
this._checkAndHandleExpiredSession();
}

this.addUpdate(() => {
void addEvent(this, {
type: EventType.Custom,
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
// but maybe we should just keep them as milliseconds
timestamp: (result.timestamp || 0) * 1000,
data: {
tag: 'breadcrumb',
payload: result,
},
});

// Do not flush after console log messages
return result.category === 'console';
});
};

/**
* Tasks to run when we consider a page to be hidden (via blurring and/or visibility)
*/
Expand Down Expand Up @@ -636,7 +612,7 @@ export class ReplayContainer implements ReplayContainerInterface {
return;
}

const isSessionActive = this._checkAndHandleExpiredSession({
const isSessionActive = this.checkAndHandleExpiredSession({
expiry: VISIBILITY_CHANGE_TIMEOUT,
});

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

/**
* Checks if recording should be stopped due to user inactivity. Otherwise
* check if session is expired and create a new session if so. Triggers a new
* full snapshot on new session.
*
* Returns true if session is not expired, false otherwise.
*/
private _checkAndHandleExpiredSession({ expiry = SESSION_IDLE_DURATION }: { expiry?: number } = {}): boolean | void {
const oldSessionId = this.getSessionId();

// Prevent starting a new session if the last user activity is older than
// MAX_SESSION_LIFE. Otherwise non-user activity can trigger a new
// session+recording. This creates noisy replays that do not have much
// content in them.
if (this._lastActivity && isExpired(this._lastActivity, MAX_SESSION_LIFE)) {
// Pause recording
this.pause();
return;
}

// --- There is recent user activity --- //
// This will create a new session if expired, based on expiry length
this._loadSession({ expiry });

// Session was expired if session ids do not match
const expired = oldSessionId !== this.getSessionId();

if (!expired) {
return true;
}

// Session is expired, trigger a full snapshot (which will create a new session)
this._triggerFullSnapshot();

return false;
}

/**
* Only flush if `this.recordingMode === 'session'`
*/
Expand Down Expand Up @@ -862,7 +801,7 @@ export class ReplayContainer implements ReplayContainerInterface {
return;
}

if (!this._checkAndHandleExpiredSession()) {
if (!this.checkAndHandleExpiredSession()) {
__DEBUG_BUILD__ && logger.error('[Replay] Attempting to finish replay event after session expired.');
return;
}
Expand Down
3 changes: 1 addition & 2 deletions packages/replay/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ export interface SendReplayData {
options: ReplayPluginOptions;
}

export type InstrumentationTypeBreadcrumb = 'dom' | 'scope';

/**
* The request payload to worker
*/
Expand Down Expand Up @@ -251,6 +249,7 @@ export interface ReplayContainer {
addUpdate(cb: AddUpdateCallback): void;
getOptions(): ReplayPluginOptions;
getSessionId(): string | undefined;
checkAndHandleExpiredSession(): boolean | void;
}

export interface ReplayPerformanceEntry {
Expand Down
Loading