Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
ref(replay): Extract handleGlobalEvent handler out
  • Loading branch information
mydea committed Dec 9, 2022
commit db1ab04b1b280adcabd1de581983dd9eb41b7c18
1 change: 1 addition & 0 deletions packages/replay/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
export const REPLAY_SESSION_KEY = 'sentryReplaySession';
export const REPLAY_EVENT_NAME = 'replay_event';
export const RECORDING_EVENT_NAME = 'replay_recording';
export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';

// The idle limit for a session
export const SESSION_IDLE_DURATION = 300_000; // 5 minutes in ms
Expand Down
74 changes: 74 additions & 0 deletions packages/replay/src/coreHandlers/handleGlobalEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Event } from '@sentry/types';

import { REPLAY_EVENT_NAME, UNABLE_TO_SEND_REPLAY } from '../constants';
import { ReplayContainer } from '../replay';
import { addInternalBreadcrumb } from '../util/addInternalBreadcrumb';

export function handleGlobalEventListener(replay: ReplayContainer): (event: Event) => Event {
return (event: Event) => {
// Do not apply replayId to the root event
if (
// @ts-ignore new event type
event.type === REPLAY_EVENT_NAME
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

m: can we create a issue or add a todo somewhere that adds this event type to the TS types?

Also @JonasBa do we need to do the same for profiling?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is planned as next step - need to write up an issue!

) {
// Replays have separate set of breadcrumbs, do not include breadcrumbs
// from core SDK
delete event.breadcrumbs;
return event;
}

// Only tag transactions with replayId if not waiting for an error
// @ts-ignore private
if (event.type !== 'transaction' || !replay._waitForError) {
event.tags = { ...event.tags, replayId: replay.session?.id };
}

// Collect traceIds in _context regardless of `_waitForError` - if it's true,
// _context gets cleared on every checkout
if (event.type === 'transaction') {
// @ts-ignore private
replay._context.traceIds.add(String(event.contexts?.trace?.trace_id || ''));
return event;
}

// XXX: Is it safe to assume that all other events are error events?
// @ts-ignore: Type 'undefined' is not assignable to type 'string'.ts(2345)
replay._context.errorIds.add(event.event_id);

const exc = event.exception?.values?.[0];
addInternalBreadcrumb({
message: `Tagging event (${event.event_id}) - ${event.message} - ${exc?.type || 'Unknown'}: ${
exc?.value || 'n/a'
}`,
});

// Need to be very careful that this does not cause an infinite loop
if (
// @ts-ignore private
replay._waitForError &&
event.exception &&
event.message !== UNABLE_TO_SEND_REPLAY // ignore this error because otherwise we could loop indefinitely with trying to capture replay and failing
) {
setTimeout(async () => {
// Allow flush to complete before resuming as a session recording, otherwise
// the checkout from `startRecording` may be included in the payload.
// Prefer to keep the error replay as a separate (and smaller) segment
// than the session replay.
await replay.flushImmediate();

// @ts-ignore private
if (replay._stopRecording) {
// @ts-ignore private
replay._stopRecording();
// Reset all "capture on error" configuration before
// starting a new recording
// @ts-ignore private
replay._waitForError = false;
replay.startRecording();
}
});
}

return event;
};
}
72 changes: 3 additions & 69 deletions packages/replay/src/replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import {
MAX_SESSION_LIFE,
REPLAY_EVENT_NAME,
SESSION_IDLE_DURATION,
UNABLE_TO_SEND_REPLAY,
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 { createMemoryEntry, createPerformanceEntries, ReplayPerformanceEntry } from './createPerformanceEntry';
Expand All @@ -33,7 +35,6 @@ import type {
ReplayPluginOptions,
SendReplay,
} from './types';
import { addInternalBreadcrumb } from './util/addInternalBreadcrumb';
import { captureInternalException } from './util/captureInternalException';
import { createBreadcrumb } from './util/createBreadcrumb';
import { createPayload } from './util/createPayload';
Expand All @@ -49,7 +50,6 @@ type AddUpdateCallback = () => boolean | void;

const BASE_RETRY_INTERVAL = 5000;
const MAX_RETRY_COUNT = 3;
const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';

export class ReplayContainer {
public eventBuffer: EventBuffer | null = null;
Expand Down Expand Up @@ -321,7 +321,7 @@ export class ReplayContainer {

// 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(this.handleGlobalEvent);
addGlobalEventProcessor(handleGlobalEventListener(this));

this._hasInitializedCoreListeners = true;
}
Expand Down Expand Up @@ -412,72 +412,6 @@ export class ReplayContainer {
this._debouncedFlush();
}

/**
* Core Sentry SDK global event handler. Attaches `replayId` to all [non-replay]
* events as a tag. Also handles the case where we only want to capture a reply
* when an error occurs.
**/
handleGlobalEvent: (event: Event) => Event = (event: Event) => {
// Do not apply replayId to the root event
if (
// @ts-ignore new event type
event.type === REPLAY_EVENT_NAME
) {
// Replays have separate set of breadcrumbs, do not include breadcrumbs
// from core SDK
delete event.breadcrumbs;
return event;
}

// Only tag transactions with replayId if not waiting for an error
if (event.type !== 'transaction' || !this._waitForError) {
event.tags = { ...event.tags, replayId: this.session?.id };
}

// Collect traceIds in _context regardless of `_waitForError` - if it's true,
// _context gets cleared on every checkout
if (event.type === 'transaction') {
this._context.traceIds.add(String(event.contexts?.trace?.trace_id || ''));
return event;
}

// XXX: Is it safe to assume that all other events are error events?
// @ts-ignore: Type 'undefined' is not assignable to type 'string'.ts(2345)
this._context.errorIds.add(event.event_id);

const exc = event.exception?.values?.[0];
addInternalBreadcrumb({
message: `Tagging event (${event.event_id}) - ${event.message} - ${exc?.type || 'Unknown'}: ${
exc?.value || 'n/a'
}`,
});

// Need to be very careful that this does not cause an infinite loop
if (
this._waitForError &&
event.exception &&
event.message !== UNABLE_TO_SEND_REPLAY // ignore this error because otherwise we could loop indefinitely with trying to capture replay and failing
) {
setTimeout(async () => {
// Allow flush to complete before resuming as a session recording, otherwise
// the checkout from `startRecording` may be included in the payload.
// Prefer to keep the error replay as a separate (and smaller) segment
// than the session replay.
await this.flushImmediate();

if (this._stopRecording) {
this._stopRecording();
// Reset all "capture on error" configuration before
// starting a new recording
this._waitForError = false;
this.startRecording();
}
});
}

return event;
};

/**
* Handler for recording events.
*
Expand Down
21 changes: 11 additions & 10 deletions packages/replay/test/unit/index-handleGlobalEvent.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getCurrentHub } from '@sentry/core';

import { REPLAY_EVENT_NAME } from '../../src/constants';
import { handleGlobalEventListener } from '../../src/coreHandlers/handleGlobalEvent';
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from '../../src/util/monkeyPatchRecordDroppedEvent';
import { ReplayContainer } from './../../src/replay';
import { Error } from './../fixtures/error';
Expand Down Expand Up @@ -34,14 +35,14 @@ it('deletes breadcrumbs from replay events', () => {
};

// @ts-ignore replay event type
expect(replay.handleGlobalEvent(replayEvent)).toEqual({
expect(handleGlobalEventListener(replay)(replayEvent)).toEqual({
type: REPLAY_EVENT_NAME,
});
});

it('does not delete breadcrumbs from error and transaction events', () => {
expect(
replay.handleGlobalEvent({
handleGlobalEventListener(replay)({
breadcrumbs: [{ type: 'fakecrumb' }],
}),
).toEqual(
Expand All @@ -50,7 +51,7 @@ it('does not delete breadcrumbs from error and transaction events', () => {
}),
);
expect(
replay.handleGlobalEvent({
handleGlobalEventListener(replay)({
type: 'transaction',
breadcrumbs: [{ type: 'fakecrumb' }],
}),
Expand All @@ -65,12 +66,12 @@ it('only tags errors with replay id, adds trace and error id to context for erro
const transaction = Transaction();
const error = Error();
// @ts-ignore idc
expect(replay.handleGlobalEvent(transaction)).toEqual(
expect(handleGlobalEventListener(replay)(transaction)).toEqual(
expect.objectContaining({
tags: expect.not.objectContaining({ replayId: expect.anything() }),
}),
);
expect(replay.handleGlobalEvent(error)).toEqual(
expect(handleGlobalEventListener(replay)(error)).toEqual(
expect.objectContaining({
tags: expect.objectContaining({ replayId: expect.any(String) }),
}),
Expand Down Expand Up @@ -99,9 +100,9 @@ it('strips out dropped events from errorIds', async () => {

const client = getCurrentHub().getClient()!;

replay.handleGlobalEvent(error1);
replay.handleGlobalEvent(error2);
replay.handleGlobalEvent(error3);
handleGlobalEventListener(replay)(error1);
handleGlobalEventListener(replay)(error2);
handleGlobalEventListener(replay)(error3);

client.recordDroppedEvent('before_send', 'error', { event_id: 'err2' });

Expand All @@ -117,12 +118,12 @@ it('tags errors and transactions with replay id for session samples', async () =
const transaction = Transaction();
const error = Error();
// @ts-ignore idc
expect(replay.handleGlobalEvent(transaction)).toEqual(
expect(handleGlobalEventListener(replay)(transaction)).toEqual(
expect.objectContaining({
tags: expect.objectContaining({ replayId: expect.any(String) }),
}),
);
expect(replay.handleGlobalEvent(error)).toEqual(
expect(handleGlobalEventListener(replay)(error)).toEqual(
expect.objectContaining({
tags: expect.objectContaining({ replayId: expect.any(String) }),
}),
Expand Down