useReplayData.spec.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. import {duration} from 'moment';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {reactHooks} from 'sentry-test/reactTestingLibrary';
  4. import useReplayData from 'sentry/utils/replays/hooks/useReplayData';
  5. import useProjects from 'sentry/utils/useProjects';
  6. import type {ReplayRecord} from 'sentry/views/replays/types';
  7. jest.useFakeTimers();
  8. jest.mock('sentry/utils/useProjects');
  9. const {organization, project} = initializeOrg();
  10. const mockUseProjects = useProjects as jest.MockedFunction<typeof useProjects>;
  11. mockUseProjects.mockReturnValue({
  12. fetching: false,
  13. projects: [project],
  14. fetchError: null,
  15. hasMore: false,
  16. initiallyLoaded: true,
  17. onSearch: () => Promise.resolve(),
  18. placeholders: [],
  19. });
  20. function getMockReplayRecord(replayRecord?: Partial<ReplayRecord>) {
  21. const HYDRATED_REPLAY = TestStubs.ReplayRecord({
  22. ...replayRecord,
  23. project_id: project.id,
  24. });
  25. const RAW_REPLAY = {
  26. ...HYDRATED_REPLAY,
  27. duration: HYDRATED_REPLAY.duration.asSeconds(),
  28. started_at: HYDRATED_REPLAY.started_at.toString(),
  29. finished_at: HYDRATED_REPLAY.finished_at.toString(),
  30. };
  31. return {
  32. mockReplayResponse: RAW_REPLAY,
  33. expectedReplay: HYDRATED_REPLAY,
  34. };
  35. }
  36. describe('useReplayData', () => {
  37. beforeEach(() => {
  38. MockApiClient.clearMockResponses();
  39. });
  40. it('should hydrate the replayRecord', async () => {
  41. const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
  42. count_errors: 0,
  43. count_segments: 0,
  44. error_ids: [],
  45. });
  46. MockApiClient.addMockResponse({
  47. url: `/organizations/${organization.slug}/replays-events-meta/`,
  48. body: {
  49. data: [],
  50. },
  51. headers: {
  52. Link: [
  53. '<http://localhost/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:1:0"',
  54. '<http://localhost/?cursor=0:2:0>; rel="next"; results="false"; cursor="0:1:0"',
  55. ].join(','),
  56. },
  57. });
  58. MockApiClient.addMockResponse({
  59. url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
  60. body: {data: mockReplayResponse},
  61. });
  62. const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
  63. initialProps: {
  64. replayId: mockReplayResponse.id,
  65. orgSlug: organization.slug,
  66. },
  67. });
  68. await waitForNextUpdate();
  69. expect(result.current).toEqual({
  70. attachments: expect.any(Array),
  71. errors: expect.any(Array),
  72. fetchError: undefined,
  73. fetching: false,
  74. onRetry: expect.any(Function),
  75. projectSlug: project.slug,
  76. replayRecord: expectedReplay,
  77. });
  78. });
  79. it('should concat N segment responses and pass them into ReplayReader', async () => {
  80. const startedAt = new Date('12:00:00 01-01-2023');
  81. const finishedAt = new Date('12:00:10 01-01-2023');
  82. const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
  83. started_at: startedAt,
  84. finished_at: finishedAt,
  85. duration: duration(10, 'seconds'),
  86. count_errors: 0,
  87. count_segments: 2,
  88. error_ids: [],
  89. });
  90. MockApiClient.addMockResponse({
  91. url: `/organizations/${organization.slug}/replays-events-meta/`,
  92. body: {
  93. data: [],
  94. },
  95. headers: {
  96. Link: [
  97. '<http://localhost/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:1:0"',
  98. '<http://localhost/?cursor=0:2:0>; rel="next"; results="false"; cursor="0:1:0"',
  99. ].join(','),
  100. },
  101. });
  102. const mockSegmentResponse1 = TestStubs.ReplaySegmentInit({timestamp: startedAt});
  103. const mockSegmentResponse2 = [
  104. ...TestStubs.ReplaySegmentConsole({timestamp: startedAt}),
  105. ...TestStubs.ReplaySegmentNavigation({timestamp: startedAt}),
  106. ];
  107. MockApiClient.addMockResponse({
  108. url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
  109. body: {data: mockReplayResponse},
  110. });
  111. const mockedSegmentsCall1 = MockApiClient.addMockResponse({
  112. url: `/projects/${organization.slug}/${project.slug}/replays/${mockReplayResponse.id}/recording-segments/`,
  113. body: mockSegmentResponse1,
  114. match: [(_url, options) => options.query?.cursor === '0:0:0'],
  115. });
  116. const mockedSegmentsCall2 = MockApiClient.addMockResponse({
  117. url: `/projects/${organization.slug}/${project.slug}/replays/${mockReplayResponse.id}/recording-segments/`,
  118. body: mockSegmentResponse2,
  119. match: [(_url, options) => options.query?.cursor === '0:1:0'],
  120. });
  121. const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
  122. initialProps: {
  123. replayId: mockReplayResponse.id,
  124. orgSlug: organization.slug,
  125. segmentsPerPage: 1,
  126. },
  127. });
  128. jest.runAllTimers();
  129. await waitForNextUpdate();
  130. expect(mockedSegmentsCall1).toHaveBeenCalledTimes(1);
  131. expect(mockedSegmentsCall2).toHaveBeenCalledTimes(1);
  132. expect(result.current).toStrictEqual(
  133. expect.objectContaining({
  134. attachments: [...mockSegmentResponse1, ...mockSegmentResponse2],
  135. errors: [],
  136. replayRecord: expectedReplay,
  137. })
  138. );
  139. });
  140. it('should concat N error responses and pass them through to Replay Reader', async () => {
  141. const ERROR_IDS = [
  142. '5c83aaccfffb4a708ae893bad9be3a1c',
  143. '6d94aaccfffb4a708ae893bad9be3a1c',
  144. ];
  145. const startedAt = new Date('12:00:00 01-01-2023');
  146. const finishedAt = new Date('12:00:10 01-01-2023');
  147. const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
  148. started_at: startedAt,
  149. finished_at: finishedAt,
  150. duration: duration(10, 'seconds'),
  151. count_errors: 2,
  152. count_segments: 0,
  153. error_ids: ERROR_IDS,
  154. });
  155. const mockErrorResponse1 = [
  156. TestStubs.ReplayError({
  157. id: ERROR_IDS[0],
  158. issue: 'JAVASCRIPT-123E',
  159. timestamp: startedAt,
  160. }),
  161. ];
  162. const mockErrorResponse2 = [
  163. TestStubs.ReplayError({
  164. id: ERROR_IDS[1],
  165. issue: 'JAVASCRIPT-789Z',
  166. timestamp: startedAt,
  167. }),
  168. ];
  169. MockApiClient.addMockResponse({
  170. url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
  171. body: {data: mockReplayResponse},
  172. });
  173. const mockedErrorsCall1 = MockApiClient.addMockResponse({
  174. url: `/organizations/${organization.slug}/replays-events-meta/`,
  175. body: {data: mockErrorResponse1},
  176. headers: {
  177. Link: [
  178. '<http://localhost/?cursor=0:0:1>; rel="previous"; results="false"; cursor="0:0:1"',
  179. '<http://localhost/?cursor=0:2:0>; rel="next"; results="true"; cursor="0:1:0"',
  180. ].join(','),
  181. },
  182. match: [
  183. (_url, options) => options.query?.query === `replayId:[${mockReplayResponse.id}]`,
  184. (_url, options) => options.query?.cursor === '0:0:0',
  185. ],
  186. });
  187. const mockedErrorsCall2 = MockApiClient.addMockResponse({
  188. url: `/organizations/${organization.slug}/replays-events-meta/`,
  189. body: {data: mockErrorResponse2},
  190. headers: {
  191. Link: [
  192. '<http://localhost/?cursor=0:0:1>; rel="previous"; results="true"; cursor="0:1:0"',
  193. '<http://localhost/?cursor=0:2:0>; rel="next"; results="false"; cursor="0:2:0"',
  194. ].join(','),
  195. },
  196. match: [
  197. (_url, options) => options.query?.query === `replayId:[${mockReplayResponse.id}]`,
  198. (_url, options) => options.query?.cursor === '0:1:0',
  199. ],
  200. });
  201. const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
  202. initialProps: {
  203. replayId: mockReplayResponse.id,
  204. orgSlug: organization.slug,
  205. errorsPerPage: 1,
  206. },
  207. });
  208. jest.runAllTimers();
  209. await waitForNextUpdate();
  210. expect(mockedErrorsCall1).toHaveBeenCalledTimes(1);
  211. expect(mockedErrorsCall2).toHaveBeenCalledTimes(1);
  212. expect(result.current).toStrictEqual(
  213. expect.objectContaining({
  214. attachments: [],
  215. errors: [...mockErrorResponse1, ...mockErrorResponse2],
  216. replayRecord: expectedReplay,
  217. })
  218. );
  219. });
  220. it('should incrementally load attachments and errors', async () => {
  221. const ERROR_ID = '5c83aaccfffb4a708ae893bad9be3a1c';
  222. const startedAt = new Date('12:00:00 01-01-2023');
  223. const finishedAt = new Date('12:00:10 01-01-2023');
  224. const {mockReplayResponse, expectedReplay} = getMockReplayRecord({
  225. started_at: startedAt,
  226. finished_at: finishedAt,
  227. duration: duration(10, 'seconds'),
  228. count_errors: 1,
  229. count_segments: 1,
  230. error_ids: [ERROR_ID],
  231. });
  232. const mockSegmentResponse = TestStubs.ReplaySegmentInit({timestamp: startedAt});
  233. const mockErrorResponse = [
  234. TestStubs.ReplayError({
  235. id: ERROR_ID,
  236. issue: 'JAVASCRIPT-123E',
  237. timestamp: startedAt,
  238. }),
  239. ];
  240. const mockedReplayCall = MockApiClient.addMockResponse({
  241. asyncDelay: 1,
  242. url: `/organizations/${organization.slug}/replays/${mockReplayResponse.id}/`,
  243. body: {data: mockReplayResponse},
  244. });
  245. const mockedSegmentsCall = MockApiClient.addMockResponse({
  246. asyncDelay: 100, // Simulate 100ms response time
  247. url: `/projects/${organization.slug}/${project.slug}/replays/${mockReplayResponse.id}/recording-segments/`,
  248. body: mockSegmentResponse,
  249. });
  250. const mockedEventsMetaCall = MockApiClient.addMockResponse({
  251. asyncDelay: 250, // Simulate 250ms response time
  252. url: `/organizations/${organization.slug}/replays-events-meta/`,
  253. body: {data: mockErrorResponse},
  254. });
  255. const {result, waitForNextUpdate} = reactHooks.renderHook(useReplayData, {
  256. initialProps: {
  257. replayId: mockReplayResponse.id,
  258. orgSlug: organization.slug,
  259. },
  260. });
  261. const expectedReplayData = {
  262. attachments: [],
  263. errors: [],
  264. fetchError: undefined,
  265. fetching: true,
  266. onRetry: expect.any(Function),
  267. projectSlug: null,
  268. replayRecord: undefined,
  269. } as Record<string, unknown>;
  270. // Immediately we will see the replay call is made
  271. expect(mockedReplayCall).toHaveBeenCalledTimes(1);
  272. expect(mockedEventsMetaCall).not.toHaveBeenCalledTimes(1);
  273. expect(mockedSegmentsCall).not.toHaveBeenCalledTimes(1);
  274. expect(result.current).toEqual(expectedReplayData);
  275. jest.advanceTimersByTime(10);
  276. await waitForNextUpdate();
  277. // Afterwards we see the attachments & errors requests are made
  278. expect(mockedReplayCall).toHaveBeenCalledTimes(1);
  279. expect(mockedEventsMetaCall).toHaveBeenCalledTimes(1);
  280. expect(mockedSegmentsCall).toHaveBeenCalledTimes(1);
  281. expect(result.current).toStrictEqual(
  282. expect.objectContaining({
  283. attachments: [],
  284. errors: [],
  285. projectSlug: project.slug,
  286. replayRecord: expectedReplay,
  287. })
  288. );
  289. jest.advanceTimersByTime(100);
  290. await waitForNextUpdate();
  291. // Next we see that some rrweb data has arrived
  292. expect(result.current).toStrictEqual(
  293. expect.objectContaining({
  294. attachments: mockSegmentResponse,
  295. errors: [],
  296. replayRecord: expectedReplay,
  297. })
  298. );
  299. jest.advanceTimersByTime(250);
  300. await waitForNextUpdate();
  301. // Finally we see fetching is complete, errors are here too
  302. expect(result.current).toStrictEqual(
  303. expect.objectContaining({
  304. attachments: mockSegmentResponse,
  305. errors: mockErrorResponse,
  306. replayRecord: expectedReplay,
  307. })
  308. );
  309. });
  310. });