-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: break up large rrweb_events payloads (#338)
* chore: break up large rrweb_events payloads * chore: limit invidiual rrweb event size to 10 mb
- Loading branch information
1 parent
3f26e80
commit 9bff965
Showing
8 changed files
with
321 additions
and
30 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
const CURLY_AND_SQUARE_BRACKET_SIZE = 2; // [] for array, {} for object | ||
const APPROX_ADDITIONAL_SIZE = 1; // additional size for comma and stuff | ||
|
||
export const _fastApproxSizeOf = ( | ||
obj: Record<string, unknown> | Array<unknown>, | ||
max: number, | ||
): number => { | ||
let size = 0; | ||
const keys = Object.keys(obj); | ||
for (let i = 0; i < keys.length; i++) { | ||
const key = keys[i]; | ||
const value = (obj as any)[key]; | ||
size += key.length; | ||
|
||
if (typeof value === 'object' && value !== null) { | ||
size += _fastApproxSizeOf(value, max) + CURLY_AND_SQUARE_BRACKET_SIZE; | ||
} else { | ||
size += String(value).length + APPROX_ADDITIONAL_SIZE; | ||
} | ||
|
||
if (size >= max) { | ||
// exit early if we've exceeded the max | ||
return size; | ||
} | ||
} | ||
|
||
return size; | ||
}; |
119 changes: 119 additions & 0 deletions
119
packages/session-replay/src/__tests__/SessionReplaySlicing.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { MockRemoteServerEvalClient, anyUUID } from 'statsig-test-helpers'; | ||
|
||
import { PrecomputedEvaluationsInterface } from '@statsig/client-core'; | ||
|
||
import { SessionReplay } from '../SessionReplay'; | ||
import { | ||
ReplayEvent, | ||
ReplaySessionData, | ||
SessionReplayClient, | ||
} from '../SessionReplayClient'; | ||
|
||
const DUMMY_DATA = 'a'.repeat(512 * 1024); | ||
|
||
const SMALL_EVENT = { | ||
type: 0, | ||
data: DUMMY_DATA, // 512 KB | ||
timestamp: 0, | ||
}; | ||
|
||
const LARGE_EVENT = { | ||
type: 0, | ||
data: DUMMY_DATA + DUMMY_DATA + DUMMY_DATA, // 1.5 MB | ||
timestamp: 0, | ||
}; | ||
|
||
describe('Session Replay Force', () => { | ||
let client: jest.MockedObject<PrecomputedEvaluationsInterface>; | ||
let sessionReplay: SessionReplay; | ||
let replayer: SessionReplayClient; | ||
let emitter: (latest: ReplayEvent, data: ReplaySessionData) => void; | ||
|
||
async function emitEvent(event: ReplayEvent) { | ||
emitter(event, { | ||
startTime: 0, | ||
endTime: 0, | ||
clickCount: 0, | ||
}); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 1)); // allow promises to resolve | ||
} | ||
|
||
beforeAll(() => { | ||
client = MockRemoteServerEvalClient.create(); | ||
client.flush.mockResolvedValue(); | ||
|
||
const ctx = { | ||
errorBoundary: { wrap: jest.fn() }, | ||
values: { session_recording_rate: 1, can_record_session: true }, | ||
session: { data: { sessionID: 'my-session-id' } }, | ||
} as any; | ||
|
||
client.getContext.mockReturnValue(ctx); | ||
|
||
sessionReplay = new SessionReplay(client); | ||
replayer = (sessionReplay as any)._replayer; | ||
|
||
const original = replayer.record.bind(replayer); | ||
replayer.record = (cb: any, config: any, stopCallback: any) => { | ||
emitter = cb; | ||
return original(cb, config, stopCallback); | ||
}; | ||
|
||
replayer.stop(); | ||
sessionReplay.forceStartRecording(); | ||
}); | ||
|
||
beforeEach(() => { | ||
client.logEvent.mockClear(); | ||
}); | ||
|
||
it('logs a single event when below max payload size', async () => { | ||
await emitEvent(SMALL_EVENT); | ||
|
||
expect(client.logEvent).toHaveBeenCalledTimes(1); | ||
expect(client.flush).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('logs sliced events when above max payload size', async () => { | ||
await emitEvent(LARGE_EVENT); | ||
|
||
expect(client.logEvent).toHaveBeenCalledTimes(2); | ||
expect(client.flush).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
describe('Sliced events payload', () => { | ||
beforeEach(async () => { | ||
await emitEvent(LARGE_EVENT); | ||
}); | ||
|
||
it('logs the first slice with the correct metadata', () => { | ||
const metadata = (client.logEvent.mock.calls[0][0] as any).metadata; | ||
|
||
expect(metadata).toMatchObject({ | ||
sliced_id: anyUUID(), | ||
slice_index: '0', | ||
slice_count: '2', | ||
slice_byte_size: '1048576', | ||
}); | ||
}); | ||
|
||
it('logs the second slice with the correct metadata', () => { | ||
const metadata = (client.logEvent.mock.calls[1][0] as any).metadata; | ||
|
||
expect(metadata).toMatchObject({ | ||
sliced_id: anyUUID(), | ||
slice_index: '1', | ||
slice_count: '2', | ||
slice_byte_size: '524324', | ||
}); | ||
}); | ||
|
||
it('logs the same slice id across the two slices', () => { | ||
const metadata1 = (client.logEvent.mock.calls[0][0] as any).metadata; | ||
const metadata2 = (client.logEvent.mock.calls[1][0] as any).metadata; | ||
|
||
expect(metadata1.sliced_id).toEqual(metadata2.sliced_id); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.