Skip to content

Commit 93b8ec5

Browse files
authored
ref(replay): Extract sendReplay functionality into functions (#6751)
1 parent f22366d commit 93b8ec5

14 files changed

+305
-317
lines changed

packages/replay/jest.setup.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
import { getCurrentHub } from '@sentry/core';
3-
import type { ReplayRecordingData,Transport } from '@sentry/types';
3+
import type { ReplayRecordingData, Transport } from '@sentry/types';
44

55
import type { ReplayContainer, Session } from './src/types';
66

@@ -34,7 +34,7 @@ type SentReplayExpected = {
3434
replayEventPayload?: ReplayEventPayload;
3535
recordingHeader?: RecordingHeader;
3636
recordingPayloadHeader?: RecordingPayloadHeader;
37-
events?: ReplayRecordingData;
37+
recordingData?: ReplayRecordingData;
3838
};
3939

4040
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
@@ -79,7 +79,7 @@ function checkCallForSentReplay(
7979
const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems;
8080

8181
// @ts-ignore recordingPayload is always a string in our tests
82-
const [recordingPayloadHeader, events] = recordingPayload?.split('\n') || [];
82+
const [recordingPayloadHeader, recordingData] = recordingPayload?.split('\n') || [];
8383

8484
const actualObj: Required<SentReplayExpected> = {
8585
// @ts-ignore Custom envelope
@@ -91,7 +91,7 @@ function checkCallForSentReplay(
9191
// @ts-ignore Custom envelope
9292
recordingHeader: recordingHeader,
9393
recordingPayloadHeader: recordingPayloadHeader && JSON.parse(recordingPayloadHeader),
94-
events,
94+
recordingData,
9595
};
9696

9797
const isObjectContaining = expected && 'sample' in expected && 'inverse' in expected;

packages/replay/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ export const MASK_ALL_TEXT_SELECTOR = 'body *:not(style), body *:not(script)';
3333
export const DEFAULT_FLUSH_MIN_DELAY = 5_000;
3434
export const DEFAULT_FLUSH_MAX_DELAY = 15_000;
3535
export const INITIAL_FLUSH_DELAY = 5_000;
36+
37+
export const RETRY_BASE_INTERVAL = 5000;
38+
export const RETRY_MAX_COUNT = 3;

packages/replay/src/replay.ts

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

8-
import {
9-
MAX_SESSION_LIFE,
10-
REPLAY_EVENT_NAME,
11-
SESSION_IDLE_DURATION,
12-
UNABLE_TO_SEND_REPLAY,
13-
VISIBILITY_CHANGE_TIMEOUT,
14-
WINDOW,
15-
} from './constants';
8+
import { MAX_SESSION_LIFE, SESSION_IDLE_DURATION, VISIBILITY_CHANGE_TIMEOUT, WINDOW } from './constants';
169
import { breadcrumbHandler } from './coreHandlers/breadcrumbHandler';
1710
import { handleFetchSpanListener } from './coreHandlers/handleFetch';
1811
import { handleGlobalEventListener } from './coreHandlers/handleGlobalEvent';
@@ -34,28 +27,19 @@ import type {
3427
RecordingOptions,
3528
ReplayContainer as ReplayContainerInterface,
3629
ReplayPluginOptions,
37-
SendReplay,
3830
Session,
3931
} from './types';
4032
import { addEvent } from './util/addEvent';
4133
import { addMemoryEntry } from './util/addMemoryEntry';
4234
import { createBreadcrumb } from './util/createBreadcrumb';
4335
import { createPerformanceEntries } from './util/createPerformanceEntries';
4436
import { createPerformanceSpans } from './util/createPerformanceSpans';
45-
import { createRecordingData } from './util/createRecordingData';
46-
import { createReplayEnvelope } from './util/createReplayEnvelope';
4737
import { debounce } from './util/debounce';
4838
import { isExpired } from './util/isExpired';
4939
import { isSessionExpired } from './util/isSessionExpired';
5040
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent';
51-
import { prepareReplayEvent } from './util/prepareReplayEvent';
52-
53-
/**
54-
* Returns true to return control to calling function, otherwise continue with normal batching
55-
*/
56-
57-
const BASE_RETRY_INTERVAL = 5000;
58-
const MAX_RETRY_COUNT = 3;
41+
import { sendReplay } from './util/sendReplay';
42+
import { RateLimitError } from './util/sendReplayRequest';
5943

6044
/**
6145
* The main replay container class, which holds all the state and methods for recording and sending replays.
@@ -86,9 +70,6 @@ export class ReplayContainer implements ReplayContainerInterface {
8670

8771
private _performanceObserver: PerformanceObserver | null = null;
8872

89-
private _retryCount: number = 0;
90-
private _retryInterval: number = BASE_RETRY_INTERVAL;
91-
9273
private _debouncedFlush: ReturnType<typeof debounce>;
9374
private _flushLock: Promise<unknown> | null = null;
9475

@@ -129,11 +110,6 @@ export class ReplayContainer implements ReplayContainerInterface {
129110
initialUrl: '',
130111
};
131112

132-
/**
133-
* A RateLimits object holding the rate-limit durations in case a sent replay event was rate-limited.
134-
*/
135-
private _rateLimits: RateLimits = {};
136-
137113
public constructor({
138114
options,
139115
recordingOptions,
@@ -837,14 +813,20 @@ export class ReplayContainer implements ReplayContainerInterface {
837813
const segmentId = this.session.segmentId++;
838814
this._maybeSaveSession();
839815

840-
await this._sendReplay({
816+
await sendReplay({
841817
replayId,
842-
events: recordingData,
818+
recordingData,
843819
segmentId,
844820
includeReplayStartTimestamp: segmentId === 0,
845821
eventContext,
822+
session: this.session,
823+
options: this.getOptions(),
824+
timestamp: new Date().getTime(),
846825
});
847826
} catch (err) {
827+
if (err instanceof RateLimitError) {
828+
this._handleRateLimit(err.rateLimits);
829+
}
848830
this._handleException(err);
849831
}
850832
}
@@ -897,185 +879,6 @@ export class ReplayContainer implements ReplayContainerInterface {
897879
}
898880
};
899881

900-
/**
901-
* Send replay attachment using `fetch()`
902-
*/
903-
private async _sendReplayRequest({
904-
events,
905-
replayId,
906-
segmentId: segment_id,
907-
includeReplayStartTimestamp,
908-
eventContext,
909-
timestamp = new Date().getTime(),
910-
}: SendReplay): Promise<void | TransportMakeRequestResponse> {
911-
const recordingData = createRecordingData({
912-
events,
913-
headers: {
914-
segment_id,
915-
},
916-
});
917-
918-
const { urls, errorIds, traceIds, initialTimestamp } = eventContext;
919-
920-
const hub = getCurrentHub();
921-
const client = hub.getClient();
922-
const scope = hub.getScope();
923-
const transport = client && client.getTransport();
924-
const dsn = client?.getDsn();
925-
926-
if (!client || !scope || !transport || !dsn || !this.session || !this.session.sampled) {
927-
return;
928-
}
929-
930-
const baseEvent: ReplayEvent = {
931-
// @ts-ignore private api
932-
type: REPLAY_EVENT_NAME,
933-
...(includeReplayStartTimestamp ? { replay_start_timestamp: initialTimestamp / 1000 } : {}),
934-
timestamp: timestamp / 1000,
935-
error_ids: errorIds,
936-
trace_ids: traceIds,
937-
urls,
938-
replay_id: replayId,
939-
segment_id,
940-
replay_type: this.session.sampled,
941-
};
942-
943-
const replayEvent = await prepareReplayEvent({ scope, client, replayId, event: baseEvent });
944-
945-
if (!replayEvent) {
946-
// Taken from baseclient's `_processEvent` method, where this is handled for errors/transactions
947-
client.recordDroppedEvent('event_processor', 'replay_event', baseEvent);
948-
__DEBUG_BUILD__ && logger.log('An event processor returned `null`, will not send event.');
949-
return;
950-
}
951-
952-
replayEvent.tags = {
953-
...replayEvent.tags,
954-
sessionSampleRate: this._options.sessionSampleRate,
955-
errorSampleRate: this._options.errorSampleRate,
956-
};
957-
958-
/*
959-
For reference, the fully built event looks something like this:
960-
{
961-
"type": "replay_event",
962-
"timestamp": 1670837008.634,
963-
"error_ids": [
964-
"errorId"
965-
],
966-
"trace_ids": [
967-
"traceId"
968-
],
969-
"urls": [
970-
"https://example.com"
971-
],
972-
"replay_id": "eventId",
973-
"segment_id": 3,
974-
"replay_type": "error",
975-
"platform": "javascript",
976-
"event_id": "eventId",
977-
"environment": "production",
978-
"sdk": {
979-
"integrations": [
980-
"BrowserTracing",
981-
"Replay"
982-
],
983-
"name": "sentry.javascript.browser",
984-
"version": "7.25.0"
985-
},
986-
"sdkProcessingMetadata": {},
987-
"tags": {
988-
"sessionSampleRate": 1,
989-
"errorSampleRate": 0,
990-
}
991-
}
992-
*/
993-
994-
const envelope = createReplayEnvelope(replayEvent, recordingData, dsn, client.getOptions().tunnel);
995-
996-
try {
997-
const response = await transport.send(envelope);
998-
// TODO (v8): we can remove this guard once transport.send's type signature doesn't include void anymore
999-
if (response) {
1000-
this._rateLimits = updateRateLimits(this._rateLimits, response);
1001-
if (isRateLimited(this._rateLimits, 'replay')) {
1002-
this._handleRateLimit();
1003-
}
1004-
}
1005-
return response;
1006-
} catch {
1007-
throw new Error(UNABLE_TO_SEND_REPLAY);
1008-
}
1009-
}
1010-
1011-
/**
1012-
* Reset the counter of retries for sending replays.
1013-
*/
1014-
private _resetRetries(): void {
1015-
this._retryCount = 0;
1016-
this._retryInterval = BASE_RETRY_INTERVAL;
1017-
}
1018-
1019-
/**
1020-
* Finalize and send the current replay event to Sentry
1021-
*/
1022-
private async _sendReplay({
1023-
replayId,
1024-
events,
1025-
segmentId,
1026-
includeReplayStartTimestamp,
1027-
eventContext,
1028-
}: SendReplay): Promise<unknown> {
1029-
// short circuit if there's no events to upload (this shouldn't happen as _runFlush makes this check)
1030-
if (!events.length) {
1031-
return;
1032-
}
1033-
1034-
try {
1035-
await this._sendReplayRequest({
1036-
events,
1037-
replayId,
1038-
segmentId,
1039-
includeReplayStartTimestamp,
1040-
eventContext,
1041-
});
1042-
this._resetRetries();
1043-
return true;
1044-
} catch (err) {
1045-
// Capture error for every failed replay
1046-
setContext('Replays', {
1047-
_retryCount: this._retryCount,
1048-
});
1049-
this._handleException(err);
1050-
1051-
// If an error happened here, it's likely that uploading the attachment
1052-
// failed, we'll can retry with the same events payload
1053-
if (this._retryCount >= MAX_RETRY_COUNT) {
1054-
throw new Error(`${UNABLE_TO_SEND_REPLAY} - max retries exceeded`);
1055-
}
1056-
1057-
// will retry in intervals of 5, 10, 30
1058-
this._retryInterval = ++this._retryCount * this._retryInterval;
1059-
1060-
return await new Promise((resolve, reject) => {
1061-
setTimeout(async () => {
1062-
try {
1063-
await this._sendReplay({
1064-
replayId,
1065-
events,
1066-
segmentId,
1067-
includeReplayStartTimestamp,
1068-
eventContext,
1069-
});
1070-
resolve(true);
1071-
} catch (err) {
1072-
reject(err);
1073-
}
1074-
}, this._retryInterval);
1075-
});
1076-
}
1077-
}
1078-
1079882
/** Save the session, if it is sticky */
1080883
private _maybeSaveSession(): void {
1081884
if (this.session && this._options.stickySession) {
@@ -1086,14 +889,14 @@ export class ReplayContainer implements ReplayContainerInterface {
1086889
/**
1087890
* Pauses the replay and resumes it after the rate-limit duration is over.
1088891
*/
1089-
private _handleRateLimit(): void {
892+
private _handleRateLimit(rateLimits: RateLimits): void {
1090893
// in case recording is already paused, we don't need to do anything, as we might have already paused because of a
1091894
// rate limit
1092895
if (this.isPaused()) {
1093896
return;
1094897
}
1095898

1096-
const rateLimitEnd = disabledUntil(this._rateLimits, 'replay');
899+
const rateLimitEnd = disabledUntil(rateLimits, 'replay');
1097900
const rateLimitDuration = rateLimitEnd - Date.now();
1098901

1099902
if (rateLimitDuration > 0) {

packages/replay/src/types.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@ import type { eventWithTime, recordOptions } from './types/rrweb';
55
export type RecordingEvent = eventWithTime;
66
export type RecordingOptions = recordOptions;
77

8-
export type RecordedEvents = Uint8Array | string;
9-
108
export type AllPerformanceEntry = PerformancePaintTiming | PerformanceResourceTiming | PerformanceNavigationTiming;
119

12-
export interface SendReplay {
13-
events: RecordedEvents;
10+
export interface SendReplayData {
11+
recordingData: ReplayRecordingData;
1412
replayId: string;
1513
segmentId: number;
1614
includeReplayStartTimestamp: boolean;
1715
eventContext: PopEventContext;
18-
timestamp?: number;
16+
timestamp: number;
17+
session: Session;
18+
options: ReplayPluginOptions;
1919
}
2020

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

0 commit comments

Comments
 (0)