Skip to content

ref(replay): Extract some functions out from class #6448

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 15 commits into from
Dec 9, 2022
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
3 changes: 1 addition & 2 deletions packages/replay/jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
import { getCurrentHub } from '@sentry/core';
import { Transport } from '@sentry/types';

import { ReplayContainer } from './src/replay';
import { Session } from './src/session/Session';
import type { ReplayContainer, Session } from './src/types';

// @ts-ignore TS error, this is replaced in prod builds bc of rollup
global.__SENTRY_REPLAY_VERSION__ = 'version:Test';
Expand Down
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
2 changes: 1 addition & 1 deletion packages/replay/src/coreHandlers/breadcrumbHandler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Breadcrumb, Scope } from '@sentry/types';

import { InstrumentationTypeBreadcrumb } from '../types';
import type { InstrumentationTypeBreadcrumb } from '../types';
import { DomHandlerData, handleDom } from './handleDom';
import { handleScope } from './handleScope';

Expand Down
32 changes: 30 additions & 2 deletions packages/replay/src/coreHandlers/handleFetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ReplayPerformanceEntry } from '../createPerformanceEntry';
import type { ReplayPerformanceEntry } from '../createPerformanceEntry';
import type { ReplayContainer } from '../types';
import { createPerformanceSpans } from '../util/createPerformanceSpans';
import { isIngestHost } from '../util/isIngestHost';

export interface FetchHandlerData {
interface FetchHandlerData {
args: Parameters<typeof fetch>;
fetchData: {
method: string;
Expand All @@ -18,6 +20,7 @@ export interface FetchHandlerData {
endTimestamp?: number;
}

/** only exported for tests */
export function handleFetch(handlerData: FetchHandlerData): null | ReplayPerformanceEntry {
if (!handlerData.endTimestamp) {
return null;
Expand All @@ -41,3 +44,28 @@ export function handleFetch(handlerData: FetchHandlerData): null | ReplayPerform
},
};
}

/**
* Returns a listener to be added to `addInstrumentationHandler('fetch', listener)`.
*/
export function handleFetchSpanListener(replay: ReplayContainer): (handlerData: FetchHandlerData) => void {
return (handlerData: FetchHandlerData) => {
if (!replay.isEnabled()) {
return;
}

const result = handleFetch(handlerData);

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

replay.addUpdate(() => {
createPerformanceSpans(replay, [result]);
// Returning true will cause `addUpdate` to not flush
// We do not want network requests to cause a flush. This will prevent
// recurring/polling requests from keeping the replay session alive.
return true;
});
};
}
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 type { ReplayContainer } from '../types';
import { addInternalBreadcrumb } from '../util/addInternalBreadcrumb';

/**
* Returns a listener to be added to `addGlobalEventProcessor(listener)`.
*/
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' && event.contexts && event.contexts.trace && event.contexts.trace.trace_id) {
replay.getContext().traceIds.add(event.contexts.trace.trace_id as string);
return event;
}

// no event type means error
if (!event.type) {
replay.getContext().errorIds.add(event.event_id as string);
Comment on lines +32 to +38
Copy link
Member

Choose a reason for hiding this comment

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

Might be good to add methods to replay to so that we don't expose the data structure/context here.

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, I would like to keep refining the public API surface of this over time. there are a few more places where IMHO we can/should tighten this up! I'll leave this as is for now, but note down to revisit this!

}

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();

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

return event;
};
}
33 changes: 31 additions & 2 deletions packages/replay/src/coreHandlers/handleHistory.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { ReplayPerformanceEntry } from '../createPerformanceEntry';
import type { ReplayContainer } from '../types';
import { createPerformanceSpans } from '../util/createPerformanceSpans';

export interface HistoryHandlerData {
interface HistoryHandlerData {
from: string;
to: string;
}

export function handleHistory(handlerData: HistoryHandlerData): ReplayPerformanceEntry {
function handleHistory(handlerData: HistoryHandlerData): ReplayPerformanceEntry {
const { from, to } = handlerData;

const now = new Date().getTime() / 1000;
Expand All @@ -20,3 +22,30 @@ export function handleHistory(handlerData: HistoryHandlerData): ReplayPerformanc
},
};
}

/**
* Returns a listener to be added to `addInstrumentationHandler('history', listener)`.
*/
export function handleHistorySpanListener(replay: ReplayContainer): (handlerData: HistoryHandlerData) => void {
return (handlerData: HistoryHandlerData) => {
if (!replay.isEnabled()) {
return;
}

const result = handleHistory(handlerData);

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

// Need to collect visited URLs
replay.getContext().urls.push(result.name);
Copy link
Member

Choose a reason for hiding this comment

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

Similar thing to above with trace/error ids.

replay.triggerUserActivity();

replay.addUpdate(() => {
createPerformanceSpans(replay, [result]);
// Returning false to flush
return false;
});
};
}
31 changes: 29 additions & 2 deletions packages/replay/src/coreHandlers/handleXhr.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ReplayPerformanceEntry } from '../createPerformanceEntry';
import type { ReplayContainer } from '../types';
import { createPerformanceSpans } from '../util/createPerformanceSpans';
import { isIngestHost } from '../util/isIngestHost';

// From sentry-javascript
Expand All @@ -19,14 +21,14 @@ interface SentryWrappedXMLHttpRequest extends XMLHttpRequest {
__sentry_own_request__?: boolean;
}

export interface XhrHandlerData {
interface XhrHandlerData {
args: [string, string];
xhr: SentryWrappedXMLHttpRequest;
startTimestamp: number;
endTimestamp?: number;
}

export function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null {
function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry | null {
if (handlerData.xhr.__sentry_own_request__) {
// Taken from sentry-javascript
// Only capture non-sentry requests
Expand Down Expand Up @@ -61,3 +63,28 @@ export function handleXhr(handlerData: XhrHandlerData): ReplayPerformanceEntry |
},
};
}

/**
* Returns a listener to be added to `addInstrumentationHandler('xhr', listener)`.
*/
export function handleXhrSpanListener(replay: ReplayContainer): (handlerData: XhrHandlerData) => void {
return (handlerData: XhrHandlerData) => {
if (!replay.isEnabled()) {
return;
}

const result = handleXhr(handlerData);

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

replay.addUpdate(() => {
createPerformanceSpans(replay, [result]);
// Returning true will cause `addUpdate` to not flush
// We do not want network requests to cause a flush. This will prevent
// recurring/polling requests from keeping the replay session alive.
return true;
});
};
}
43 changes: 43 additions & 0 deletions packages/replay/src/coreHandlers/performanceObserver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { AllPerformanceEntry, ReplayContainer } from '../types';
import { dedupePerformanceEntries } from '../util/dedupePerformanceEntries';

/**
* Sets up a PerformanceObserver to listen to all performance entry types.
*/
export function setupPerformanceObserver(replay: ReplayContainer): PerformanceObserver {
const performanceObserverHandler = (list: PerformanceObserverEntryList): void => {
// For whatever reason the observer was returning duplicate navigation
// entries (the other entry types were not duplicated).
const newPerformanceEntries = dedupePerformanceEntries(
replay.performanceEvents,
list.getEntries() as AllPerformanceEntry[],
);
replay.performanceEvents = newPerformanceEntries;
};

const performanceObserver = new PerformanceObserver(performanceObserverHandler);

[
'element',
'event',
'first-input',
'largest-contentful-paint',
'layout-shift',
'longtask',
'navigation',
'paint',
'resource',
].forEach(type => {
try {
performanceObserver.observe({
type,
buffered: true,
});
} catch {
// This can throw if an entry type is not supported in the browser.
// Ignore these errors.
}
});

return performanceObserver;
}
17 changes: 0 additions & 17 deletions packages/replay/src/coreHandlers/spanHandler.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/replay/src/createPerformanceEntry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { browserPerformanceTimeOrigin } from '@sentry/utils';
import { record } from 'rrweb';

import { WINDOW } from './constants';
import { AllPerformanceEntry, PerformanceNavigationTiming, PerformancePaintTiming } from './types';
import type { AllPerformanceEntry, PerformanceNavigationTiming, PerformancePaintTiming } from './types';
import { isIngestHost } from './util/isIngestHost';

export interface ReplayPerformanceEntry {
Expand Down
9 changes: 1 addition & 8 deletions packages/replay/src/eventBuffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { captureException } from '@sentry/core';
import { logger } from '@sentry/utils';

import { RecordingEvent, WorkerRequest, WorkerResponse } from './types';
import type { EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from './types';
import workerString from './worker/worker.js';

interface CreateEventBufferParams {
Expand Down Expand Up @@ -35,13 +35,6 @@ export function createEventBuffer({ useCompression }: CreateEventBufferParams):
return new EventBufferArray();
}

export interface EventBuffer {
readonly length: number;
destroy(): void;
addEvent(event: RecordingEvent, isCheckout?: boolean): void;
finish(): Promise<string | Uint8Array>;
}

class EventBufferArray implements EventBuffer {
private _events: RecordingEvent[];

Expand Down
Loading