index.spec.tsx 31 KB

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