mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
493 lines
14 KiB
TypeScript
493 lines
14 KiB
TypeScript
import {EventType, IncrementalSource} from '@sentry-internal/rrweb';
|
|
import {
|
|
ReplayClickEventFixture,
|
|
ReplayConsoleEventFixture,
|
|
ReplayDeadClickEventFixture,
|
|
ReplayMemoryEventFixture,
|
|
ReplayNavigateEventFixture,
|
|
} from 'sentry-fixture/replay/helpers';
|
|
import {ReplayNavFrameFixture} from 'sentry-fixture/replay/replayBreadcrumbFrameData';
|
|
import {
|
|
ReplayBreadcrumbFrameEventFixture,
|
|
ReplayOptionFrameEventFixture,
|
|
ReplayOptionFrameFixture,
|
|
ReplaySpanFrameEventFixture,
|
|
} from 'sentry-fixture/replay/replayFrameEvents';
|
|
import {ReplayRequestFrameFixture} from 'sentry-fixture/replay/replaySpanFrameData';
|
|
import {
|
|
RRWebDOMFrameFixture,
|
|
RRWebFullSnapshotFrameEventFixture,
|
|
} from 'sentry-fixture/replay/rrweb';
|
|
import {ReplayErrorFixture} from 'sentry-fixture/replayError';
|
|
import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';
|
|
|
|
import {BreadcrumbType} from 'sentry/types/breadcrumbs';
|
|
import ReplayReader from 'sentry/utils/replays/replayReader';
|
|
|
|
describe('ReplayReader', () => {
|
|
const replayRecord = ReplayRecordFixture({});
|
|
|
|
it('Should return null if there are missing arguments', () => {
|
|
const missingAttachments = ReplayReader.factory({
|
|
attachments: undefined,
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
expect(missingAttachments).toBeNull();
|
|
|
|
const missingErrors = ReplayReader.factory({
|
|
attachments: [],
|
|
errors: undefined,
|
|
replayRecord,
|
|
});
|
|
expect(missingErrors).toBeNull();
|
|
|
|
const missingRecord = ReplayReader.factory({
|
|
attachments: [],
|
|
errors: [],
|
|
replayRecord: undefined,
|
|
});
|
|
expect(missingRecord).toBeNull();
|
|
});
|
|
|
|
it('should calculate started_at/finished_at/duration based on first/last events', () => {
|
|
const minuteZero = new Date('2023-12-25T00:00:00');
|
|
const minuteTen = new Date('2023-12-25T00:10:00');
|
|
|
|
const replay = ReplayReader.factory({
|
|
attachments: [
|
|
ReplayConsoleEventFixture({timestamp: minuteZero}),
|
|
ReplayConsoleEventFixture({timestamp: minuteTen}),
|
|
],
|
|
errors: [],
|
|
replayRecord: ReplayRecordFixture({
|
|
started_at: new Date('2023-12-25T00:01:00'),
|
|
finished_at: new Date('2023-12-25T00:09:00'),
|
|
duration: undefined, // will be calculated
|
|
}),
|
|
});
|
|
|
|
const expectedDuration = 10 * 60 * 1000; // 10 minutes, in ms
|
|
expect(replay?.getReplay().started_at).toEqual(minuteZero);
|
|
expect(replay?.getReplay().finished_at).toEqual(minuteTen);
|
|
expect(replay?.getReplay().duration.asMilliseconds()).toEqual(expectedDuration);
|
|
expect(replay?.getDurationMs()).toEqual(expectedDuration);
|
|
});
|
|
|
|
it('should make the replayRecord available through a getter method', () => {
|
|
const replay = ReplayReader.factory({
|
|
attachments: [],
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
expect(replay?.getReplay()).toEqual(replayRecord);
|
|
});
|
|
|
|
describe('attachment splitting', () => {
|
|
const timestamp = new Date('2023-12-25T00:02:00');
|
|
const secondTimestamp = new Date('2023-12-25T00:04:00');
|
|
const thirdTimestamp = new Date('2023-12-25T00:05:00');
|
|
|
|
const optionsFrame = ReplayOptionFrameFixture({});
|
|
const optionsEvent = ReplayOptionFrameEventFixture({
|
|
timestamp,
|
|
data: {payload: optionsFrame},
|
|
});
|
|
const firstDiv = RRWebFullSnapshotFrameEventFixture({timestamp});
|
|
const secondDiv = RRWebFullSnapshotFrameEventFixture({timestamp});
|
|
const clickEvent = ReplayClickEventFixture({timestamp});
|
|
const secondClickEvent = ReplayClickEventFixture({timestamp: secondTimestamp});
|
|
const thirdClickEvent = ReplayClickEventFixture({timestamp: thirdTimestamp});
|
|
const deadClickEvent = ReplayDeadClickEventFixture({timestamp});
|
|
const firstMemory = ReplayMemoryEventFixture({
|
|
startTimestamp: timestamp,
|
|
endTimestamp: timestamp,
|
|
});
|
|
const secondMemory = ReplayMemoryEventFixture({
|
|
startTimestamp: timestamp,
|
|
endTimestamp: timestamp,
|
|
});
|
|
const navigationEvent = ReplayNavigateEventFixture({
|
|
startTimestamp: new Date('2023-12-25T00:03:00'),
|
|
endTimestamp: new Date('2023-12-25T00:03:30'),
|
|
});
|
|
const navCrumb = ReplayBreadcrumbFrameEventFixture({
|
|
timestamp: new Date('2023-12-25T00:03:00'),
|
|
data: {
|
|
payload: ReplayNavFrameFixture({
|
|
timestamp: new Date('2023-12-25T00:03:00'),
|
|
}),
|
|
},
|
|
});
|
|
const consoleEvent = ReplayConsoleEventFixture({timestamp});
|
|
const customEvent = ReplayBreadcrumbFrameEventFixture({
|
|
timestamp: new Date('2023-12-25T00:02:30'),
|
|
data: {
|
|
payload: {
|
|
category: 'redux.action',
|
|
data: {
|
|
action: 'save.click',
|
|
},
|
|
message: '',
|
|
timestamp: new Date('2023-12-25T00:02:30').getTime() / 1000,
|
|
type: BreadcrumbType.DEFAULT,
|
|
},
|
|
},
|
|
});
|
|
const attachments = [
|
|
clickEvent,
|
|
secondClickEvent,
|
|
thirdClickEvent,
|
|
consoleEvent,
|
|
firstDiv,
|
|
firstMemory,
|
|
navigationEvent,
|
|
navCrumb,
|
|
optionsEvent,
|
|
secondDiv,
|
|
secondMemory,
|
|
customEvent,
|
|
deadClickEvent,
|
|
];
|
|
|
|
it.each([
|
|
{
|
|
method: 'getRRWebFrames',
|
|
expected: [
|
|
{
|
|
type: EventType.Custom,
|
|
timestamp: expect.any(Number),
|
|
data: {tag: 'replay.start', payload: {}},
|
|
},
|
|
firstDiv,
|
|
secondDiv,
|
|
{
|
|
type: EventType.Custom,
|
|
timestamp: expect.any(Number),
|
|
data: {tag: 'replay.end', payload: {}},
|
|
},
|
|
],
|
|
},
|
|
{
|
|
method: 'getConsoleFrames',
|
|
expected: [
|
|
expect.objectContaining({category: 'console'}),
|
|
expect.objectContaining({category: 'redux.action'}),
|
|
],
|
|
},
|
|
{
|
|
method: 'getNetworkFrames',
|
|
expected: [expect.objectContaining({op: 'navigation.navigate'})],
|
|
},
|
|
{
|
|
method: 'getDOMFrames',
|
|
expected: [
|
|
expect.objectContaining({category: 'ui.slowClickDetected'}),
|
|
expect.objectContaining({category: 'ui.click'}),
|
|
expect.objectContaining({category: 'ui.click'}),
|
|
],
|
|
},
|
|
{
|
|
method: 'getMemoryFrames',
|
|
expected: [
|
|
expect.objectContaining({op: 'memory'}),
|
|
expect.objectContaining({op: 'memory'}),
|
|
],
|
|
},
|
|
{
|
|
method: 'getChapterFrames',
|
|
expected: [
|
|
expect.objectContaining({category: 'replay.init'}),
|
|
expect.objectContaining({category: 'ui.slowClickDetected'}),
|
|
expect.objectContaining({category: 'navigation'}),
|
|
expect.objectContaining({op: 'navigation.navigate'}),
|
|
expect.objectContaining({category: 'ui.click'}),
|
|
expect.objectContaining({category: 'ui.click'}),
|
|
],
|
|
},
|
|
{
|
|
method: 'getSDKOptions',
|
|
expected: optionsFrame,
|
|
},
|
|
])('Calling $method will filter frames', ({method, expected}) => {
|
|
const replay = ReplayReader.factory({
|
|
attachments,
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
const exec = replay?.[method];
|
|
expect(exec()).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
it('shoud return the SDK config if there is a RecordingOptions event found', () => {
|
|
const timestamp = new Date();
|
|
const optionsFrame = ReplayOptionFrameFixture({});
|
|
|
|
const replay = ReplayReader.factory({
|
|
attachments: [
|
|
ReplayOptionFrameEventFixture({
|
|
timestamp,
|
|
data: {payload: optionsFrame},
|
|
}),
|
|
],
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
expect(replay?.getSDKOptions()).toBe(optionsFrame);
|
|
});
|
|
|
|
describe('isNetworkDetailsSetup', () => {
|
|
it('should have isNetworkDetailsSetup=true if sdkConfig says so', () => {
|
|
const timestamp = new Date();
|
|
|
|
const replay = ReplayReader.factory({
|
|
attachments: [
|
|
ReplayOptionFrameEventFixture({
|
|
timestamp,
|
|
data: {
|
|
payload: ReplayOptionFrameFixture({
|
|
networkDetailHasUrls: true,
|
|
}),
|
|
},
|
|
}),
|
|
],
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
expect(replay?.isNetworkDetailsSetup()).toBeTruthy();
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
data: {
|
|
method: 'GET',
|
|
request: {headers: {accept: 'application/json'}},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
data: {
|
|
method: 'GET',
|
|
},
|
|
expected: false,
|
|
},
|
|
])('should have isNetworkDetailsSetup=$expected', ({data, expected}) => {
|
|
const startTimestamp = new Date();
|
|
const endTimestamp = new Date();
|
|
const replay = ReplayReader.factory({
|
|
attachments: [
|
|
ReplaySpanFrameEventFixture({
|
|
timestamp: startTimestamp,
|
|
data: {
|
|
payload: ReplayRequestFrameFixture({
|
|
op: 'resource.fetch',
|
|
startTimestamp,
|
|
endTimestamp,
|
|
description: '/api/0/issues/',
|
|
data,
|
|
}),
|
|
},
|
|
}),
|
|
],
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
expect(replay?.isNetworkDetailsSetup()).toBe(expected);
|
|
});
|
|
});
|
|
|
|
it('detects canvas element from full snapshot', () => {
|
|
const timestamp = new Date('2023-12-25T00:02:00');
|
|
|
|
const firstDiv = RRWebFullSnapshotFrameEventFixture({
|
|
timestamp,
|
|
childNodes: [
|
|
RRWebDOMFrameFixture({
|
|
tagName: 'div',
|
|
childNodes: [
|
|
RRWebDOMFrameFixture({
|
|
tagName: 'canvas',
|
|
}),
|
|
],
|
|
}),
|
|
],
|
|
});
|
|
const attachments = [firstDiv];
|
|
|
|
const replay = ReplayReader.factory({
|
|
attachments,
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
expect(replay?.hasCanvasElementInReplay()).toBe(true);
|
|
});
|
|
|
|
it('detects canvas element from dom mutations', () => {
|
|
const timestamp = new Date('2023-12-25T00:02:00');
|
|
|
|
const snapshot = RRWebFullSnapshotFrameEventFixture({timestamp});
|
|
const attachments = [
|
|
snapshot,
|
|
{
|
|
type: EventType.IncrementalSnapshot,
|
|
timestamp,
|
|
data: {
|
|
source: IncrementalSource.Mutation,
|
|
adds: [
|
|
{
|
|
node: RRWebDOMFrameFixture({
|
|
tagName: 'canvas',
|
|
}),
|
|
},
|
|
],
|
|
removes: [],
|
|
texts: [],
|
|
attributes: [],
|
|
},
|
|
},
|
|
];
|
|
|
|
const replay = ReplayReader.factory({
|
|
attachments,
|
|
errors: [],
|
|
replayRecord,
|
|
});
|
|
|
|
expect(replay?.hasCanvasElementInReplay()).toBe(true);
|
|
});
|
|
|
|
describe('clip window', () => {
|
|
const replayStartedAt = new Date('2024-01-01T00:02:00');
|
|
const replayFinishedAt = new Date('2024-01-01T00:04:00');
|
|
|
|
const clipStartTimestamp = new Date('2024-01-01T00:03:00');
|
|
const clipEndTimestamp = new Date('2024-01-01T00:03:10');
|
|
|
|
const rrwebFrame1 = RRWebFullSnapshotFrameEventFixture({
|
|
timestamp: new Date('2024-01-01T00:02:30'),
|
|
});
|
|
const rrwebFrame2 = RRWebFullSnapshotFrameEventFixture({
|
|
timestamp: new Date('2024-01-01T00:03:09'),
|
|
});
|
|
const rrwebFrame3 = RRWebFullSnapshotFrameEventFixture({
|
|
timestamp: new Date('2024-01-01T00:03:30'),
|
|
});
|
|
|
|
const breadcrumbAttachment1 = ReplayBreadcrumbFrameEventFixture({
|
|
timestamp: new Date('2024-01-01T00:02:30'),
|
|
data: {
|
|
payload: ReplayNavFrameFixture({
|
|
timestamp: new Date('2024-01-01T00:02:30'),
|
|
}),
|
|
},
|
|
});
|
|
const breadcrumbAttachment2 = ReplayBreadcrumbFrameEventFixture({
|
|
timestamp: new Date('2024-01-01T00:03:05'),
|
|
data: {
|
|
payload: ReplayNavFrameFixture({
|
|
timestamp: new Date('2024-01-01T00:03:05'),
|
|
}),
|
|
},
|
|
});
|
|
const breadcrumbAttachment3 = ReplayBreadcrumbFrameEventFixture({
|
|
timestamp: new Date('2024-01-01T00:03:30'),
|
|
data: {
|
|
payload: ReplayNavFrameFixture({
|
|
timestamp: new Date('2024-01-01T00:03:30'),
|
|
}),
|
|
},
|
|
});
|
|
|
|
const error1 = ReplayErrorFixture({
|
|
id: '1',
|
|
issue: '100',
|
|
timestamp: '2024-01-01T00:02:30',
|
|
});
|
|
const error2 = ReplayErrorFixture({
|
|
id: '2',
|
|
issue: '200',
|
|
timestamp: '2024-01-01T00:03:06',
|
|
});
|
|
const error3 = ReplayErrorFixture({
|
|
id: '1',
|
|
issue: '100',
|
|
timestamp: '2024-01-01T00:03:30',
|
|
});
|
|
|
|
const replay = ReplayReader.factory({
|
|
attachments: [
|
|
rrwebFrame1,
|
|
rrwebFrame2,
|
|
rrwebFrame3,
|
|
breadcrumbAttachment1,
|
|
breadcrumbAttachment2,
|
|
breadcrumbAttachment3,
|
|
],
|
|
errors: [error1, error2, error3],
|
|
replayRecord: ReplayRecordFixture({
|
|
started_at: replayStartedAt,
|
|
finished_at: replayFinishedAt,
|
|
}),
|
|
clipWindow: {
|
|
startTimestampMs: clipStartTimestamp.getTime(),
|
|
endTimestampMs: clipEndTimestamp.getTime(),
|
|
},
|
|
});
|
|
|
|
it('should adjust the end time and duration for the clip window', () => {
|
|
// Duration should be between the clip start time and the last rrweb frame
|
|
// within the clip window
|
|
expect(replay?.getDurationMs()).toEqual(
|
|
rrwebFrame2.timestamp - clipStartTimestamp.getTime()
|
|
);
|
|
// Start offset should be set
|
|
expect(replay?.getStartOffsetMs()).toEqual(
|
|
clipStartTimestamp.getTime() - replayStartedAt.getTime()
|
|
);
|
|
expect(replay?.getStartTimestampMs()).toEqual(clipStartTimestamp.getTime());
|
|
});
|
|
|
|
it('should trim rrweb frames from the end but not the beginning', () => {
|
|
expect(replay?.getRRWebFrames()).toEqual([
|
|
expect.objectContaining({
|
|
type: EventType.Custom,
|
|
data: {tag: 'replay.start', payload: {}},
|
|
}),
|
|
expect.objectContaining({
|
|
type: EventType.FullSnapshot,
|
|
timestamp: rrwebFrame1.timestamp,
|
|
}),
|
|
expect.objectContaining({
|
|
type: EventType.FullSnapshot,
|
|
timestamp: rrwebFrame2.timestamp,
|
|
}),
|
|
// rrwebFrame3 should not be returned
|
|
]);
|
|
});
|
|
|
|
it('should only return chapter frames within window and shift their clipOffsets', () => {
|
|
expect(replay?.getChapterFrames()).toEqual([
|
|
// Only breadcrumb2 and error2 should be included
|
|
expect.objectContaining({
|
|
category: 'navigation',
|
|
timestampMs: breadcrumbAttachment2.timestamp,
|
|
// offset is relative to the start of the clip window
|
|
offsetMs: 5_000,
|
|
}),
|
|
expect.objectContaining({
|
|
category: 'issue',
|
|
timestampMs: new Date(error2.timestamp).getTime(),
|
|
offsetMs: 6_000,
|
|
}),
|
|
]);
|
|
});
|
|
});
|
|
});
|