Files
2024-02-15 17:01:50 +00:00

379 lines
12 KiB
TypeScript

import type {ReactNode} from 'react';
import {duration} from 'moment';
import {
ReplayConsoleEventFixture,
ReplayNavigateEventFixture,
} from 'sentry-fixture/replay/helpers';
import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb';
import {ReplayErrorFixture} from 'sentry-fixture/replayError';
import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {makeTestQueryClient} from 'sentry-test/queryClient';
import {reactHooks} from 'sentry-test/reactTestingLibrary';
import {QueryClientProvider} from 'sentry/utils/queryClient';
import useReplayData from 'sentry/utils/replays/hooks/useReplayData';
import useProjects from 'sentry/utils/useProjects';
import type {ReplayRecord} from 'sentry/views/replays/types';
jest.mock('sentry/utils/useProjects');
const {organization, project} = initializeOrg();
jest.mocked(useProjects).mockReturnValue({
fetching: false,
projects: [project],
fetchError: null,
hasMore: false,
initiallyLoaded: true,
onSearch: () => Promise.resolve(),
placeholders: [],
});
function wrapper({children}: {children?: ReactNode}) {
return (
<QueryClientProvider client={makeTestQueryClient()}>{children}</QueryClientProvider>
);
}
function getMockReplayRecord(replayRecord?: Partial<ReplayRecord>) {
const HYDRATED_REPLAY = ReplayRecordFixture({
...replayRecord,
project_id: project.id,
});
const RAW_REPLAY = {
...HYDRATED_REPLAY,
duration: HYDRATED_REPLAY.duration.asSeconds(),
started_at: HYDRATED_REPLAY.started_at.toString(),
finished_at: HYDRATED_REPLAY.finished_at.toString(),
};
return {
mockReplayResponse: RAW_REPLAY,
expectedReplay: HYDRATED_REPLAY,
};
}
describe('useReplayData', () => {
beforeEach(() => {
MockApiClient.clearMockResponses();
});
it('should hydrate the replayRecord', async () => {
const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
count_errors: 0,
count_segments: 0,
error_ids: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
body: {data: mockReplayResponse},
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays-events-meta/`,
body: {
data: [],
},
headers: {
Link: [
'<http://localhost/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:1:0"',
'<http://localhost/?cursor=0:2:0>; rel="next"; results="false"; cursor="0:1:0"',
].join(','),
},
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
body: {data: mockReplayResponse},
});
const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
wrapper,
initialProps: {
replayId: mockReplayResponse.id,
orgSlug: organization.slug,
},
});
await waitForNextUpdate();
expect(result.current).toEqual({
attachments: expect.any(Array),
errors: expect.any(Array),
fetchError: undefined,
fetching: false,
onRetry: expect.any(Function),
projectSlug: project.slug,
replayRecord: expectedReplay,
});
});
it('should concat N segment responses and pass them into ReplayReader', async () => {
const startedAt = new Date('12:00:00 01-01-2023');
const finishedAt = new Date('12:00:10 01-01-2023');
const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
started_at: startedAt,
finished_at: finishedAt,
duration: duration(10, 'seconds'),
count_errors: 0,
count_segments: 2,
error_ids: [],
});
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays-events-meta/`,
body: {
data: [],
},
headers: {
Link: [
'<http://localhost/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:1:0"',
'<http://localhost/?cursor=0:2:0>; rel="next"; results="false"; cursor="0:1:0"',
].join(','),
},
});
const mockSegmentResponse1 = RRWebInitFrameEventsFixture({
timestamp: startedAt,
});
const mockSegmentResponse2 = [
ReplayConsoleEventFixture({timestamp: startedAt}),
ReplayNavigateEventFixture({
startTimestamp: startedAt,
endTimestamp: finishedAt,
}),
];
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
body: {data: mockReplayResponse},
});
const mockedSegmentsCall1 = MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/replays/${mockReplayResponse.id}/recording-segments/`,
body: mockSegmentResponse1,
match: [(_url, options) => options.query?.cursor === '0:0:0'],
});
const mockedSegmentsCall2 = MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.slug}/replays/${mockReplayResponse.id}/recording-segments/`,
body: mockSegmentResponse2,
match: [(_url, options) => options.query?.cursor === '0:1:0'],
});
const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
wrapper,
initialProps: {
replayId: mockReplayResponse.id,
orgSlug: organization.slug,
segmentsPerPage: 1,
},
});
await waitForNextUpdate();
expect(mockedSegmentsCall1).toHaveBeenCalledTimes(1);
expect(mockedSegmentsCall2).toHaveBeenCalledTimes(1);
expect(result.current).toStrictEqual(
expect.objectContaining({
attachments: [...mockSegmentResponse1, ...mockSegmentResponse2],
errors: [],
replayRecord: expectedReplay,
})
);
});
it('should concat N error responses and pass them through to Replay Reader', async () => {
const ERROR_IDS = [
'5c83aaccfffb4a708ae893bad9be3a1c',
'6d94aaccfffb4a708ae893bad9be3a1c',
];
const startedAt = new Date('12:00:00 01-01-2023');
const finishedAt = new Date('12:00:10 01-01-2023');
const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
started_at: startedAt,
finished_at: finishedAt,
duration: duration(10, 'seconds'),
count_errors: 2,
count_segments: 0,
error_ids: ERROR_IDS,
});
const mockErrorResponse1 = [
ReplayErrorFixture({
id: ERROR_IDS[0],
issue: 'JAVASCRIPT-123E',
timestamp: startedAt.toISOString(),
}),
];
const mockErrorResponse2 = [
ReplayErrorFixture({
id: ERROR_IDS[1],
issue: 'JAVASCRIPT-789Z',
timestamp: startedAt.toISOString(),
}),
];
MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
body: {data: mockReplayResponse},
});
const mockedErrorsCall1 = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays-events-meta/`,
body: {data: mockErrorResponse1},
headers: {
Link: [
'<http://localhost/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1"',
'<http://localhost/?cursor=0:2:0>; rel="next"; results="true"; cursor="0:1:0"',
].join(','),
},
match: [
(_url, options) => options.query?.query === `replayId:[${mockReplayResponse.id}]`,
(_url, options) => options.query?.cursor === '0:0:0',
],
});
const mockedErrorsCall2 = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/replays-events-meta/`,
body: {data: mockErrorResponse2},
headers: {
Link: [
'<http://localhost/?cursor=0:0:1>; rel="previous"; results="true"; cursor="0:1:0"',
'<http://localhost/?cursor=0:2:0>; rel="next"; results="false"; cursor="0:2:0"',
].join(','),
},
match: [
(_url, options) => options.query?.query === `replayId:[${mockReplayResponse.id}]`,
(_url, options) => options.query?.cursor === '0:1:0',
],
});
const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
wrapper,
initialProps: {
replayId: mockReplayResponse.id,
orgSlug: organization.slug,
errorsPerPage: 1,
},
});
await waitForNextUpdate();
expect(mockedErrorsCall1).toHaveBeenCalledTimes(1);
expect(mockedErrorsCall2).toHaveBeenCalledTimes(1);
expect(result.current).toStrictEqual(
expect.objectContaining({
attachments: [],
errors: [...mockErrorResponse1, ...mockErrorResponse2],
replayRecord: expectedReplay,
})
);
});
it('should incrementally load attachments and errors', async () => {
const ERROR_ID = '5c83aaccfffb4a708ae893bad9be3a1c';
const startedAt = new Date('12:00:00 01-01-2023');
const finishedAt = new Date('12:00:10 01-01-2023');
const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
started_at: startedAt,
finished_at: finishedAt,
duration: duration(10, 'seconds'),
count_errors: 1,
count_segments: 1,
error_ids: [ERROR_ID],
});
const mockSegmentResponse = RRWebInitFrameEventsFixture({
timestamp: startedAt,
});
const mockErrorResponse = [
ReplayErrorFixture({
id: ERROR_ID,
issue: 'JAVASCRIPT-123E',
timestamp: startedAt.toISOString(),
}),
];
const mockedReplayCall = MockApiClient.addMockResponse({
asyncDelay: 1,
url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
body: {data: mockReplayResponse},
});
const mockedSegmentsCall = MockApiClient.addMockResponse({
asyncDelay: 100, // Simulate 100ms response time
url: `/projects/${organization.slug}/${project.slug}/replays/${mockReplayResponse.id}/recording-segments/`,
body: mockSegmentResponse,
});
const mockedEventsMetaCall = MockApiClient.addMockResponse({
asyncDelay: 250, // Simulate 250ms response time
url: `/organizations/${organization.slug}/replays-events-meta/`,
body: {data: mockErrorResponse},
});
const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
wrapper,
initialProps: {
replayId: mockReplayResponse.id,
orgSlug: organization.slug,
},
});
const expectedReplayData = {
attachments: [],
errors: [],
fetchError: undefined,
fetching: true,
onRetry: expect.any(Function),
projectSlug: null,
replayRecord: undefined,
} as Record<string, unknown>;
// Immediately we will see the replay call is made
expect(mockedReplayCall).toHaveBeenCalledTimes(1);
expect(mockedEventsMetaCall).not.toHaveBeenCalledTimes(1);
expect(mockedSegmentsCall).not.toHaveBeenCalledTimes(1);
expect(result.current).toEqual(expectedReplayData);
await waitForNextUpdate();
// Afterwards we see the attachments & errors requests are made
expect(mockedReplayCall).toHaveBeenCalledTimes(1);
expect(mockedEventsMetaCall).toHaveBeenCalledTimes(1);
expect(mockedSegmentsCall).toHaveBeenCalledTimes(1);
expect(result.current).toStrictEqual(
expect.objectContaining({
attachments: [],
errors: [],
projectSlug: project.slug,
replayRecord: expectedReplay,
})
);
await waitForNextUpdate();
// Next we see that some rrweb data has arrived
expect(result.current).toStrictEqual(
expect.objectContaining({
attachments: mockSegmentResponse,
errors: [],
replayRecord: expectedReplay,
})
);
await waitForNextUpdate();
// Finally we see fetching is complete, errors are here too
expect(result.current).toStrictEqual(
expect.objectContaining({
attachments: mockSegmentResponse,
errors: mockErrorResponse,
replayRecord: expectedReplay,
})
);
});
});