Skip to content

Commit f8bc998

Browse files
committed
update worker
1 parent ea61856 commit f8bc998

File tree

9 files changed

+147
-22
lines changed

9 files changed

+147
-22
lines changed

packages/replay/src/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { GLOBAL_OBJ } from '@sentry/utils';
77
export const WINDOW = GLOBAL_OBJ as typeof GLOBAL_OBJ & Window;
88

99
export const REPLAY_SESSION_KEY = 'sentryReplaySession';
10+
export const PENDING_REPLAY_STATUS_KEY = 'sentryReplayFlushStatus';
11+
export const PENDING_REPLAY_DATA_KEY = 'sentryReplayFlushData';
1012
export const REPLAY_EVENT_NAME = 'replay_event';
1113
export const RECORDING_EVENT_NAME = 'replay_recording';
1214
export const UNABLE_TO_SEND_REPLAY = 'Unable to send Replay';

packages/replay/src/coreHandlers/handleGlobalEvent.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,6 @@ export function handleGlobalEventListener(replay: ReplayContainer): (event: Even
1313
return (event: Event) => {
1414
// Do not apply replayId to the root event
1515
if (event.type === REPLAY_EVENT_NAME) {
16-
// Replays have separate set of breadcrumbs, do not include breadcrumbs
17-
// from core SDK
18-
delete event.breadcrumbs;
1916
return event;
2017
}
2118

packages/replay/src/integration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
170170
return;
171171
}
172172

173-
this._replay.start();
173+
void this._replay.start();
174174
}
175175

176176
/**

packages/replay/src/replay.ts

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,21 @@ import type {
2929
ReplayPluginOptions,
3030
Session,
3131
} from './types';
32+
import { FlushState } from './types';
3233
import { addEvent } from './util/addEvent';
3334
import { addMemoryEntry } from './util/addMemoryEntry';
35+
import { clearPendingReplay } from './util/clearPendingReplay';
3436
import { createBreadcrumb } from './util/createBreadcrumb';
3537
import { createPerformanceEntries } from './util/createPerformanceEntries';
3638
import { createPerformanceSpans } from './util/createPerformanceSpans';
3739
import { debounce } from './util/debounce';
40+
import { getPendingReplay } from './util/getPendingReplay';
3841
import { isExpired } from './util/isExpired';
3942
import { isSessionExpired } from './util/isSessionExpired';
4043
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent';
4144
import { sendReplay } from './util/sendReplay';
42-
import { RateLimitError } from './util/sendReplayRequest';
45+
import { RateLimitError,sendReplayRequest } from './util/sendReplayRequest';
46+
import { setFlushState } from './util/setFlushState';
4347

4448
/**
4549
* The main replay container class, which holds all the state and methods for recording and sending replays.
@@ -151,7 +155,7 @@ export class ReplayContainer implements ReplayContainerInterface {
151155
* Creates or loads a session, attaches listeners to varying events (DOM,
152156
* _performanceObserver, Recording, Sentry SDK, etc)
153157
*/
154-
public start(): void {
158+
public async start(): Promise<void> {
155159
this._setInitialState();
156160

157161
this._loadSession({ expiry: SESSION_IDLE_DURATION });
@@ -162,6 +166,23 @@ export class ReplayContainer implements ReplayContainerInterface {
162166
return;
163167
}
164168

169+
const useCompression = Boolean(this._options.useCompression);
170+
171+
// Flush any pending events that were previously unable to be sent
172+
try {
173+
const pendingEvent = await getPendingReplay({ useCompression });
174+
if (pendingEvent) {
175+
await sendReplayRequest({
176+
...pendingEvent,
177+
session: this.session,
178+
options: this._options,
179+
});
180+
clearPendingReplay();
181+
}
182+
} catch {
183+
// ignore
184+
}
185+
165186
if (!this.session.sampled) {
166187
// If session was not sampled, then we do not initialize the integration at all.
167188
return;
@@ -178,7 +199,7 @@ export class ReplayContainer implements ReplayContainerInterface {
178199
this._updateSessionActivity();
179200

180201
this.eventBuffer = createEventBuffer({
181-
useCompression: Boolean(this._options.useCompression),
202+
useCompression,
182203
});
183204

184205
this._addListeners();
@@ -366,6 +387,7 @@ export class ReplayContainer implements ReplayContainerInterface {
366387
// enable flag to create the root replay
367388
if (type === 'new') {
368389
this._setInitialState();
390+
clearPendingReplay();
369391
}
370392

371393
const currentSessionId = this.getSessionId();
@@ -801,34 +823,45 @@ export class ReplayContainer implements ReplayContainerInterface {
801823
return;
802824
}
803825

804-
await this._addPerformanceEntries();
826+
try {
827+
const promises: Promise<any>[] = [];
805828

806-
// Check eventBuffer again, as it could have been stopped in the meanwhile
807-
if (!this.eventBuffer || !this.eventBuffer.pendingLength) {
808-
return;
809-
}
829+
promises.push(this._addPerformanceEntries());
810830

811-
// Only attach memory event if eventBuffer is not empty
812-
await addMemoryEntry(this);
831+
// Do not continue if there are no pending events in buffer
832+
if (!this.eventBuffer?.pendingLength) {
833+
return;
834+
}
813835

814-
// Check eventBuffer again, as it could have been stopped in the meanwhile
815-
if (!this.eventBuffer) {
816-
return;
817-
}
836+
// Only attach memory entry if eventBuffer is not empty
837+
promises.push(addMemoryEntry(this));
818838

819-
try {
820-
// Note this empties the event buffer regardless of outcome of sending replay
821-
const recordingData = await this.eventBuffer.finish();
839+
// This empties the event buffer regardless of outcome of sending replay
840+
promises.push(this.eventBuffer.finish());
822841

823842
// NOTE: Copy values from instance members, as it's possible they could
824843
// change before the flush finishes.
825844
const replayId = this.session.id;
826845
const eventContext = this._popEventContext();
827846
// Always increment segmentId regardless of outcome of sending replay
828847
const segmentId = this.session.segmentId++;
848+
849+
// Write to local storage before flushing, in case flush request never starts
850+
setFlushState(FlushState.START, {
851+
recordingData: this.eventBuffer.pendingEvents,
852+
replayId,
853+
eventContext,
854+
segmentId,
855+
includeReplayStartTimestamp: segmentId === 0,
856+
timestamp: new Date().getTime(),
857+
});
858+
859+
// Save session (new segment id) after we save flush data assuming
829860
this._maybeSaveSession();
830861

831-
await sendReplay({
862+
const [, , recordingData] = await Promise.all(promises);
863+
864+
const sendReplayPromise = sendReplay({
832865
replayId,
833866
recordingData,
834867
segmentId,
@@ -838,8 +871,16 @@ export class ReplayContainer implements ReplayContainerInterface {
838871
options: this.getOptions(),
839872
timestamp: new Date().getTime(),
840873
});
874+
875+
// If replay request starts, optimistically update some states
876+
setFlushState(FlushState.SENT_REQUEST);
877+
878+
await sendReplayPromise;
879+
880+
setFlushState(FlushState.SENT_REQUEST);
841881
} catch (err) {
842882
this._handleException(err);
883+
setFlushState(FlushState.ERROR);
843884

844885
if (err instanceof RateLimitError) {
845886
this._handleRateLimit(err.rateLimits);

packages/replay/src/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@ export interface SendReplayData {
1818
options: ReplayPluginOptions;
1919
}
2020

21+
export enum FlushState {
22+
START = 'start',
23+
SENT_REQUEST = 'sent-request',
24+
COMPLETE = 'complete',
25+
ERROR = 'error',
26+
}
27+
28+
export type PendingReplayData = Omit<SendReplayData, 'recordingData'|'session'|'options'> & {
29+
recordingData: RecordingEvent[];
30+
};
31+
2132
export type InstrumentationTypeBreadcrumb = 'dom' | 'scope';
2233

2334
/**
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { PENDING_REPLAY_DATA_KEY, PENDING_REPLAY_STATUS_KEY, WINDOW } from '../constants';
2+
3+
/**
4+
* Clears pending segment that was previously unable to be sent (e.g. due to page reload).
5+
*/
6+
export function clearPendingReplay(): void {
7+
WINDOW.sessionStorage.removeItem(PENDING_REPLAY_STATUS_KEY);
8+
WINDOW.sessionStorage.removeItem(PENDING_REPLAY_DATA_KEY);
9+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { PENDING_REPLAY_DATA_KEY, WINDOW } from '../constants';
2+
import { createEventBuffer } from '../eventBuffer';
3+
import type { PendingReplayData, SendReplayData } from '../types';
4+
5+
/**
6+
* Attempts to flush pending segment that was previously unable to be sent
7+
* (e.g. due to page reload).
8+
*/
9+
export async function getPendingReplay({ useCompression }: { useCompression: boolean }): Promise<Omit<SendReplayData, 'session'|'options'> | null> {
10+
try {
11+
const leftoverData = WINDOW.sessionStorage.getItem(PENDING_REPLAY_DATA_KEY);
12+
13+
// Potential data that was not flushed
14+
const parsedData = leftoverData && (JSON.parse(leftoverData) as Partial<PendingReplayData> | null);
15+
const { recordingData, replayId, segmentId, includeReplayStartTimestamp, eventContext, timestamp } = parsedData || {};
16+
17+
// Ensure data integrity -- also *skip* includeReplayStartTimestamp as it
18+
// implies a checkout which we do not store due to potential size
19+
if (!recordingData || !replayId || !segmentId || !eventContext || !timestamp || includeReplayStartTimestamp) {
20+
return null;
21+
}
22+
23+
// start a local eventBuffer
24+
const eventBuffer = createEventBuffer({
25+
useCompression,
26+
});
27+
28+
await Promise.all(recordingData.map(event => eventBuffer.addEvent(event)));
29+
30+
return {
31+
recordingData: await eventBuffer.finish(),
32+
replayId,
33+
segmentId,
34+
includeReplayStartTimestamp: false,
35+
eventContext,
36+
timestamp,
37+
};
38+
} catch {
39+
return null;
40+
}
41+
}

packages/replay/src/util/sendReplayRequest.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ export async function sendReplayRequest({
6969
errorSampleRate: options.errorSampleRate,
7070
};
7171

72+
// Replays have separate set of breadcrumbs, do not include breadcrumbs
73+
// from core SDK
74+
delete replayEvent.breadcrumbs;
75+
7276
/*
7377
For reference, the fully built event looks something like this:
7478
{
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { PENDING_REPLAY_DATA_KEY, PENDING_REPLAY_STATUS_KEY, WINDOW } from '../constants';
2+
import type { PendingReplayData } from '../types';
3+
import { FlushState } from '../types';
4+
5+
/**
6+
*
7+
*/
8+
export function setFlushState(state: FlushState, data?: PendingReplayData): void {
9+
if (data) {
10+
WINDOW.sessionStorage.setItem(PENDING_REPLAY_DATA_KEY, JSON.stringify(data));
11+
}
12+
13+
if (state === FlushState.SENT_REQUEST) {
14+
WINDOW.sessionStorage.removeItem(PENDING_REPLAY_DATA_KEY);
15+
} else if (state === FlushState.COMPLETE) {
16+
WINDOW.sessionStorage.removeItem(PENDING_REPLAY_STATUS_KEY);
17+
} else {
18+
WINDOW.sessionStorage.setItem(PENDING_REPLAY_STATUS_KEY, state);
19+
}
20+
}

0 commit comments

Comments
 (0)