index.spec.tsx 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  1. import {browserHistory, InjectedRouter} from 'react-router';
  2. import {initializeOrg} from 'sentry-test/initializeOrg';
  3. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  4. import ProjectsStore from 'sentry/stores/projectsStore';
  5. import TeamStore from 'sentry/stores/teamStore';
  6. import {Project} from 'sentry/types';
  7. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  8. import {
  9. MEPSetting,
  10. MEPState,
  11. } from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  12. import {QueryClient, QueryClientProvider} from 'sentry/utils/queryClient';
  13. import TransactionSummary from 'sentry/views/performance/transactionSummary/transactionOverview';
  14. import {RouteContext} from 'sentry/views/routeContext';
  15. const teams = [
  16. TestStubs.Team({id: '1', slug: 'team1', name: 'Team 1'}),
  17. TestStubs.Team({id: '2', slug: 'team2', name: 'Team 2'}),
  18. ];
  19. function initializeData({
  20. features: additionalFeatures = [],
  21. query = {},
  22. project: prj,
  23. }: {features?: string[]; project?: Project; query?: Record<string, any>} = {}) {
  24. const features = ['discover-basic', 'performance-view', ...additionalFeatures];
  25. const project = prj ?? TestStubs.Project({teams});
  26. const organization = TestStubs.Organization({
  27. features,
  28. projects: [project],
  29. });
  30. const initialData = initializeOrg({
  31. organization,
  32. router: {
  33. location: {
  34. query: {
  35. transaction: '/performance',
  36. project: project.id,
  37. transactionCursor: '1:0:0',
  38. ...query,
  39. },
  40. },
  41. },
  42. });
  43. ProjectsStore.loadInitialData(initialData.organization.projects);
  44. TeamStore.loadInitialData(teams, false, null);
  45. return initialData;
  46. }
  47. function TestComponent({
  48. router,
  49. ...props
  50. }: React.ComponentProps<typeof TransactionSummary> & {
  51. router: InjectedRouter<Record<string, string>, any>;
  52. }) {
  53. const client = new QueryClient();
  54. return (
  55. <QueryClientProvider client={client}>
  56. <RouteContext.Provider value={{router, ...router}}>
  57. <TransactionSummary {...props} />
  58. </RouteContext.Provider>
  59. </QueryClientProvider>
  60. );
  61. }
  62. describe('Performance > TransactionSummary', function () {
  63. let eventStatsMock: jest.Mock;
  64. beforeEach(function () {
  65. // @ts-ignore no-console
  66. // eslint-disable-next-line no-console
  67. jest.spyOn(console, 'error').mockImplementation(jest.fn());
  68. MockApiClient.clearMockResponses();
  69. MockApiClient.addMockResponse({
  70. url: '/organizations/org-slug/projects/',
  71. body: [],
  72. });
  73. MockApiClient.addMockResponse({
  74. url: '/organizations/org-slug/tags/',
  75. body: [],
  76. });
  77. MockApiClient.addMockResponse({
  78. url: '/organizations/org-slug/tags/user.email/values/',
  79. body: [],
  80. });
  81. eventStatsMock = MockApiClient.addMockResponse({
  82. url: '/organizations/org-slug/events-stats/',
  83. body: {data: [[123, []]]},
  84. });
  85. MockApiClient.addMockResponse({
  86. url: '/organizations/org-slug/releases/stats/',
  87. body: [],
  88. });
  89. MockApiClient.addMockResponse({
  90. url: '/organizations/org-slug/issues/?limit=5&project=2&query=is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  91. body: [],
  92. });
  93. MockApiClient.addMockResponse({
  94. url: '/organizations/org-slug/users/',
  95. body: [],
  96. });
  97. MockApiClient.addMockResponse({
  98. url: '/organizations/org-slug/recent-searches/',
  99. body: [],
  100. });
  101. MockApiClient.addMockResponse({
  102. url: '/organizations/org-slug/recent-searches/',
  103. method: 'POST',
  104. body: [],
  105. });
  106. MockApiClient.addMockResponse({
  107. url: '/organizations/org-slug/sdk-updates/',
  108. body: [],
  109. });
  110. MockApiClient.addMockResponse({
  111. url: '/prompts-activity/',
  112. body: {},
  113. });
  114. MockApiClient.addMockResponse({
  115. url: '/organizations/org-slug/events-facets-performance/',
  116. body: {},
  117. });
  118. // Events Mock totals for the sidebar and other summary data
  119. MockApiClient.addMockResponse({
  120. url: '/organizations/org-slug/events/',
  121. body: {
  122. meta: {
  123. fields: {
  124. 'count()': 'number',
  125. 'apdex()': 'number',
  126. 'count_miserable_user()': 'number',
  127. 'user_misery()': 'number',
  128. 'count_unique_user()': 'number',
  129. 'p95()': 'number',
  130. 'failure_rate()': 'number',
  131. 'tpm()': 'number',
  132. project_threshold_config: 'string',
  133. },
  134. },
  135. data: [
  136. {
  137. 'count()': 2,
  138. 'apdex()': 0.6,
  139. 'count_miserable_user()': 122,
  140. 'user_misery()': 0.114,
  141. 'count_unique_user()': 1,
  142. 'p95()': 750.123,
  143. 'failure_rate()': 1,
  144. 'tpm()': 1,
  145. project_threshold_config: ['duration', 300],
  146. },
  147. ],
  148. },
  149. match: [
  150. (_url, options) => {
  151. return options.query?.field?.includes('p95()');
  152. },
  153. ],
  154. });
  155. // [Metrics Enhanced] Events Mock totals for the sidebar and other summary data
  156. MockApiClient.addMockResponse({
  157. url: '/organizations/org-slug/events/',
  158. body: {
  159. meta: {
  160. fields: {
  161. 'count()': 'number',
  162. 'apdex()': 'number',
  163. 'count_miserable_user()': 'number',
  164. 'user_misery()': 'number',
  165. 'count_unique_user()': 'number',
  166. 'p95()': 'number',
  167. 'failure_rate()': 'number',
  168. 'tpm()': 'number',
  169. project_threshold_config: 'string',
  170. },
  171. isMetricsData: true,
  172. },
  173. data: [
  174. {
  175. 'count()': 200,
  176. 'apdex()': 0.5,
  177. 'count_miserable_user()': 120,
  178. 'user_misery()': 0.1,
  179. 'count_unique_user()': 100,
  180. 'p95()': 731.3132,
  181. 'failure_rate()': 1,
  182. 'tpm()': 100,
  183. project_threshold_config: ['duration', 300],
  184. },
  185. ],
  186. },
  187. match: [
  188. (_url, options) => {
  189. const isMetricsEnhanced =
  190. options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED;
  191. return options.query?.field?.includes('p95()') && isMetricsEnhanced;
  192. },
  193. ],
  194. });
  195. // Events Mock unfiltered totals for percentage calculations
  196. MockApiClient.addMockResponse({
  197. url: '/organizations/org-slug/events/',
  198. body: {
  199. meta: {
  200. fields: {
  201. 'tpm()': 'number',
  202. },
  203. },
  204. data: [
  205. {
  206. 'tpm()': 1,
  207. },
  208. ],
  209. },
  210. match: [
  211. (_url, options) => {
  212. return (
  213. options.query?.field?.includes('tpm()') &&
  214. !options.query?.field?.includes('p95()')
  215. );
  216. },
  217. ],
  218. });
  219. // Events Mock count totals for histogram percentage calculations
  220. MockApiClient.addMockResponse({
  221. url: '/organizations/org-slug/events/',
  222. body: {
  223. meta: {
  224. fields: {
  225. 'count()': 'number',
  226. },
  227. },
  228. data: [
  229. {
  230. 'count()': 2,
  231. },
  232. ],
  233. },
  234. match: [
  235. (_url, options) => {
  236. return (
  237. options.query?.field?.includes('count()') &&
  238. !options.query?.field?.includes('p95()')
  239. );
  240. },
  241. ],
  242. });
  243. // Events Transaction list response
  244. MockApiClient.addMockResponse({
  245. url: '/organizations/org-slug/events/',
  246. headers: {
  247. Link:
  248. '<http://localhost/api/0/organizations/org-slug/events/?cursor=2:0:0>; rel="next"; results="true"; cursor="2:0:0",' +
  249. '<http://localhost/api/0/organizations/org-slug/events/?cursor=1:0:0>; rel="previous"; results="false"; cursor="1:0:0"',
  250. },
  251. body: {
  252. meta: {
  253. fields: {
  254. id: 'string',
  255. 'user.display': 'string',
  256. 'transaction.duration': 'duration',
  257. 'project.id': 'integer',
  258. timestamp: 'date',
  259. },
  260. },
  261. data: [
  262. {
  263. id: 'deadbeef',
  264. 'user.display': 'uhoh@example.com',
  265. 'transaction.duration': 400,
  266. 'project.id': 2,
  267. timestamp: '2020-05-21T15:31:18+00:00',
  268. },
  269. ],
  270. },
  271. match: [
  272. (_url, options) => {
  273. return options.query?.field?.includes('user.display');
  274. },
  275. ],
  276. });
  277. // Events Mock totals for status breakdown
  278. MockApiClient.addMockResponse({
  279. url: '/organizations/org-slug/events/',
  280. body: {
  281. meta: {
  282. fields: {
  283. 'transaction.status': 'string',
  284. 'count()': 'number',
  285. },
  286. },
  287. data: [
  288. {
  289. 'count()': 2,
  290. 'transaction.status': 'ok',
  291. },
  292. ],
  293. },
  294. match: [
  295. (_url, options) => {
  296. return options.query?.field?.includes('transaction.status');
  297. },
  298. ],
  299. });
  300. MockApiClient.addMockResponse({
  301. url: '/organizations/org-slug/events-facets/',
  302. body: [
  303. {
  304. key: 'release',
  305. topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
  306. },
  307. {
  308. key: 'environment',
  309. topValues: [
  310. {count: 2, value: 'dev', name: 'dev'},
  311. {count: 1, value: 'prod', name: 'prod'},
  312. ],
  313. },
  314. {
  315. key: 'foo',
  316. topValues: [
  317. {count: 2, value: 'bar', name: 'bar'},
  318. {count: 1, value: 'baz', name: 'baz'},
  319. ],
  320. },
  321. {
  322. key: 'user',
  323. topValues: [
  324. {count: 2, value: 'id:100', name: '100'},
  325. {count: 1, value: 'id:101', name: '101'},
  326. ],
  327. },
  328. ],
  329. });
  330. MockApiClient.addMockResponse({
  331. url: '/organizations/org-slug/project-transaction-threshold-override/',
  332. method: 'GET',
  333. body: {
  334. threshold: '800',
  335. metric: 'lcp',
  336. },
  337. });
  338. MockApiClient.addMockResponse({
  339. url: '/organizations/org-slug/events-vitals/',
  340. body: {
  341. 'measurements.fcp': {
  342. poor: 3,
  343. meh: 100,
  344. good: 47,
  345. total: 150,
  346. p75: 1500,
  347. },
  348. 'measurements.lcp': {
  349. poor: 2,
  350. meh: 38,
  351. good: 40,
  352. total: 80,
  353. p75: 2750,
  354. },
  355. 'measurements.fid': {
  356. poor: 2,
  357. meh: 53,
  358. good: 5,
  359. total: 60,
  360. p75: 1000,
  361. },
  362. 'measurements.cls': {
  363. poor: 3,
  364. meh: 10,
  365. good: 4,
  366. total: 17,
  367. p75: 0.2,
  368. },
  369. },
  370. });
  371. MockApiClient.addMockResponse({
  372. method: 'GET',
  373. url: `/organizations/org-slug/key-transactions-list/`,
  374. body: teams.map(({id}) => ({
  375. team: id,
  376. count: 0,
  377. keyed: [],
  378. })),
  379. });
  380. MockApiClient.addMockResponse({
  381. url: '/organizations/org-slug/events-has-measurements/',
  382. body: {measurements: false},
  383. });
  384. MockApiClient.addMockResponse({
  385. url: '/organizations/org-slug/events-spans-performance/',
  386. body: [
  387. {
  388. op: 'ui.long-task',
  389. group: 'c777169faad84eb4',
  390. description: 'Main UI thread blocked',
  391. frequency: 713,
  392. count: 9040,
  393. avgOccurrences: null,
  394. sumExclusiveTime: 1743893.9822921753,
  395. p50ExclusiveTime: null,
  396. p75ExclusiveTime: 244.9998779296875,
  397. p95ExclusiveTime: null,
  398. p99ExclusiveTime: null,
  399. },
  400. ],
  401. });
  402. MockApiClient.addMockResponse({
  403. url: `/projects/org-slug/project-slug/profiling/functions/`,
  404. body: {functions: []},
  405. });
  406. jest.spyOn(MEPSetting, 'get').mockImplementation(() => MEPState.auto);
  407. });
  408. afterEach(function () {
  409. MockApiClient.clearMockResponses();
  410. ProjectsStore.reset();
  411. jest.clearAllMocks();
  412. jest.restoreAllMocks();
  413. });
  414. describe('with events', function () {
  415. it('renders basic UI elements', async function () {
  416. const {organization, router, routerContext} = initializeData();
  417. render(<TestComponent router={router} location={router.location} />, {
  418. context: routerContext,
  419. organization,
  420. });
  421. // It shows the header
  422. await screen.findByText('Transaction Summary');
  423. expect(screen.getByRole('heading', {name: '/performance'})).toBeInTheDocument();
  424. // It shows a chart
  425. expect(
  426. screen.getByRole('button', {name: 'Display Duration Breakdown'})
  427. ).toBeInTheDocument();
  428. // It shows a searchbar
  429. expect(screen.getByLabelText('Search events')).toBeInTheDocument();
  430. // It shows a table
  431. expect(screen.getByTestId('transactions-table')).toBeInTheDocument();
  432. // Ensure open in discover button exists.
  433. expect(screen.getByTestId('transaction-events-open')).toBeInTheDocument();
  434. // Ensure open issues button exists.
  435. expect(screen.getByRole('button', {name: 'Open in Issues'})).toBeInTheDocument();
  436. // Ensure transaction filter button exists
  437. expect(
  438. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  439. ).toBeInTheDocument();
  440. // Ensure create alert from discover is hidden without metric alert
  441. expect(
  442. screen.queryByRole('button', {name: 'Create Alert'})
  443. ).not.toBeInTheDocument();
  444. // Ensure status breakdown exists
  445. expect(screen.getByText('Status Breakdown')).toBeInTheDocument();
  446. });
  447. it('renders feature flagged UI elements', function () {
  448. const {organization, router, routerContext} = initializeData({
  449. features: ['incidents'],
  450. });
  451. render(<TestComponent router={router} location={router.location} />, {
  452. context: routerContext,
  453. organization,
  454. });
  455. // Ensure create alert from discover is shown with metric alerts
  456. expect(screen.getByRole('button', {name: 'Create Alert'})).toBeInTheDocument();
  457. });
  458. it('renders Web Vitals widget', async function () {
  459. const {organization, router, routerContext} = initializeData({
  460. project: TestStubs.Project({teams, platform: 'javascript'}),
  461. query: {
  462. query:
  463. 'transaction.duration:<15m transaction.op:pageload event.type:transaction transaction:/organizations/:orgId/issues/',
  464. },
  465. });
  466. render(<TestComponent router={router} location={router.location} />, {
  467. context: routerContext,
  468. organization,
  469. });
  470. // It renders the web vitals widget
  471. await screen.findByRole('heading', {name: 'Web Vitals'});
  472. const vitalStatues = screen.getAllByTestId('vital-status');
  473. expect(vitalStatues).toHaveLength(3);
  474. expect(vitalStatues[0]).toHaveTextContent('31%');
  475. expect(vitalStatues[1]).toHaveTextContent('65%');
  476. expect(vitalStatues[2]).toHaveTextContent('3%');
  477. });
  478. it('renders sidebar widgets', async function () {
  479. const {organization, router, routerContext} = initializeData({});
  480. render(<TestComponent router={router} location={router.location} />, {
  481. context: routerContext,
  482. organization,
  483. });
  484. // Renders Apdex widget
  485. await screen.findByRole('heading', {name: 'Apdex'});
  486. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  487. // Renders Failure Rate widget
  488. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  489. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  490. // Renders TPM widget
  491. expect(
  492. screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
  493. ).toBeInTheDocument();
  494. expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
  495. '100%'
  496. );
  497. });
  498. it('fetches transaction threshold', function () {
  499. const {organization, router, routerContext} = initializeData();
  500. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  501. url: '/organizations/org-slug/project-transaction-threshold-override/',
  502. method: 'GET',
  503. body: {
  504. threshold: '800',
  505. metric: 'lcp',
  506. },
  507. });
  508. const getProjectThresholdMock = MockApiClient.addMockResponse({
  509. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  510. method: 'GET',
  511. body: {
  512. threshold: '200',
  513. metric: 'duration',
  514. },
  515. });
  516. render(<TestComponent router={router} location={router.location} />, {
  517. context: routerContext,
  518. organization,
  519. });
  520. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  521. expect(getProjectThresholdMock).not.toHaveBeenCalled();
  522. });
  523. it('fetches project transaction threshdold', async function () {
  524. const {organization, router, routerContext} = initializeData();
  525. const getTransactionThresholdMock = MockApiClient.addMockResponse({
  526. url: '/organizations/org-slug/project-transaction-threshold-override/',
  527. method: 'GET',
  528. statusCode: 404,
  529. });
  530. const getProjectThresholdMock = MockApiClient.addMockResponse({
  531. url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
  532. method: 'GET',
  533. body: {
  534. threshold: '200',
  535. metric: 'duration',
  536. },
  537. });
  538. render(<TestComponent router={router} location={router.location} />, {
  539. context: routerContext,
  540. organization,
  541. });
  542. await screen.findByText('Transaction Summary');
  543. expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
  544. expect(getProjectThresholdMock).toHaveBeenCalledTimes(1);
  545. });
  546. it('triggers a navigation on search', async function () {
  547. const {organization, router, routerContext} = initializeData();
  548. render(<TestComponent router={router} location={router.location} />, {
  549. context: routerContext,
  550. organization,
  551. });
  552. // Fill out the search box, and submit it.
  553. await userEvent.type(
  554. screen.getByLabelText('Search events'),
  555. 'user.email:uhoh*{enter}'
  556. );
  557. // Check the navigation.
  558. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  559. expect(browserHistory.push).toHaveBeenCalledWith({
  560. pathname: undefined,
  561. query: {
  562. transaction: '/performance',
  563. project: '2',
  564. statsPeriod: '14d',
  565. query: 'user.email:uhoh*',
  566. transactionCursor: '1:0:0',
  567. },
  568. });
  569. });
  570. it('can mark a transaction as key', async function () {
  571. const {organization, router, routerContext} = initializeData();
  572. render(<TestComponent router={router} location={router.location} />, {
  573. context: routerContext,
  574. organization,
  575. });
  576. const mockUpdate = MockApiClient.addMockResponse({
  577. url: `/organizations/org-slug/key-transactions/`,
  578. method: 'POST',
  579. body: {},
  580. });
  581. await screen.findByRole('button', {name: 'Star for Team'});
  582. // Click the key transaction button
  583. await userEvent.click(screen.getByRole('button', {name: 'Star for Team'}));
  584. await userEvent.click(screen.getByText('team1'));
  585. // Ensure request was made.
  586. expect(mockUpdate).toHaveBeenCalled();
  587. });
  588. it('triggers a navigation on transaction filter', async function () {
  589. const {organization, router, routerContext} = initializeData();
  590. render(<TestComponent router={router} location={router.location} />, {
  591. context: routerContext,
  592. organization,
  593. });
  594. await screen.findByText('Transaction Summary');
  595. await waitFor(() => {
  596. expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
  597. });
  598. // Open the transaction filter dropdown
  599. await userEvent.click(
  600. screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
  601. );
  602. await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]);
  603. // Check the navigation.
  604. expect(browserHistory.push).toHaveBeenCalledWith({
  605. pathname: undefined,
  606. query: {
  607. transaction: '/performance',
  608. project: '2',
  609. showTransactions: 'slow',
  610. transactionCursor: undefined,
  611. },
  612. });
  613. });
  614. it('renders pagination buttons', async function () {
  615. const {organization, router, routerContext} = initializeData();
  616. render(<TestComponent router={router} location={router.location} />, {
  617. context: routerContext,
  618. organization,
  619. });
  620. await screen.findByText('Transaction Summary');
  621. expect(await screen.findByLabelText('Previous')).toBeInTheDocument();
  622. // Click the 'next' button
  623. await userEvent.click(screen.getByLabelText('Next'));
  624. // Check the navigation.
  625. expect(browserHistory.push).toHaveBeenCalledWith({
  626. pathname: undefined,
  627. query: {
  628. transaction: '/performance',
  629. project: '2',
  630. transactionCursor: '2:0:0',
  631. },
  632. });
  633. });
  634. it('forwards conditions to related issues', async function () {
  635. const issueGet = MockApiClient.addMockResponse({
  636. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  637. body: [],
  638. });
  639. const {organization, router, routerContext} = initializeData({
  640. query: {query: 'tag:value'},
  641. });
  642. render(<TestComponent router={router} location={router.location} />, {
  643. context: routerContext,
  644. organization,
  645. });
  646. await screen.findByText('Transaction Summary');
  647. expect(issueGet).toHaveBeenCalled();
  648. });
  649. it('does not forward event type to related issues', async function () {
  650. const issueGet = MockApiClient.addMockResponse({
  651. url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  652. body: [],
  653. match: [
  654. (_, options) => {
  655. // event.type must NOT be in the query params
  656. return !options.query?.query?.includes('event.type');
  657. },
  658. ],
  659. });
  660. const {organization, router, routerContext} = initializeData({
  661. query: {query: 'tag:value event.type:transaction'},
  662. });
  663. render(<TestComponent router={router} location={router.location} />, {
  664. context: routerContext,
  665. organization,
  666. });
  667. await screen.findByText('Transaction Summary');
  668. expect(issueGet).toHaveBeenCalled();
  669. });
  670. it('renders the suspect spans table if the feature is enabled', async function () {
  671. MockApiClient.addMockResponse({
  672. url: '/organizations/org-slug/events-spans-performance/',
  673. body: [],
  674. });
  675. const {organization, router, routerContext} = initializeData({
  676. features: ['performance-suspect-spans-view'],
  677. });
  678. render(<TestComponent router={router} location={router.location} />, {
  679. context: routerContext,
  680. organization,
  681. });
  682. expect(await screen.findByText('Suspect Spans')).toBeInTheDocument();
  683. });
  684. it('adds search condition on transaction status when clicking on status breakdown', async function () {
  685. const {organization, router, routerContext} = initializeData();
  686. render(<TestComponent router={router} location={router.location} />, {
  687. context: routerContext,
  688. organization,
  689. });
  690. await screen.findByTestId('status-ok');
  691. await userEvent.click(screen.getByTestId('status-ok'));
  692. expect(browserHistory.push).toHaveBeenCalledTimes(1);
  693. expect(browserHistory.push).toHaveBeenCalledWith(
  694. expect.objectContaining({
  695. query: expect.objectContaining({
  696. query: expect.stringContaining('transaction.status:ok'),
  697. }),
  698. })
  699. );
  700. });
  701. it('appends tag value to existing query when clicked', async function () {
  702. const {organization, router, routerContext} = initializeData();
  703. render(<TestComponent router={router} location={router.location} />, {
  704. context: routerContext,
  705. organization,
  706. });
  707. await screen.findByText('Tag Summary');
  708. await userEvent.click(
  709. await screen.findByLabelText(
  710. 'environment, dev, 100% of all events. View events with this tag value.'
  711. )
  712. );
  713. await userEvent.click(
  714. await screen.findByLabelText(
  715. 'foo, bar, 100% of all events. View events with this tag value.'
  716. )
  717. );
  718. expect(router.push).toHaveBeenCalledTimes(2);
  719. expect(router.push).toHaveBeenNthCalledWith(1, {
  720. query: {
  721. project: '2',
  722. query: 'tags[environment]:dev',
  723. transaction: '/performance',
  724. transactionCursor: '1:0:0',
  725. },
  726. });
  727. expect(router.push).toHaveBeenNthCalledWith(2, {
  728. query: {
  729. project: '2',
  730. query: 'foo:bar',
  731. transaction: '/performance',
  732. transactionCursor: '1:0:0',
  733. },
  734. });
  735. });
  736. it('does not use MEP dataset for stats query without features', async function () {
  737. const {organization, router, routerContext} = initializeData({
  738. query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
  739. features: [''], // No 'dynamic-sampling' feature to indicate it can use metrics dataset or metrics enhanced.
  740. });
  741. render(<TestComponent router={router} location={router.location} />, {
  742. context: routerContext,
  743. organization,
  744. });
  745. await screen.findByText('Transaction Summary');
  746. await screen.findByRole('heading', {name: 'Apdex'});
  747. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
  748. expect(eventStatsMock).toHaveBeenNthCalledWith(
  749. 1,
  750. expect.anything(),
  751. expect.objectContaining({
  752. query: expect.objectContaining({
  753. environment: [],
  754. interval: '30m',
  755. partial: '1',
  756. project: [2],
  757. query:
  758. 'transaction.op:pageload event.type:transaction transaction:/performance',
  759. referrer: 'api.performance.transaction-summary.duration-chart',
  760. statsPeriod: '14d',
  761. yAxis: ['p50()', 'p75()', 'p95()', 'p99()', 'p100()'],
  762. }),
  763. })
  764. );
  765. });
  766. it('uses MEP dataset for stats query', async function () {
  767. const {organization, router, routerContext} = initializeData({
  768. query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
  769. features: ['dynamic-sampling', 'mep-rollout-flag'],
  770. });
  771. render(<TestComponent router={router} location={router.location} />, {
  772. context: routerContext,
  773. organization,
  774. });
  775. await screen.findByText('Transaction Summary');
  776. // Renders Apdex widget
  777. await screen.findByRole('heading', {name: 'Apdex'});
  778. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5');
  779. expect(eventStatsMock).toHaveBeenNthCalledWith(
  780. 1,
  781. expect.anything(),
  782. expect.objectContaining({
  783. query: expect.objectContaining({
  784. query:
  785. 'transaction.op:pageload event.type:transaction transaction:/performance',
  786. dataset: 'metricsEnhanced',
  787. }),
  788. })
  789. );
  790. // Renders Failure Rate widget
  791. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  792. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  793. // Renders TPM widget
  794. expect(
  795. screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
  796. ).toBeInTheDocument();
  797. expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
  798. '100%'
  799. );
  800. expect(
  801. screen.queryByTestId('search-metrics-fallback-warning')
  802. ).not.toBeInTheDocument();
  803. });
  804. it('uses MEP dataset for stats query and shows fallback warning', async function () {
  805. MockApiClient.addMockResponse({
  806. url: '/organizations/org-slug/issues/?limit=5&project=2&query=has%3Anot-compatible%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
  807. body: [],
  808. });
  809. MockApiClient.addMockResponse({
  810. url: '/organizations/org-slug/events/',
  811. body: {
  812. meta: {
  813. fields: {
  814. 'count()': 'number',
  815. 'apdex()': 'number',
  816. 'count_miserable_user()': 'number',
  817. 'user_misery()': 'number',
  818. 'count_unique_user()': 'number',
  819. 'p95()': 'number',
  820. 'failure_rate()': 'number',
  821. 'tpm()': 'number',
  822. project_threshold_config: 'string',
  823. },
  824. isMetricsData: false, // The total response is setting the metrics fallback behaviour.
  825. },
  826. data: [
  827. {
  828. 'count()': 200,
  829. 'apdex()': 0.5,
  830. 'count_miserable_user()': 120,
  831. 'user_misery()': 0.1,
  832. 'count_unique_user()': 100,
  833. 'p95()': 731.3132,
  834. 'failure_rate()': 1,
  835. 'tpm()': 100,
  836. project_threshold_config: ['duration', 300],
  837. },
  838. ],
  839. },
  840. match: [
  841. (_url, options) => {
  842. const isMetricsEnhanced =
  843. options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED;
  844. return (
  845. options.query?.field?.includes('p95()') &&
  846. isMetricsEnhanced &&
  847. options.query?.query?.includes('not-compatible')
  848. );
  849. },
  850. ],
  851. });
  852. const {organization, router, routerContext} = initializeData({
  853. query: {query: 'transaction.op:pageload has:not-compatible'}, // Adds incompatible w/ metrics tag
  854. features: ['dynamic-sampling', 'mep-rollout-flag'],
  855. });
  856. render(<TestComponent router={router} location={router.location} />, {
  857. context: routerContext,
  858. organization,
  859. });
  860. await screen.findByText('Transaction Summary');
  861. // Renders Apdex widget
  862. await screen.findByRole('heading', {name: 'Apdex'});
  863. expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5');
  864. expect(eventStatsMock).toHaveBeenNthCalledWith(
  865. 1,
  866. expect.anything(),
  867. expect.objectContaining({
  868. query: expect.objectContaining({
  869. query:
  870. 'transaction.op:pageload has:not-compatible event.type:transaction transaction:/performance',
  871. dataset: 'metricsEnhanced',
  872. }),
  873. })
  874. );
  875. // Renders Failure Rate widget
  876. expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
  877. expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
  878. // Renders TPM widget
  879. expect(
  880. screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
  881. ).toBeInTheDocument();
  882. expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
  883. '100%'
  884. );
  885. expect(screen.getByTestId('search-metrics-fallback-warning')).toBeInTheDocument();
  886. });
  887. });
  888. });