groupReplays.spec.tsx 18 KB

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