Skip to content

Commit 4009e3f

Browse files
mydeac298lee
andauthored
fix(replay): Ignore old events when manually starting replay (#12349)
This PR ensures that if a replay is manually started (=no sample rates are defined at all, and a user later calls `start()` or `startBuffering()`, we do not back-port the `initialTimestamp` of the replay based on the event buffer. By default (for the first segment) we'll backport the `initialTimestamp` to the time of the first event in the event buffer, to ensure that e.g. the pageload browser metrics that may be emitted with an earlier timestamp all show up correctly. However, this may be unexpected if manually calling `startBuffering()` and seeing things for stuff that happened before. Now, we keep track of this and adjust the behavior accordingly. Fixes #11984 --------- Co-authored-by: Catherine Lee <[email protected]>
1 parent 302f149 commit 4009e3f

File tree

2 files changed

+119
-2
lines changed

2 files changed

+119
-2
lines changed

packages/replay-internal/src/replay.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ export class ReplayContainer implements ReplayContainerInterface {
100100
*/
101101
public readonly timeouts: Timeouts;
102102

103+
/** The replay has to be manually started, because no sample rate (neither session or error) was provided. */
104+
private _requiresManualStart: boolean;
105+
103106
private _throttledAddEvent: (
104107
event: RecordingEvent,
105108
isCheckout?: boolean,
@@ -170,6 +173,7 @@ export class ReplayContainer implements ReplayContainerInterface {
170173
this._lastActivity = Date.now();
171174
this._isEnabled = false;
172175
this._isPaused = false;
176+
this._requiresManualStart = false;
173177
this._hasInitializedCoreListeners = false;
174178
this._context = {
175179
errorIds: new Set(),
@@ -246,7 +250,11 @@ export class ReplayContainer implements ReplayContainerInterface {
246250

247251
// If neither sample rate is > 0, then do nothing - user will need to call one of
248252
// `start()` or `startBuffering` themselves.
249-
if (errorSampleRate <= 0 && sessionSampleRate <= 0) {
253+
const requiresManualStart = errorSampleRate <= 0 && sessionSampleRate <= 0;
254+
255+
this._requiresManualStart = requiresManualStart;
256+
257+
if (requiresManualStart) {
250258
return;
251259
}
252260

@@ -1049,7 +1057,9 @@ export class ReplayContainer implements ReplayContainerInterface {
10491057
/** Update the initial timestamp based on the buffer content. */
10501058
private _updateInitialTimestampFromEventBuffer(): void {
10511059
const { session, eventBuffer } = this;
1052-
if (!session || !eventBuffer) {
1060+
// If replay was started manually (=no sample rate was given),
1061+
// We do not want to back-port the initial timestamp
1062+
if (!session || !eventBuffer || this._requiresManualStart) {
10531063
return;
10541064
}
10551065

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { BASE_TIMESTAMP } from '..';
2+
import { resetSdkMock } from '../mocks/resetSdkMock';
3+
import { useFakeTimers } from '../utils/use-fake-timers';
4+
5+
useFakeTimers();
6+
7+
describe('Integration | early events', () => {
8+
beforeEach(() => {
9+
vi.clearAllMocks();
10+
});
11+
12+
it('updates initialTimestamp for early performance entries', async () => {
13+
const earlyTimeStampSeconds = BASE_TIMESTAMP / 1000 - 10;
14+
15+
const { replay } = await resetSdkMock({
16+
replayOptions: {
17+
stickySession: true,
18+
},
19+
sentryOptions: {
20+
replaysSessionSampleRate: 0,
21+
replaysOnErrorSampleRate: 1.0,
22+
},
23+
});
24+
25+
expect(replay.session).toBeDefined();
26+
expect(replay['_requiresManualStart']).toBe(false);
27+
28+
const initialTimestamp = replay.getContext().initialTimestamp;
29+
30+
expect(initialTimestamp).not.toEqual(earlyTimeStampSeconds * 1000);
31+
32+
// A performance entry that happend before should not extend the session when we manually started
33+
replay.replayPerformanceEntries.push({
34+
type: 'largest-contentful-paint',
35+
name: 'largest-contentful-paint',
36+
start: earlyTimeStampSeconds,
37+
end: earlyTimeStampSeconds,
38+
data: {
39+
value: 100,
40+
size: 100,
41+
nodeId: undefined,
42+
},
43+
});
44+
45+
// _too_ early events are always thrown away
46+
replay.replayPerformanceEntries.push({
47+
type: 'largest-contentful-paint',
48+
name: 'largest-contentful-paint',
49+
start: earlyTimeStampSeconds - 999999,
50+
end: earlyTimeStampSeconds - 99999,
51+
data: {
52+
value: 100,
53+
size: 100,
54+
nodeId: undefined,
55+
},
56+
});
57+
58+
await replay.flushImmediate();
59+
vi.runAllTimers();
60+
61+
expect(replay.getContext().initialTimestamp).toEqual(earlyTimeStampSeconds * 1000);
62+
});
63+
64+
it('does not change initialTimestamp when replay is manually started', async () => {
65+
const earlyTimeStampSeconds = Date.now() / 1000 - 5;
66+
67+
const { replay } = await resetSdkMock({
68+
replayOptions: {
69+
stickySession: true,
70+
},
71+
sentryOptions: {
72+
replaysSessionSampleRate: 0.0,
73+
replaysOnErrorSampleRate: 0.0,
74+
},
75+
});
76+
77+
expect(replay.session).toBe(undefined);
78+
expect(replay['_requiresManualStart']).toBe(true);
79+
80+
replay.start();
81+
vi.runAllTimers();
82+
83+
const initialTimestamp = replay.getContext().initialTimestamp;
84+
85+
expect(initialTimestamp).not.toEqual(earlyTimeStampSeconds * 1000);
86+
expect(replay.session).toBeDefined();
87+
expect(replay['_requiresManualStart']).toBe(true);
88+
89+
// A performance entry that happened before should not extend the session when we manually started
90+
replay.replayPerformanceEntries.push({
91+
type: 'largest-contentful-paint',
92+
name: 'largest-contentful-paint',
93+
start: earlyTimeStampSeconds,
94+
end: earlyTimeStampSeconds,
95+
data: {
96+
value: 100,
97+
size: 100,
98+
nodeId: undefined,
99+
},
100+
});
101+
102+
await replay.flushImmediate();
103+
vi.runAllTimers();
104+
105+
expect(replay.getContext().initialTimestamp).toEqual(initialTimestamp);
106+
});
107+
});

0 commit comments

Comments
 (0)