Skip to content

Commit 646b54d

Browse files
authored
ref(replay): Move earliest timestamp tracking to eventBuffer (#7983)
In preparation to allow us to handle this better for buffering sessions.
1 parent 62c57a6 commit 646b54d

File tree

11 files changed

+89
-43
lines changed

11 files changed

+89
-43
lines changed

packages/replay/src/eventBuffer/EventBufferArray.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
2+
import { timestampToMs } from '../util/timestampToMs';
23

34
/**
45
* A basic event buffer that does not do any compression.
@@ -44,4 +45,15 @@ export class EventBufferArray implements EventBuffer {
4445
resolve(JSON.stringify(eventsRet));
4546
});
4647
}
48+
49+
/** @inheritdoc */
50+
public getEarliestTimestamp(): number | null {
51+
const timestamp = this.events.map(event => event.timestamp).sort()[0];
52+
53+
if (!timestamp) {
54+
return null;
55+
}
56+
57+
return timestampToMs(timestamp);
58+
}
4759
}

packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,25 @@
11
import type { ReplayRecordingData } from '@sentry/types';
22

33
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
4+
import { timestampToMs } from '../util/timestampToMs';
45
import { WorkerHandler } from './WorkerHandler';
56

67
/**
78
* Event buffer that uses a web worker to compress events.
89
* Exported only for testing.
910
*/
1011
export class EventBufferCompressionWorker implements EventBuffer {
11-
/** @inheritdoc */
12-
public hasEvents: boolean;
13-
1412
private _worker: WorkerHandler;
13+
private _earliestTimestamp: number | null;
1514

1615
public constructor(worker: Worker) {
1716
this._worker = new WorkerHandler(worker);
18-
this.hasEvents = false;
17+
this._earliestTimestamp = null;
18+
}
19+
20+
/** @inheritdoc */
21+
public get hasEvents(): boolean {
22+
return !!this._earliestTimestamp;
1923
}
2024

2125
/**
@@ -39,14 +43,17 @@ export class EventBufferCompressionWorker implements EventBuffer {
3943
* Returns true if event was successfuly received and processed by worker.
4044
*/
4145
public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
42-
this.hasEvents = true;
43-
4446
if (isCheckout) {
4547
// This event is a checkout, make sure worker buffer is cleared before
4648
// proceeding.
4749
await this._clear();
4850
}
4951

52+
const timestamp = timestampToMs(event.timestamp);
53+
if (!this._earliestTimestamp || timestamp < this._earliestTimestamp) {
54+
this._earliestTimestamp = timestamp;
55+
}
56+
5057
return this._sendEventToWorker(event);
5158
}
5259

@@ -57,6 +64,11 @@ export class EventBufferCompressionWorker implements EventBuffer {
5764
return this._finishRequest();
5865
}
5966

67+
/** @inheritdoc */
68+
public getEarliestTimestamp(): number | null {
69+
return this._earliestTimestamp;
70+
}
71+
6072
/**
6173
* Send the event to the worker.
6274
*/
@@ -70,13 +82,14 @@ export class EventBufferCompressionWorker implements EventBuffer {
7082
private async _finishRequest(): Promise<Uint8Array> {
7183
const response = await this._worker.postMessage<Uint8Array>('finish');
7284

73-
this.hasEvents = false;
85+
this._earliestTimestamp = null;
7486

7587
return response;
7688
}
7789

7890
/** Clear any pending events from the worker. */
7991
private _clear(): Promise<void> {
92+
this._earliestTimestamp = null;
8093
return this._worker.postMessage('clear');
8194
}
8295
}

packages/replay/src/eventBuffer/EventBufferProxy.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export class EventBufferProxy implements EventBuffer {
3535
this._compression.destroy();
3636
}
3737

38+
/** @inheritdoc */
39+
public getEarliestTimestamp(): number | null {
40+
return this._used.getEarliestTimestamp();
41+
}
42+
3843
/**
3944
* Add an event to the event buffer.
4045
*

packages/replay/src/replay.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export class ReplayContainer implements ReplayContainerInterface {
118118
errorIds: new Set(),
119119
traceIds: new Set(),
120120
urls: [],
121-
earliestEvent: null,
122121
initialTimestamp: Date.now(),
123122
initialUrl: '',
124123
};
@@ -819,22 +818,35 @@ export class ReplayContainer implements ReplayContainerInterface {
819818
this._context.errorIds.clear();
820819
this._context.traceIds.clear();
821820
this._context.urls = [];
822-
this._context.earliestEvent = null;
821+
}
822+
823+
/** Update the initial timestamp based on the buffer content. */
824+
private _updateInitialTimestampFromEventBuffer(): void {
825+
const { session, eventBuffer } = this;
826+
if (!session || !eventBuffer) {
827+
return;
828+
}
829+
830+
// we only ever update this on the initial segment
831+
if (session.segmentId) {
832+
return;
833+
}
834+
835+
const earliestEvent = eventBuffer.getEarliestTimestamp();
836+
if (earliestEvent && earliestEvent < this._context.initialTimestamp) {
837+
this._context.initialTimestamp = earliestEvent;
838+
}
823839
}
824840

825841
/**
826842
* Return and clear _context
827843
*/
828844
private _popEventContext(): PopEventContext {
829-
if (this._context.earliestEvent && this._context.earliestEvent < this._context.initialTimestamp) {
830-
this._context.initialTimestamp = this._context.earliestEvent;
831-
}
832-
833845
const _context = {
834846
initialTimestamp: this._context.initialTimestamp,
835847
initialUrl: this._context.initialUrl,
836-
errorIds: Array.from(this._context.errorIds).filter(Boolean),
837-
traceIds: Array.from(this._context.traceIds).filter(Boolean),
848+
errorIds: Array.from(this._context.errorIds),
849+
traceIds: Array.from(this._context.traceIds),
838850
urls: this._context.urls,
839851
};
840852

@@ -873,6 +885,9 @@ export class ReplayContainer implements ReplayContainerInterface {
873885
}
874886

875887
try {
888+
// This uses the data from the eventBuffer, so we need to call this before `finish()
889+
this._updateInitialTimestampFromEventBuffer();
890+
876891
// Note this empties the event buffer regardless of outcome of sending replay
877892
const recordingData = await this.eventBuffer.finish();
878893

packages/replay/src/types.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ interface CommonEventContext {
360360
initialUrl: string;
361361

362362
/**
363-
* The initial starting timestamp of the session
363+
* The initial starting timestamp in ms of the session.
364364
*/
365365
initialTimestamp: number;
366366

@@ -395,11 +395,6 @@ export interface InternalEventContext extends CommonEventContext {
395395
* Set of Sentry trace ids that have occurred during a replay segment
396396
*/
397397
traceIds: Set<string>;
398-
399-
/**
400-
* The timestamp of the earliest event that has been added to event buffer. This can happen due to the Performance Observer which buffers events.
401-
*/
402-
earliestEvent: number | null;
403398
}
404399

405400
export type Sampled = false | 'session' | 'buffer';
@@ -408,12 +403,12 @@ export interface Session {
408403
id: string;
409404

410405
/**
411-
* Start time of current session
406+
* Start time of current session (in ms)
412407
*/
413408
started: number;
414409

415410
/**
416-
* Last known activity of the session
411+
* Last known activity of the session (in ms)
417412
*/
418413
lastActivity: number;
419414

@@ -463,6 +458,11 @@ export interface EventBuffer {
463458
* Clears and returns the contents of the buffer.
464459
*/
465460
finish(): Promise<ReplayRecordingData>;
461+
462+
/**
463+
* Get the earliest timestamp in ms of any event currently in the buffer.
464+
*/
465+
getEarliestTimestamp(): number | null;
466466
}
467467

468468
export type AddUpdateCallback = () => boolean | void;

packages/replay/src/util/addEvent.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { getCurrentHub } from '@sentry/core';
22
import { logger } from '@sentry/utils';
33

44
import type { AddEventResult, RecordingEvent, ReplayContainer } from '../types';
5+
import { timestampToMs } from './timestampToMs';
56

67
/**
78
* Add an event to the event buffer.
@@ -22,10 +23,7 @@ export async function addEvent(
2223
return null;
2324
}
2425

25-
// TODO: sadness -- we will want to normalize timestamps to be in ms -
26-
// requires coordination with frontend
27-
const isMs = event.timestamp > 9999999999;
28-
const timestampInMs = isMs ? event.timestamp : event.timestamp * 1000;
26+
const timestampInMs = timestampToMs(event.timestamp);
2927

3028
// Throw out events that happen more than 5 minutes ago. This can happen if
3129
// page has been left open and idle for a long period of time and user
@@ -35,13 +33,6 @@ export async function addEvent(
3533
return null;
3634
}
3735

38-
// Only record earliest event if a new session was created, otherwise it
39-
// shouldn't be relevant
40-
const earliestEvent = replay.getContext().earliestEvent;
41-
if (replay.session && replay.session.segmentId === 0 && (!earliestEvent || timestampInMs < earliestEvent)) {
42-
replay.getContext().earliestEvent = timestampInMs;
43-
}
44-
4536
try {
4637
return await replay.eventBuffer.addEvent(event, isCheckout);
4738
} catch (error) {

packages/replay/src/util/handleRecordingEmit.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ export function getHandleRecordingEmit(replay: ReplayContainer): RecordingEmitCa
5858
return true;
5959
}
6060

61-
// See note above re: session start needs to reflect the most recent
62-
// checkout.
63-
if (replay.recordingMode === 'buffer' && replay.session) {
64-
const { earliestEvent } = replay.getContext();
61+
// When in buffer mode, make sure we adjust the session started date to the current earliest event of the buffer
62+
// this should usually be the timestamp of the checkout event, but to be safe...
63+
if (replay.recordingMode === 'buffer' && replay.session && replay.eventBuffer) {
64+
const earliestEvent = replay.eventBuffer.getEarliestTimestamp();
6565
if (earliestEvent) {
6666
replay.session.started = earliestEvent;
6767

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/**
2+
* Converts a timestamp to ms, if it was in s, or keeps it as ms.
3+
*/
4+
export function timestampToMs(timestamp: number): number {
5+
const isMs = timestamp > 9999999999;
6+
return isMs ? timestamp : timestamp * 1000;
7+
}

packages/replay/test/integration/events.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ describe('Integration | events', () => {
156156
);
157157

158158
// This should be null because `addEvent` has not been called yet
159-
expect(replay.getContext().earliestEvent).toBe(null);
159+
expect(replay.eventBuffer?.getEarliestTimestamp()).toBe(null);
160160
expect(mockTransportSend).toHaveBeenCalledTimes(0);
161161

162162
// A new checkout occurs (i.e. a new session was started)
@@ -196,6 +196,6 @@ describe('Integration | events', () => {
196196
});
197197

198198
// This gets reset after sending replay
199-
expect(replay.getContext().earliestEvent).toBe(null);
199+
expect(replay.eventBuffer?.getEarliestTimestamp()).toBe(null);
200200
});
201201
});

packages/replay/test/integration/sampling.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,13 @@ describe('Integration | sampling', () => {
2424
jest.runAllTimers();
2525

2626
expect(replay.session).toBe(undefined);
27+
expect(replay.eventBuffer).toBeNull();
2728

2829
// This is what the `_context` member is initialized with
2930
expect(replay.getContext()).toEqual({
3031
errorIds: new Set(),
3132
traceIds: new Set(),
3233
urls: [],
33-
earliestEvent: null,
3434
initialTimestamp: expect.any(Number),
3535
initialUrl: '',
3636
});
@@ -63,11 +63,12 @@ describe('Integration | sampling', () => {
6363
jest.runAllTimers();
6464

6565
expect(replay.session?.id).toBeDefined();
66+
expect(replay.eventBuffer).toBeDefined();
67+
expect(replay.eventBuffer?.getEarliestTimestamp()).toEqual(expect.any(Number));
6668

6769
// This is what the `_context` member is initialized with
6870
expect(replay.getContext()).toEqual({
6971
errorIds: new Set(),
70-
earliestEvent: expect.any(Number),
7172
initialTimestamp: expect.any(Number),
7273
initialUrl: 'http://localhost/',
7374
traceIds: new Set(),

packages/replay/test/integration/session.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,9 +221,11 @@ describe('Integration | session', () => {
221221
]),
222222
});
223223

224+
// Earliest event is reset
225+
expect(replay.eventBuffer?.getEarliestTimestamp()).toBeNull();
226+
224227
// `_context` should be reset when a new session is created
225228
expect(replay.getContext()).toEqual({
226-
earliestEvent: null,
227229
initialUrl: 'http://dummy/',
228230
initialTimestamp: newTimestamp,
229231
urls: [],

0 commit comments

Comments
 (0)