groupReplays.spec.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617
  1. import {GroupFixture} from 'sentry-fixture/group';
  2. import {ProjectFixture} from 'sentry-fixture/project';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import ProjectsStore from 'sentry/stores/projectsStore';
  6. import GroupReplays from 'sentry/views/issueDetails/groupReplays';
  7. jest.mock('sentry/utils/useMedia', () => ({
  8. __esModule: true,
  9. default: jest.fn(() => true),
  10. }));
  11. const mockReplayCountUrl = '/organizations/org-slug/replay-count/';
  12. const mockReplayUrl = '/organizations/org-slug/replays/';
  13. type InitializeOrgProps = {
  14. organizationProps?: {
  15. features?: string[];
  16. };
  17. };
  18. import {browserHistory} from 'react-router';
  19. import {duration} from 'moment';
  20. import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb';
  21. import {ReplayListFixture} from 'sentry-fixture/replayList';
  22. import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';
  23. import {resetMockDate, setMockDate} from 'sentry-test/utils';
  24. import useReplayReader from 'sentry/utils/replays/hooks/useReplayReader';
  25. import ReplayReader from 'sentry/utils/replays/replayReader';
  26. const REPLAY_ID_1 = '346789a703f6454384f1de473b8b9fcc';
  27. const REPLAY_ID_2 = 'b05dae9b6be54d21a4d5ad9f8f02b780';
  28. let router, organization, routerContext;
  29. jest.mock('sentry/utils/replays/hooks/useReplayReader');
  30. // Mock screenfull library
  31. jest.mock('screenfull', () => ({
  32. enabled: true,
  33. isFullscreen: false,
  34. request: jest.fn(),
  35. exit: jest.fn(),
  36. on: jest.fn(),
  37. off: jest.fn(),
  38. }));
  39. const mockUseReplayReader = jest.mocked(useReplayReader);
  40. const mockEventTimestamp = new Date('2022-09-22T16:59:41Z');
  41. const mockEventTimestampMs = mockEventTimestamp.getTime();
  42. // Get replay data with the mocked replay reader params
  43. const mockReplay = ReplayReader.factory({
  44. replayRecord: ReplayRecordFixture({
  45. browser: {
  46. name: 'Chrome',
  47. version: '110.0.0',
  48. },
  49. started_at: new Date('Sep 22, 2022 4:58:39 PM UTC'),
  50. finished_at: new Date(mockEventTimestampMs + 5_000),
  51. duration: duration(10, 'seconds'),
  52. }),
  53. errors: [],
  54. attachments: RRWebInitFrameEventsFixture({
  55. timestamp: new Date('Sep 22, 2022 4:58:39 PM UTC'),
  56. }),
  57. clipWindow: {
  58. startTimestampMs: mockEventTimestampMs - 5_000,
  59. endTimestampMs: mockEventTimestampMs + 5_000,
  60. },
  61. });
  62. mockUseReplayReader.mockImplementation(() => {
  63. return {
  64. attachments: [],
  65. errors: [],
  66. fetchError: undefined,
  67. fetching: false,
  68. onRetry: jest.fn(),
  69. projectSlug: ProjectFixture().slug,
  70. replay: mockReplay,
  71. replayId: REPLAY_ID_1,
  72. replayRecord: ReplayRecordFixture(),
  73. };
  74. });
  75. function init({organizationProps = {features: ['session-replay']}}: InitializeOrgProps) {
  76. const mockProject = ProjectFixture();
  77. ({router, organization, routerContext} = initializeOrg({
  78. organization: {
  79. ...organizationProps,
  80. },
  81. project: mockProject,
  82. projects: [mockProject],
  83. router: {
  84. routes: [
  85. {path: '/'},
  86. {path: '/organizations/:orgId/issues/:groupId/'},
  87. {path: 'replays/'},
  88. ],
  89. location: {
  90. pathname: '/organizations/org-slug/replays/',
  91. query: {},
  92. },
  93. },
  94. }));
  95. ProjectsStore.init();
  96. ProjectsStore.loadInitialData(organization.projects);
  97. return {router, organization, routerContext};
  98. }
  99. describe('GroupReplays', () => {
  100. beforeEach(() => {
  101. MockApiClient.clearMockResponses();
  102. MockApiClient.addMockResponse({
  103. method: 'GET',
  104. url: `/organizations/org-slug/sdk-updates/`,
  105. body: [],
  106. });
  107. });
  108. afterEach(() => {
  109. resetMockDate();
  110. });
  111. describe('Replay Feature Disabled', () => {
  112. const mockGroup = GroupFixture();
  113. it("should show a message when the organization doesn't have access to the replay feature", () => {
  114. ({router, organization, routerContext} = init({
  115. organizationProps: {features: []},
  116. }));
  117. render(<GroupReplays group={mockGroup} />, {
  118. context: routerContext,
  119. organization,
  120. router,
  121. });
  122. expect(
  123. screen.getByText("You don't have access to this feature")
  124. ).toBeInTheDocument();
  125. });
  126. });
  127. describe('Replay Feature Enabled', () => {
  128. beforeEach(() => {
  129. ({router, organization, routerContext} = init({}));
  130. });
  131. it('should query the replay-count endpoint with the fetched replayIds', async () => {
  132. const mockGroup = GroupFixture();
  133. const mockReplayCountApi = MockApiClient.addMockResponse({
  134. url: mockReplayCountUrl,
  135. body: {
  136. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  137. },
  138. });
  139. const mockReplayApi = MockApiClient.addMockResponse({
  140. url: mockReplayUrl,
  141. body: {
  142. data: [],
  143. },
  144. });
  145. render(<GroupReplays group={mockGroup} />, {
  146. context: routerContext,
  147. organization,
  148. router,
  149. });
  150. await waitFor(() => {
  151. expect(mockReplayCountApi).toHaveBeenCalledWith(
  152. mockReplayCountUrl,
  153. expect.objectContaining({
  154. query: {
  155. returnIds: true,
  156. data_source: 'discover',
  157. query: `issue.id:[${mockGroup.id}]`,
  158. statsPeriod: '14d',
  159. project: -1,
  160. },
  161. })
  162. );
  163. // Expect api path to have the correct query params
  164. expect(mockReplayApi).toHaveBeenCalledWith(
  165. mockReplayUrl,
  166. expect.objectContaining({
  167. query: expect.objectContaining({
  168. environment: [],
  169. field: [
  170. 'activity',
  171. 'browser',
  172. 'count_dead_clicks',
  173. 'count_errors',
  174. 'count_rage_clicks',
  175. 'duration',
  176. 'finished_at',
  177. 'id',
  178. 'is_archived',
  179. 'os',
  180. 'project_id',
  181. 'started_at',
  182. 'user',
  183. ],
  184. per_page: 50,
  185. project: -1,
  186. queryReferrer: 'issueReplays',
  187. query: `id:[${REPLAY_ID_1},${REPLAY_ID_2}]`,
  188. sort: '-started_at',
  189. statsPeriod: '14d',
  190. }),
  191. })
  192. );
  193. });
  194. });
  195. it('should show empty message when no replays are found', async () => {
  196. const mockGroup = GroupFixture();
  197. const mockReplayCountApi = MockApiClient.addMockResponse({
  198. url: mockReplayCountUrl,
  199. body: {
  200. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  201. },
  202. });
  203. const mockReplayApi = MockApiClient.addMockResponse({
  204. url: mockReplayUrl,
  205. body: {
  206. data: [],
  207. },
  208. });
  209. render(<GroupReplays group={mockGroup} />, {
  210. context: routerContext,
  211. organization,
  212. router,
  213. });
  214. expect(
  215. await screen.findByText('There are no items to display')
  216. ).toBeInTheDocument();
  217. expect(mockReplayCountApi).toHaveBeenCalled();
  218. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  219. });
  220. it('should display error message when api call fails', async () => {
  221. const mockGroup = GroupFixture();
  222. const mockReplayCountApi = MockApiClient.addMockResponse({
  223. url: mockReplayCountUrl,
  224. body: {
  225. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  226. },
  227. });
  228. const mockReplayApi = MockApiClient.addMockResponse({
  229. url: mockReplayUrl,
  230. statusCode: 500,
  231. body: {
  232. detail: 'Invalid number: asdf. Expected number.',
  233. },
  234. });
  235. render(<GroupReplays group={mockGroup} />, {
  236. context: routerContext,
  237. organization,
  238. router,
  239. });
  240. expect(
  241. await screen.findByText('Invalid number: asdf. Expected number.')
  242. ).toBeInTheDocument();
  243. await waitFor(() => {
  244. expect(mockReplayCountApi).toHaveBeenCalled();
  245. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  246. });
  247. });
  248. it('should display default error message when api call fails without a body', async () => {
  249. const mockGroup = GroupFixture();
  250. const mockReplayCountApi = MockApiClient.addMockResponse({
  251. url: mockReplayCountUrl,
  252. body: {
  253. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  254. },
  255. });
  256. const mockReplayApi = MockApiClient.addMockResponse({
  257. url: mockReplayUrl,
  258. statusCode: 500,
  259. body: {},
  260. });
  261. render(<GroupReplays group={mockGroup} />, {
  262. context: routerContext,
  263. organization,
  264. router,
  265. });
  266. expect(
  267. await screen.findByText(
  268. 'Sorry, the list of replays could not be loaded. This could be due to invalid search parameters or an internal systems error.'
  269. )
  270. ).toBeInTheDocument();
  271. await waitFor(() => {
  272. expect(mockReplayCountApi).toHaveBeenCalled();
  273. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  274. });
  275. });
  276. it('should show loading indicator when loading replays', async () => {
  277. const mockGroup = GroupFixture();
  278. const mockReplayCountApi = MockApiClient.addMockResponse({
  279. url: mockReplayCountUrl,
  280. body: {
  281. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  282. },
  283. });
  284. const mockReplayApi = MockApiClient.addMockResponse({
  285. url: mockReplayUrl,
  286. statusCode: 200,
  287. body: {
  288. data: [],
  289. },
  290. });
  291. render(<GroupReplays group={mockGroup} />, {
  292. context: routerContext,
  293. organization,
  294. router,
  295. });
  296. expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
  297. await waitFor(() => {
  298. expect(mockReplayCountApi).toHaveBeenCalled();
  299. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  300. });
  301. });
  302. it('should show a list of replays and have the correct values', async () => {
  303. const mockGroup = GroupFixture();
  304. const mockReplayCountApi = MockApiClient.addMockResponse({
  305. url: mockReplayCountUrl,
  306. body: {
  307. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  308. },
  309. });
  310. const mockReplayApi = MockApiClient.addMockResponse({
  311. url: mockReplayUrl,
  312. statusCode: 200,
  313. body: {
  314. data: [
  315. {
  316. ...ReplayListFixture()[0],
  317. count_errors: 1,
  318. duration: 52346,
  319. finished_at: new Date('2022-09-15T06:54:00+00:00'),
  320. id: '346789a703f6454384f1de473b8b9fcc',
  321. started_at: new Date('2022-09-15T06:50:00+00:00'),
  322. urls: [
  323. 'https://dev.getsentry.net:7999/replays/',
  324. '/organizations/org-slug/replays/?project=2',
  325. ],
  326. },
  327. {
  328. ...ReplayListFixture()[0],
  329. count_errors: 4,
  330. duration: 400,
  331. finished_at: new Date('2022-09-21T21:40:38+00:00'),
  332. id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
  333. started_at: new Date('2022-09-21T21:30:44+00:00'),
  334. urls: [
  335. 'https://dev.getsentry.net:7999/organizations/org-slug/replays/?project=2&statsPeriod=24h',
  336. '/organizations/org-slug/issues/',
  337. '/organizations/org-slug/issues/?project=2',
  338. ],
  339. },
  340. ].map(hydrated => ({
  341. ...hydrated,
  342. started_at: hydrated.started_at.toString(),
  343. finished_at: hydrated.finished_at.toString(),
  344. })),
  345. },
  346. });
  347. // Mock the system date to be 2022-09-28
  348. setMockDate(new Date('Sep 28, 2022 11:29:13 PM UTC'));
  349. render(<GroupReplays group={mockGroup} />, {
  350. context: routerContext,
  351. organization,
  352. router,
  353. });
  354. await waitFor(() => {
  355. expect(mockReplayCountApi).toHaveBeenCalled();
  356. expect(mockReplayApi).toHaveBeenCalledTimes(1);
  357. });
  358. // Expect the table to have 2 rows
  359. expect(await screen.findAllByText('testDisplayName')).toHaveLength(2);
  360. const expectedQuery =
  361. 'query=&referrer=%2Forganizations%2F%3AorgId%2Fissues%2F%3AgroupId%2Freplays%2F&statsPeriod=14d&yAxis=count%28%29';
  362. // Expect the first row to have the correct href
  363. expect(screen.getAllByRole('link', {name: 'testDisplayName'})[0]).toHaveAttribute(
  364. 'href',
  365. `/organizations/org-slug/replays/${REPLAY_ID_1}/?${expectedQuery}`
  366. );
  367. // Expect the second row to have the correct href
  368. expect(screen.getAllByRole('link', {name: 'testDisplayName'})[1]).toHaveAttribute(
  369. 'href',
  370. `/organizations/org-slug/replays/${REPLAY_ID_2}/?${expectedQuery}`
  371. );
  372. // Expect the first row to have the correct duration
  373. expect(screen.getByText('14:32:26')).toBeInTheDocument();
  374. // Expect the second row to have the correct duration
  375. expect(screen.getByText('06:40')).toBeInTheDocument();
  376. // Expect the first row to have the correct errors
  377. expect(screen.getAllByTestId('replay-table-count-errors')[0]).toHaveTextContent(
  378. '1'
  379. );
  380. // Expect the second row to have the correct errors
  381. expect(screen.getAllByTestId('replay-table-count-errors')[1]).toHaveTextContent(
  382. '4'
  383. );
  384. // Expect the first row to have the correct date
  385. expect(screen.getByText('14 days ago')).toBeInTheDocument();
  386. // Expect the second row to have the correct date
  387. expect(screen.getByText('7 days ago')).toBeInTheDocument();
  388. });
  389. it('Should render the replay player when replay-play-from-replay-tab is enabled', async () => {
  390. ({router, organization, routerContext} = init({
  391. organizationProps: {features: ['replay-play-from-replay-tab', 'session-replay']},
  392. }));
  393. const mockGroup = GroupFixture();
  394. const mockReplayCountApi = MockApiClient.addMockResponse({
  395. url: mockReplayCountUrl,
  396. body: {
  397. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  398. },
  399. });
  400. MockApiClient.addMockResponse({
  401. url: mockReplayUrl,
  402. statusCode: 200,
  403. body: {
  404. data: [
  405. {
  406. ...ReplayListFixture()[0],
  407. count_errors: 1,
  408. duration: 52346,
  409. finished_at: new Date('2022-09-15T06:54:00+00:00'),
  410. id: '346789a703f6454384f1de473b8b9fcc',
  411. started_at: new Date('2022-09-15T06:50:00+00:00'),
  412. urls: [
  413. 'https://dev.getsentry.net:7999/replays/',
  414. '/organizations/org-slug/replays/?project=2',
  415. ],
  416. },
  417. {
  418. ...ReplayListFixture()[0],
  419. count_errors: 4,
  420. duration: 400,
  421. finished_at: new Date('2022-09-21T21:40:38+00:00'),
  422. id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
  423. started_at: new Date('2022-09-21T21:30:44+00:00'),
  424. urls: [
  425. 'https://dev.getsentry.net:7999/organizations/org-slug/replays/?project=2&statsPeriod=24h',
  426. '/organizations/org-slug/issues/',
  427. '/organizations/org-slug/issues/?project=2',
  428. ],
  429. },
  430. ].map(hydrated => ({
  431. ...hydrated,
  432. started_at: hydrated.started_at.toString(),
  433. finished_at: hydrated.finished_at.toString(),
  434. })),
  435. },
  436. });
  437. render(<GroupReplays group={mockGroup} />, {
  438. context: routerContext,
  439. organization,
  440. router,
  441. });
  442. expect(await screen.findByText('See Full Replay')).toBeInTheDocument();
  443. expect(mockReplayCountApi).toHaveBeenCalledWith(
  444. mockReplayCountUrl,
  445. expect.objectContaining({
  446. query: {
  447. returnIds: true,
  448. data_source: 'discover',
  449. query: `issue.id:[${mockGroup.id}]`,
  450. statsPeriod: '14d',
  451. project: -1,
  452. },
  453. })
  454. );
  455. });
  456. // Test seems to be flaky
  457. // eslint-disable-next-line jest/no-disabled-tests
  458. it('Should switch replays when clicking and replay-play-from-replay-tab is enabled', async () => {
  459. ({router, organization, routerContext} = init({
  460. organizationProps: {features: ['replay-play-from-replay-tab', 'session-replay']},
  461. }));
  462. const mockGroup = GroupFixture();
  463. const mockReplayCountApi = MockApiClient.addMockResponse({
  464. url: mockReplayCountUrl,
  465. body: {
  466. [mockGroup.id]: [REPLAY_ID_1, REPLAY_ID_2],
  467. },
  468. });
  469. MockApiClient.addMockResponse({
  470. url: mockReplayUrl,
  471. statusCode: 200,
  472. body: {
  473. data: [
  474. {
  475. ...ReplayListFixture()[0],
  476. count_errors: 1,
  477. duration: 52346,
  478. finished_at: new Date('2022-09-15T06:54:00+00:00'),
  479. id: '346789a703f6454384f1de473b8b9fcc',
  480. started_at: new Date('2022-09-15T06:50:00+00:00'),
  481. urls: [
  482. 'https://dev.getsentry.net:7999/replays/',
  483. '/organizations/org-slug/replays/?project=2',
  484. ],
  485. },
  486. {
  487. ...ReplayListFixture()[0],
  488. count_errors: 4,
  489. duration: 400,
  490. finished_at: new Date('2022-09-21T21:40:38+00:00'),
  491. id: 'b05dae9b6be54d21a4d5ad9f8f02b780',
  492. started_at: new Date('2022-09-21T21:30:44+00:00'),
  493. urls: [
  494. 'https://dev.getsentry.net:7999/organizations/org-slug/replays/?project=2&statsPeriod=24h',
  495. '/organizations/org-slug/issues/',
  496. '/organizations/org-slug/issues/?project=2',
  497. ],
  498. },
  499. ].map(hydrated => ({
  500. ...hydrated,
  501. started_at: hydrated.started_at.toString(),
  502. finished_at: hydrated.finished_at.toString(),
  503. })),
  504. },
  505. });
  506. render(<GroupReplays group={mockGroup} />, {
  507. context: routerContext,
  508. organization,
  509. router,
  510. });
  511. await waitFor(() => {
  512. expect(mockReplayCountApi).toHaveBeenCalledWith(
  513. mockReplayCountUrl,
  514. expect.objectContaining({
  515. query: {
  516. returnIds: true,
  517. data_source: 'discover',
  518. query: `issue.id:[${mockGroup.id}]`,
  519. statsPeriod: '14d',
  520. project: -1,
  521. },
  522. })
  523. );
  524. });
  525. const mockReplace = jest.mocked(browserHistory.replace);
  526. const replayPlayPlause = (
  527. await screen.findAllByTestId('replay-table-play-button')
  528. )[0];
  529. await userEvent.click(replayPlayPlause);
  530. await waitFor(() =>
  531. expect(mockReplace).toHaveBeenCalledWith(
  532. expect.objectContaining({
  533. pathname: '/organizations/org-slug/replays/',
  534. query: {
  535. selected_replay_index: 1,
  536. },
  537. })
  538. )
  539. );
  540. });
  541. });
  542. });