123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354 |
- import {duration} from 'moment';
- import {initializeOrg} from 'sentry-test/initializeOrg';
- import {reactHooks} from 'sentry-test/reactTestingLibrary';
- import useReplayData from 'sentry/utils/replays/hooks/useReplayData';
- import useProjects from 'sentry/utils/useProjects';
- import type {ReplayRecord} from 'sentry/views/replays/types';
- jest.useFakeTimers();
- jest.mock('sentry/utils/useProjects');
- const {organization, project} = initializeOrg();
- const mockUseProjects = useProjects as jest.MockedFunction<typeof useProjects>;
- mockUseProjects.mockReturnValue({
- fetching: false,
- projects: [project],
- fetchError: null,
- hasMore: false,
- initiallyLoaded: true,
- onSearch: () => Promise.resolve(),
- placeholders: [],
- });
- function getMockReplayRecord(replayRecord?: Partial<ReplayRecord>) {
- const HYDRATED_REPLAY = TestStubs.ReplayRecord({
- ...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-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, {
- 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 = TestStubs.ReplaySegmentInit({timestamp: startedAt});
- const mockSegmentResponse2 = [
- ...TestStubs.ReplaySegmentConsole({timestamp: startedAt}),
- ...TestStubs.ReplaySegmentNavigation({timestamp: startedAt}),
- ];
- 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, {
- initialProps: {
- replayId: mockReplayResponse.id,
- orgSlug: organization.slug,
- segmentsPerPage: 1,
- },
- });
- jest.runAllTimers();
- 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 = [
- TestStubs.ReplayError({
- id: ERROR_IDS[0],
- issue: 'JAVASCRIPT-123E',
- timestamp: startedAt,
- }),
- ];
- const mockErrorResponse2 = [
- TestStubs.ReplayError({
- id: ERROR_IDS[1],
- issue: 'JAVASCRIPT-789Z',
- timestamp: startedAt,
- }),
- ];
- 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, {
- initialProps: {
- replayId: mockReplayResponse.id,
- orgSlug: organization.slug,
- errorsPerPage: 1,
- },
- });
- jest.runAllTimers();
- 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 = TestStubs.ReplaySegmentInit({timestamp: startedAt});
- const mockErrorResponse = [
- TestStubs.ReplayError({
- id: ERROR_ID,
- issue: 'JAVASCRIPT-123E',
- timestamp: startedAt,
- }),
- ];
- 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, {
- 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);
- jest.advanceTimersByTime(10);
- 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,
- })
- );
- jest.advanceTimersByTime(100);
- await waitForNextUpdate();
- // Next we see that some rrweb data has arrived
- expect(result.current).toStrictEqual(
- expect.objectContaining({
- attachments: mockSegmentResponse,
- errors: [],
- replayRecord: expectedReplay,
- })
- );
- jest.advanceTimersByTime(250);
- await waitForNextUpdate();
- // Finally we see fetching is complete, errors are here too
- expect(result.current).toStrictEqual(
- expect.objectContaining({
- attachments: mockSegmentResponse,
- errors: mockErrorResponse,
- replayRecord: expectedReplay,
- })
- );
- });
- });
|