Skip to content

Commit ae7e13e

Browse files
committed
fix(replay): Ensure handleRecordingEmit aborts when event is not added
1 parent 86b3c69 commit ae7e13e

File tree

3 files changed

+67
-28
lines changed

3 files changed

+67
-28
lines changed

packages/replay/src/replay.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import type {
3838
Timeouts,
3939
} from './types';
4040
import { ReplayEventTypeCustom } from './types';
41-
import { addEvent } from './util/addEvent';
41+
import { addEvent, addEventSync } from './util/addEvent';
4242
import { addGlobalListeners } from './util/addGlobalListeners';
4343
import { addMemoryEntry } from './util/addMemoryEntry';
4444
import { createBreadcrumb } from './util/createBreadcrumb';
@@ -692,7 +692,8 @@ export class ReplayContainer implements ReplayContainerInterface {
692692
});
693693

694694
this.addUpdate(() => {
695-
void addEvent(this, {
695+
// Return `false` if the event _was_ added, as that means we schedule a flush
696+
return !addEventSync(this, {
696697
type: ReplayEventTypeCustom,
697698
timestamp: breadcrumb.timestamp || 0,
698699
data: {

packages/replay/src/util/addEvent.ts

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,39 +13,46 @@ function isCustomEvent(event: RecordingEvent): event is ReplayFrameEvent {
1313

1414
/**
1515
* Add an event to the event buffer.
16+
* In contrast to `addEvent`, this does not return a promise & does not wait for the adding of the event to succeed/fail.
17+
* Instead this returns `true` if we tried to add the event, else false.
18+
* It returns `false` e.g. if we are paused, disabled, or out of the max replay duration.
19+
*
20+
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
21+
*/
22+
export function addEventSync(replay: ReplayContainer, event: RecordingEvent, isCheckout?: boolean): boolean {
23+
if (!shouldAddEvent(replay, event)) {
24+
return false;
25+
}
26+
27+
void _addEvent(replay, event, isCheckout);
28+
29+
return true;
30+
}
31+
32+
/**
33+
* Add an event to the event buffer.
34+
* Resolves to `null` if no event was added, else to `void`.
35+
*
1636
* `isCheckout` is true if this is either the very first event, or an event triggered by `checkoutEveryNms`.
1737
*/
1838
export async function addEvent(
1939
replay: ReplayContainer,
2040
event: RecordingEvent,
2141
isCheckout?: boolean,
2242
): Promise<AddEventResult | null> {
23-
if (!replay.eventBuffer) {
24-
// This implies that `_isEnabled` is false
43+
if (!shouldAddEvent(replay, event)) {
2544
return null;
2645
}
2746

28-
if (replay.isPaused()) {
29-
// Do not add to event buffer when recording is paused
30-
return null;
31-
}
32-
33-
const timestampInMs = timestampToMs(event.timestamp);
34-
35-
// Throw out events that happen more than 5 minutes ago. This can happen if
36-
// page has been left open and idle for a long period of time and user
37-
// comes back to trigger a new session. The performance entries rely on
38-
// `performance.timeOrigin`, which is when the page first opened.
39-
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
40-
return null;
41-
}
47+
return _addEvent(replay, event, isCheckout);
48+
}
4249

43-
// Throw out events that are +60min from the initial timestamp
44-
if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) {
45-
logInfo(
46-
`[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`,
47-
replay.getOptions()._experiments.traceInternals,
48-
);
50+
async function _addEvent(
51+
replay: ReplayContainer,
52+
event: RecordingEvent,
53+
isCheckout?: boolean,
54+
): Promise<AddEventResult | null> {
55+
if (!replay.eventBuffer) {
4956
return null;
5057
}
5158

@@ -81,6 +88,34 @@ export async function addEvent(
8188
}
8289
}
8390

91+
function shouldAddEvent(replay: ReplayContainer, event: RecordingEvent): boolean {
92+
if (!replay.eventBuffer || replay.isPaused() || !replay.isEnabled()) {
93+
// This implies that `_isEnabled` is false
94+
return false;
95+
}
96+
97+
const timestampInMs = timestampToMs(event.timestamp);
98+
99+
// Throw out events that happen more than 5 minutes ago. This can happen if
100+
// page has been left open and idle for a long period of time and user
101+
// comes back to trigger a new session. The performance entries rely on
102+
// `performance.timeOrigin`, which is when the page first opened.
103+
if (timestampInMs + replay.timeouts.sessionIdlePause < Date.now()) {
104+
return false;
105+
}
106+
107+
// Throw out events that are +60min from the initial timestamp
108+
if (timestampInMs > replay.getContext().initialTimestamp + replay.getOptions().maxReplayDuration) {
109+
logInfo(
110+
`[Replay] Skipping event with timestamp ${timestampInMs} because it is after maxReplayDuration`,
111+
replay.getOptions()._experiments.traceInternals,
112+
);
113+
return false;
114+
}
115+
116+
return true;
117+
}
118+
84119
function maybeApplyCallback(
85120
event: RecordingEvent,
86121
callback: ReplayPluginOptions['beforeAddRecordingEvent'],

packages/replay/src/util/handleRecordingEmit.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { logger } from '@sentry/utils';
33

44
import { saveSession } from '../session/saveSession';
55
import type { AddEventResult, RecordingEvent, ReplayContainer, ReplayOptionFrameEvent } from '../types';
6-
import { addEvent } from './addEvent';
6+
import { addEvent, addEventSync } from './addEvent';
77
import { logInfo } from './log';
88

99
type RecordingEmitCallback = (event: RecordingEvent, isCheckout?: boolean) => void;
@@ -40,9 +40,12 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
4040
replay.setInitialState();
4141
}
4242

43-
// We need to clear existing events on a checkout, otherwise they are
44-
// incremental event updates and should be appended
45-
void addEvent(replay, event, isCheckout);
43+
// If the event is not added (e.g. due to being paused, disabled, or out of the max replay duration),
44+
// Skip all further steps
45+
if (!addEventSync(replay, event, isCheckout)) {
46+
// Return true to skip scheduling a debounced flush
47+
return true;
48+
}
4649

4750
// Different behavior for full snapshots (type=2), ignore other event types
4851
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16

0 commit comments

Comments
 (0)