Skip to content

Commit 321ba82

Browse files
authored
test(replay): Create test stubs for all replay frames and frame-data (#50246)
1 parent fa0a76d commit 321ba82

File tree

10 files changed

+821
-0
lines changed

10 files changed

+821
-0
lines changed

fixtures/js-stubs/replay.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as BreadcrumbFrameData from './replay/replayBreadcrumbFrameData';
2+
import * as ReplayFrameEvents from './replay/replayFrameEvents';
3+
import * as ReplaySpanFrameData from './replay/replaySpanFrameData';
4+
5+
export const Replay = {
6+
...BreadcrumbFrameData,
7+
...ReplayFrameEvents,
8+
...ReplaySpanFrameData,
9+
};
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import {BreadcrumbType} from 'sentry/types/breadcrumbs';
2+
import {BreadcrumbFrame as TBreadcrumbFrame} from 'sentry/utils/replays/types';
3+
4+
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
5+
6+
type TestableFrame<Cat extends TBreadcrumbFrame['category']> = Overwrite<
7+
Partial<Extract<TBreadcrumbFrame, {category: Cat}>>,
8+
{timestamp: Date}
9+
>;
10+
11+
type MockFrame<Cat extends TBreadcrumbFrame['category']> = Extract<
12+
TBreadcrumbFrame,
13+
{category: Cat}
14+
>;
15+
16+
export function ConsoleFrame(fields: TestableFrame<'console'>): MockFrame<'console'> {
17+
return {
18+
category: 'console',
19+
data: fields.data ?? {
20+
logger: 'unknown',
21+
},
22+
level: fields.level ?? 'fatal',
23+
message: fields.message ?? '',
24+
timestamp: fields.timestamp.getTime() / 1000,
25+
type: BreadcrumbType.DEBUG,
26+
};
27+
}
28+
29+
export function ClickFrame(fields: TestableFrame<'ui.click'>): MockFrame<'ui.click'> {
30+
return {
31+
category: 'ui.click',
32+
data: fields.data ?? {},
33+
message: fields.message ?? '',
34+
timestamp: fields.timestamp.getTime() / 1000,
35+
type: BreadcrumbType.DEFAULT,
36+
};
37+
}
38+
39+
export function InputFrame(fields: TestableFrame<'ui.input'>): MockFrame<'ui.input'> {
40+
return {
41+
category: 'ui.input',
42+
message: fields.message ?? '',
43+
timestamp: fields.timestamp.getTime() / 1000,
44+
type: BreadcrumbType.DEFAULT,
45+
};
46+
}
47+
48+
export function KeyboardEventFrame(
49+
fields: TestableFrame<'ui.keyDown'>
50+
): MockFrame<'ui.keyDown'> {
51+
return {
52+
category: 'ui.keyDown',
53+
data: fields.data ?? {
54+
altKey: false,
55+
ctrlKey: false,
56+
key: 'A',
57+
metaKey: false,
58+
shiftKey: false,
59+
},
60+
message: fields.message,
61+
timestamp: fields.timestamp.getTime() / 1000,
62+
type: BreadcrumbType.DEFAULT,
63+
};
64+
}
65+
66+
export function BlurFrame(fields: TestableFrame<'ui.blur'>): MockFrame<'ui.blur'> {
67+
return {
68+
category: 'ui.blur',
69+
message: fields.message,
70+
timestamp: fields.timestamp.getTime() / 1000,
71+
type: BreadcrumbType.DEFAULT,
72+
};
73+
}
74+
75+
export function FocusFrame(fields: TestableFrame<'ui.focus'>): MockFrame<'ui.focus'> {
76+
return {
77+
category: 'ui.focus',
78+
message: fields.message,
79+
timestamp: fields.timestamp.getTime() / 1000,
80+
type: BreadcrumbType.DEFAULT,
81+
};
82+
}
83+
84+
export function SlowClickFrame(
85+
fields: TestableFrame<'ui.slowClickDetected'>
86+
): MockFrame<'ui.slowClickDetected'> {
87+
return {
88+
category: 'ui.slowClickDetected',
89+
data: fields.data ?? {
90+
endReason: '',
91+
timeAfterClickFs: 5,
92+
url: '/',
93+
},
94+
message: fields.message,
95+
timestamp: fields.timestamp.getTime() / 1000,
96+
type: BreadcrumbType.DEFAULT,
97+
};
98+
}
99+
100+
export function MutationFrame(
101+
fields: TestableFrame<'replay.mutations'>
102+
): MockFrame<'replay.mutations'> {
103+
return {
104+
category: 'replay.mutations',
105+
data: fields.data ?? {
106+
count: 1100,
107+
limit: true,
108+
},
109+
message: fields.message,
110+
timestamp: fields.timestamp.getTime() / 1000,
111+
type: '',
112+
};
113+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import type {
2+
BreadcrumbFrameEvent as TBreadcrumbFrameEvent,
3+
OptionFrameEvent as TOptionFrameEvent,
4+
SpanFrameEvent as TSpanFrameEvent,
5+
} from 'sentry/utils/replays/types';
6+
import {EventType} from 'sentry/utils/replays/types';
7+
8+
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
9+
10+
type TestableFrameEvent<
11+
FrameEvent extends TBreadcrumbFrameEvent | TSpanFrameEvent | TOptionFrameEvent
12+
> = Overwrite<
13+
Omit<FrameEvent, 'type'>,
14+
{
15+
data: Omit<FrameEvent['data'], 'tag'>;
16+
timestamp: Date;
17+
}
18+
>;
19+
20+
/**
21+
* `BreadcrumbFrameData` has factories to help construct the correct payloads.
22+
*
23+
* ```
24+
* BreadcrumbFrameEvent({
25+
* timestamp,
26+
* data: {
27+
* payload: TestStubs.BreadcrumbFrameData.FOO({}),
28+
* },
29+
* });
30+
* ```
31+
*/
32+
export function BreadcrumbFrameEvent(
33+
fields: TestableFrameEvent<TBreadcrumbFrameEvent>
34+
): TBreadcrumbFrameEvent {
35+
return {
36+
type: EventType.Custom,
37+
timestamp: fields.timestamp.getTime(), // frame timestamps are in ms
38+
data: {
39+
tag: 'breadcrumb',
40+
payload: fields.data.payload,
41+
metric: fields.data.metric,
42+
},
43+
};
44+
}
45+
46+
/**
47+
* `SpanFrame()` is a factories to help consturt valid payloads given an operation name.
48+
* `ReplaySpanFrameData.*` contains more factories to build the required inner dataset.
49+
*
50+
* ```
51+
* SpanFrameEvent({
52+
* timestamp,
53+
* data: {
54+
* payload: TestStubs.Replay.SpanFrame({
55+
* data: TestStubs.ReplaySpanFrameData.FOO({...})
56+
* }),
57+
* },
58+
* });
59+
* ```
60+
*/
61+
export function SpanFrameEvent(
62+
fields: TestableFrameEvent<TSpanFrameEvent>
63+
): TSpanFrameEvent {
64+
return {
65+
type: EventType.Custom,
66+
timestamp: fields.timestamp.getTime(), // frame timestamps are in ms
67+
data: {
68+
tag: 'performanceSpan',
69+
payload: fields.data.payload,
70+
},
71+
};
72+
}
73+
74+
export function OptionFrameEvent(
75+
fields: TestableFrameEvent<TOptionFrameEvent>
76+
): TOptionFrameEvent {
77+
return {
78+
type: EventType.Custom,
79+
timestamp: fields.timestamp.getTime(), // frame timestamps are in ms
80+
data: {
81+
tag: 'options',
82+
payload: fields.data.payload,
83+
},
84+
};
85+
}
86+
87+
export function OptionFrame(
88+
fields: Partial<TOptionFrameEvent['data']['payload']>
89+
): TOptionFrameEvent['data']['payload'] {
90+
return {
91+
blockAllMedia: false,
92+
errorSampleRate: 0,
93+
maskAllInputs: false,
94+
maskAllText: false,
95+
networkCaptureBodies: false,
96+
networkDetailHasUrls: false,
97+
networkRequestHasHeaders: false,
98+
networkResponseHasHeaders: false,
99+
sessionSampleRate: 0,
100+
useCompression: false,
101+
useCompressionOption: false,
102+
...fields,
103+
};
104+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import {SpanFrame as TSpanFrame} from 'sentry/utils/replays/types';
2+
3+
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
4+
5+
type TestableFrame<Op extends TSpanFrame['op']> = Overwrite<
6+
Partial<Extract<TSpanFrame, {op: Op}>>,
7+
{endTimestamp: Date; startTimestamp: Date}
8+
>;
9+
10+
type MockFrame<Op extends TSpanFrame['op']> = Extract<TSpanFrame, {op: Op}>;
11+
12+
function BaseFrame<T extends TSpanFrame['op']>(
13+
op: T,
14+
fields: TestableFrame<T>
15+
): MockFrame<T> {
16+
return {
17+
op,
18+
description: fields.description ?? '',
19+
startTimestamp: fields.startTimestamp.getTime() / 100,
20+
endTimestamp: fields.endTimestamp.getTime() / 100,
21+
data: fields.data,
22+
} as MockFrame<T>;
23+
}
24+
25+
export function LargestContentfulPaintFrame(
26+
fields: TestableFrame<'largest-contentful-paint'>
27+
): MockFrame<'largest-contentful-paint'> {
28+
return BaseFrame('largest-contentful-paint', {
29+
...fields,
30+
data: {
31+
nodeId: fields.data?.nodeId,
32+
size: fields.data?.size ?? 0,
33+
value: fields.data?.value ?? 0,
34+
},
35+
});
36+
}
37+
38+
export function MemoryFrame(fields: TestableFrame<'memory'>): MockFrame<'memory'> {
39+
return BaseFrame('memory', {
40+
...fields,
41+
data: {
42+
memory: {
43+
jsHeapSizeLimit: fields.data?.memory?.jsHeapSizeLimit ?? 0,
44+
totalJSHeapSize: fields.data?.memory?.totalJSHeapSize ?? 0,
45+
usedJSHeapSize: fields.data?.memory?.usedJSHeapSize ?? 0,
46+
},
47+
},
48+
});
49+
}
50+
51+
export function NavigationFrame(
52+
fields: TestableFrame<
53+
'navigation.navigate' | 'navigation.reload' | 'navigation.back_forward'
54+
>
55+
): MockFrame<'navigation.navigate' | 'navigation.reload' | 'navigation.back_forward'> {
56+
return BaseFrame(fields.op ?? 'navigation.navigate', {
57+
...fields,
58+
data: {
59+
decodedBodySize: fields.data?.decodedBodySize,
60+
domComplete: fields.data?.domComplete,
61+
domContentLoadedEventEnd: fields.data?.domContentLoadedEventEnd,
62+
domContentLoadedEventStart: fields.data?.domContentLoadedEventStart,
63+
domInteractive: fields.data?.domInteractive,
64+
duration: fields.data?.duration,
65+
encodedBodySize: fields.data?.encodedBodySize,
66+
loadEventEnd: fields.data?.loadEventEnd,
67+
loadEventStart: fields.data?.loadEventStart,
68+
redirectCount: fields.data?.redirectCount,
69+
size: fields.data?.size,
70+
},
71+
});
72+
}
73+
74+
export function NavigationPushFrame(
75+
fields: TestableFrame<'navigation.push'>
76+
): MockFrame<'navigation.push'> {
77+
return BaseFrame('navigation.push', {
78+
...fields,
79+
data: {
80+
previous: fields.data?.previous ?? '/',
81+
},
82+
});
83+
}
84+
85+
export function PaintFrame(fields: TestableFrame<'paint'>): MockFrame<'paint'> {
86+
return BaseFrame('paint', fields);
87+
}
88+
89+
export function RequestFrame(
90+
fields: TestableFrame<'resource.fetch' | 'resource.xhr'>
91+
): MockFrame<'resource.fetch' | 'resource.xhr'> {
92+
return BaseFrame(fields.op ?? 'resource.xhr', {
93+
...fields,
94+
data: {
95+
method: fields.data?.method,
96+
requestBodySize: fields.data?.requestBodySize,
97+
responseBodySize: fields.data?.responseBodySize,
98+
statusCode: fields.data?.statusCode,
99+
request: fields.data?.request,
100+
response: fields.data?.response,
101+
},
102+
});
103+
}
104+
105+
export function ResourceFrame(
106+
fields: TestableFrame<
107+
| 'resource.css'
108+
| 'resource.iframe'
109+
| 'resource.img'
110+
| 'resource.link'
111+
| 'resource.other'
112+
| 'resource.script'
113+
>
114+
): MockFrame<
115+
| 'resource.css'
116+
| 'resource.iframe'
117+
| 'resource.img'
118+
| 'resource.link'
119+
| 'resource.other'
120+
| 'resource.script'
121+
> {
122+
return BaseFrame(fields.op ?? 'resource.other', {
123+
...fields,
124+
data: {
125+
decodedBodySize: fields.data?.decodedBodySize ?? 0,
126+
encodedBodySize: fields.data?.encodedBodySize ?? 0,
127+
size: fields.data?.size ?? 0,
128+
statusCode: fields.data?.statusCode,
129+
},
130+
});
131+
}

fixtures/js-stubs/types.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import type {
55
ReplaySpan,
66
} from 'sentry/views/replays/types';
77

8+
import type {Replay} from './replay';
9+
810
type SimpleStub<T = any> = () => T;
911

1012
type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
@@ -113,6 +115,7 @@ type TestStubFixtures = {
113115
PublishedApps: SimpleStub;
114116
PullRequest: OverridableStub;
115117
Release: (params?: any, healthParams?: any) => any;
118+
Replay: typeof Replay;
116119
ReplayError: OverridableStub;
117120
ReplayList: OverridableStubList<ReplayListRecord>;
118121
ReplayRRWebDivHelloWorld: OverridableStub;

0 commit comments

Comments
 (0)