Skip to content

Commit aeb40b9

Browse files
committed
update worker
1 parent d17ded7 commit aeb40b9

File tree

9 files changed

+143
-17
lines changed

9 files changed

+143
-17
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
@@ -171,7 +171,7 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
171171
return;
172172
}
173173

174-
this._replay.start();
174+
void this._replay.start();
175175
}
176176

177177
/**

packages/replay/src/replay.ts

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,21 @@ import type {
3838
Session,
3939
WorkerAddEventResponse,
4040
} from './types';
41+
import { FlushState } from './types';
4142
import { addEvent } from './util/addEvent';
4243
import { addMemoryEntry } from './util/addMemoryEntry';
44+
import { clearPendingReplay } from './util/clearPendingReplay';
4345
import { createBreadcrumb } from './util/createBreadcrumb';
4446
import { createPerformanceSpans } from './util/createPerformanceSpans';
4547
import { createRecordingData } from './util/createRecordingData';
4648
import { createReplayEnvelope } from './util/createReplayEnvelope';
4749
import { debounce } from './util/debounce';
50+
import { getPendingReplay } from './util/getPendingReplay';
4851
import { isExpired } from './util/isExpired';
4952
import { isSessionExpired } from './util/isSessionExpired';
5053
import { overwriteRecordDroppedEvent, restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent';
5154
import { prepareReplayEvent } from './util/prepareReplayEvent';
55+
import { setFlushState } from './util/setFlushState';
5256

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

170174
this.loadSession({ expiry: SESSION_IDLE_DURATION });
@@ -175,6 +179,19 @@ export class ReplayContainer implements ReplayContainerInterface {
175179
return;
176180
}
177181

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

195212
this.eventBuffer = createEventBuffer({
196-
useCompression: Boolean(this._options.useCompression),
213+
useCompression,
197214
});
198215

199216
this.addListeners();
@@ -318,6 +335,7 @@ export class ReplayContainer implements ReplayContainerInterface {
318335
// enable flag to create the root replay
319336
if (type === 'new') {
320337
this.setInitialState();
338+
clearPendingReplay();
321339
}
322340

323341
if (session.id !== this.session?.id) {
@@ -805,36 +823,61 @@ export class ReplayContainer implements ReplayContainerInterface {
805823
return;
806824
}
807825

808-
await this.addPerformanceEntries();
826+
try {
827+
const promises: Promise<any>[] = [];
809828

810-
if (!this.eventBuffer?.length) {
811-
return;
812-
}
829+
promises.push(this.addPerformanceEntries());
830+
831+
// Do not continue if there are no pending events in buffer
832+
if (!this.eventBuffer?.pendingLength) {
833+
return;
834+
}
813835

814-
// Only attach memory event if eventBuffer is not empty
815-
await addMemoryEntry(this);
836+
// Only attach memory entry if eventBuffer is not empty
837+
promises.push(addMemoryEntry(this));
816838

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

821842
// NOTE: Copy values from instance members, as it's possible they could
822843
// change before the flush finishes.
823844
const replayId = this.session.id;
824845
const eventContext = this.popEventContext();
825846
// Always increment segmentId regardless of outcome of sending replay
826847
const segmentId = this.session.segmentId++;
848+
849+
// Write to local storage before flushing, in case flush request never starts
850+
setFlushState(FlushState.START, {
851+
events: 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
827860
this._maybeSaveSession();
828861

829-
await this.sendReplay({
862+
const [, , recordingData] = await Promise.all(promises);
863+
864+
const sendReplayPromise = this.sendReplay({
830865
replayId,
831866
events: recordingData,
832867
segmentId,
833868
includeReplayStartTimestamp: segmentId === 0,
834869
eventContext,
835870
});
871+
872+
// If replay request starts, optimistically update some states
873+
setFlushState(FlushState.SENT_REQUEST);
874+
875+
await sendReplayPromise;
876+
877+
setFlushState(FlushState.SENT_REQUEST);
836878
} catch (err) {
837879
this.handleException(err);
880+
setFlushState(FlushState.ERROR);
838881
}
839882
}
840883

@@ -956,6 +999,10 @@ export class ReplayContainer implements ReplayContainerInterface {
956999
errorSampleRate: this._options.errorSampleRate,
9571000
};
9581001

1002+
// Replays have separate set of breadcrumbs, do not include breadcrumbs
1003+
// from core SDK
1004+
delete replayEvent.breadcrumbs;
1005+
9591006
/*
9601007
For reference, the fully built event looks something like this:
9611008
{

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+
}

packages/replay/src/worker/worker.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)