Skip to content

Commit 753f35a

Browse files
committed
update worker
1 parent ef83db6 commit 753f35a

File tree

8 files changed

+142
-16
lines changed

8 files changed

+142
-16
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 & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,18 +36,22 @@ import type {
3636
SendReplay,
3737
Session,
3838
} from './types';
39+
import { FlushState } from './types';
3940
import { addEvent } from './util/addEvent';
4041
import { addMemoryEntry } from './util/addMemoryEntry';
42+
import { clearPendingReplay } from './util/clearPendingReplay';
4143
import { createBreadcrumb } from './util/createBreadcrumb';
4244
import { createPerformanceEntries } from './util/createPerformanceEntries';
4345
import { createPerformanceSpans } from './util/createPerformanceSpans';
4446
import { createRecordingData } from './util/createRecordingData';
4547
import { createReplayEnvelope } from './util/createReplayEnvelope';
4648
import { debounce } from './util/debounce';
49+
import { getPendingReplay } from './util/getPendingReplay';
4750
import { isExpired } from './util/isExpired';
4851
import { isSessionExpired } from './util/isSessionExpired';
4952
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent';
5053
import { prepareReplayEvent } from './util/prepareReplayEvent';
54+
import { setFlushState } from './util/setFlushState';
5155

5256
/**
5357
* Returns true to return control to calling function, otherwise continue with normal batching
@@ -163,7 +167,7 @@ export class ReplayContainer implements ReplayContainerInterface {
163167
* Creates or loads a session, attaches listeners to varying events (DOM,
164168
* _performanceObserver, Recording, Sentry SDK, etc)
165169
*/
166-
start(): void {
170+
async start(): Promise<void> {
167171
this.setInitialState();
168172

169173
this.loadSession({ expiry: SESSION_IDLE_DURATION });
@@ -174,6 +178,19 @@ export class ReplayContainer implements ReplayContainerInterface {
174178
return;
175179
}
176180

181+
const useCompression = Boolean(this._options.useCompression);
182+
183+
// Flush any pending events that were previously unable to be sent
184+
try {
185+
const pendingEvent = await getPendingReplay({ useCompression });
186+
if (pendingEvent) {
187+
await this.sendReplayRequest(pendingEvent);
188+
clearPendingReplay();
189+
}
190+
} catch {
191+
// ignore
192+
}
193+
177194
if (!this.session.sampled) {
178195
// If session was not sampled, then we do not initialize the integration at all.
179196
return;
@@ -190,7 +207,7 @@ export class ReplayContainer implements ReplayContainerInterface {
190207
this.updateSessionActivity();
191208

192209
this.eventBuffer = createEventBuffer({
193-
useCompression: Boolean(this._options.useCompression),
210+
useCompression,
194211
});
195212

196213
this.addListeners();
@@ -305,6 +322,7 @@ export class ReplayContainer implements ReplayContainerInterface {
305322
// enable flag to create the root replay
306323
if (type === 'new') {
307324
this.setInitialState();
325+
clearPendingReplay();
308326
}
309327

310328
if (session.id !== this.session?.id) {
@@ -792,36 +810,61 @@ export class ReplayContainer implements ReplayContainerInterface {
792810
return;
793811
}
794812

795-
await this.addPerformanceEntries();
813+
try {
814+
const promises: Promise<any>[] = [];
796815

797-
if (!this.eventBuffer?.pendingLength) {
798-
return;
799-
}
816+
promises.push(this.addPerformanceEntries());
817+
818+
// Do not continue if there are no pending events in buffer
819+
if (!this.eventBuffer?.pendingLength) {
820+
return;
821+
}
800822

801-
// Only attach memory event if eventBuffer is not empty
802-
await addMemoryEntry(this);
823+
// Only attach memory entry if eventBuffer is not empty
824+
promises.push(addMemoryEntry(this));
803825

804-
try {
805-
// Note this empties the event buffer regardless of outcome of sending replay
806-
const recordingData = await this.eventBuffer.finish();
826+
// This empties the event buffer regardless of outcome of sending replay
827+
promises.push(this.eventBuffer.finish());
807828

808829
// NOTE: Copy values from instance members, as it's possible they could
809830
// change before the flush finishes.
810831
const replayId = this.session.id;
811832
const eventContext = this.popEventContext();
812833
// Always increment segmentId regardless of outcome of sending replay
813834
const segmentId = this.session.segmentId++;
835+
836+
// Write to local storage before flushing, in case flush request never starts
837+
setFlushState(FlushState.START, {
838+
events: this.eventBuffer.pendingEvents,
839+
replayId,
840+
eventContext,
841+
segmentId,
842+
includeReplayStartTimestamp: segmentId === 0,
843+
timestamp: new Date().getTime(),
844+
});
845+
846+
// Save session (new segment id) after we save flush data assuming
814847
this._maybeSaveSession();
815848

816-
await this.sendReplay({
849+
const [, , recordingData] = await Promise.all(promises);
850+
851+
const sendReplayPromise = this.sendReplay({
817852
replayId,
818853
events: recordingData,
819854
segmentId,
820855
includeReplayStartTimestamp: segmentId === 0,
821856
eventContext,
822857
});
858+
859+
// If replay request starts, optimistically update some states
860+
setFlushState(FlushState.SENT_REQUEST);
861+
862+
await sendReplayPromise;
863+
864+
setFlushState(FlushState.SENT_REQUEST);
823865
} catch (err) {
824866
this.handleException(err);
867+
setFlushState(FlushState.ERROR);
825868
}
826869
}
827870

@@ -943,6 +986,10 @@ export class ReplayContainer implements ReplayContainerInterface {
943986
errorSampleRate: this._options.errorSampleRate,
944987
};
945988

989+
// Replays have separate set of breadcrumbs, do not include breadcrumbs
990+
// from core SDK
991+
delete replayEvent.breadcrumbs;
992+
946993
/*
947994
For reference, the fully built event looks something like this:
948995
{

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 SendReplay {
1818
timestamp?: number;
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<SendReplay, 'events'> & {
29+
events: 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 { PendingReplayData, SendReplay } 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<SendReplay | 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 { events, 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 (!events || !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(events.map(event => eventBuffer.addEvent(event)));
29+
30+
return {
31+
replayId,
32+
segmentId,
33+
eventContext,
34+
timestamp,
35+
includeReplayStartTimestamp: false,
36+
events: await eventBuffer.finish(),
37+
};
38+
} catch {
39+
return null;
40+
}
41+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { PENDING_REPLAY_DATA_KEY, PENDING_REPLAY_STATUS_KEY, WINDOW } from '../constants';
2+
import { FlushState, PendingReplayData } from '../types';
3+
4+
/**
5+
*
6+
*/
7+
export function setFlushState(state: FlushState, data?: PendingReplayData): void {
8+
if (data) {
9+
WINDOW.sessionStorage.setItem(PENDING_REPLAY_DATA_KEY, JSON.stringify(data));
10+
}
11+
12+
if (state === FlushState.SENT_REQUEST) {
13+
WINDOW.sessionStorage.removeItem(PENDING_REPLAY_DATA_KEY);
14+
} else if (state === FlushState.COMPLETE) {
15+
WINDOW.sessionStorage.removeItem(PENDING_REPLAY_STATUS_KEY);
16+
} else {
17+
WINDOW.sessionStorage.setItem(PENDING_REPLAY_STATUS_KEY, state);
18+
}
19+
}

0 commit comments

Comments
 (0)