index.spec.tsx 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  1. import {browserHistory} from 'react-router';
  2. import {Location} from 'history';
  3. import {Organization} from 'sentry-fixture/organization';
  4. import {initializeOrg} from 'sentry-test/initializeOrg';
  5. import {
  6. initializeData,
  7. InitializeDataSettings,
  8. } from 'sentry-test/performance/initializePerformanceData';
  9. import {
  10. act,
  11. fireEvent,
  12. render,
  13. screen,
  14. userEvent,
  15. waitFor,
  16. within,
  17. } from 'sentry-test/reactTestingLibrary';
  18. import PageFiltersStore from 'sentry/stores/pageFiltersStore';
  19. import ProjectsStore from 'sentry/stores/projectsStore';
  20. import {WebVital} from 'sentry/utils/fields';
  21. import TrendsIndex from 'sentry/views/performance/trends/';
  22. import {defaultTrendsSelectionDate} from 'sentry/views/performance/trends/content';
  23. import {
  24. DEFAULT_MAX_DURATION,
  25. TRENDS_FUNCTIONS,
  26. TRENDS_PARAMETERS,
  27. } from 'sentry/views/performance/trends/utils';
  28. const trendsViewQuery = {
  29. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  30. };
  31. jest.mock('moment', () => {
  32. const moment = jest.requireActual('moment');
  33. moment.now = jest.fn().mockReturnValue(1601251200000);
  34. return moment;
  35. });
  36. async function getTrendDropdown() {
  37. const dropdown = await screen.findByRole('button', {name: /Percentile.+/});
  38. expect(dropdown).toBeInTheDocument();
  39. return dropdown;
  40. }
  41. async function getParameterDropdown() {
  42. const dropdown = await screen.findByRole('button', {name: /Parameter.+/});
  43. expect(dropdown).toBeInTheDocument();
  44. return dropdown;
  45. }
  46. async function waitForMockCall(mock: any) {
  47. await waitFor(() => {
  48. expect(mock).toHaveBeenCalled();
  49. });
  50. }
  51. function enterSearch(el, text) {
  52. fireEvent.change(el, {target: {value: text}});
  53. fireEvent.submit(el);
  54. }
  55. // Might swap on/off the skiphover to check perf later.
  56. async function clickEl(el) {
  57. await userEvent.click(el, {skipHover: true});
  58. }
  59. function _initializeData(
  60. settings: InitializeDataSettings,
  61. options?: {selectedProjectId?: string}
  62. ) {
  63. const newSettings = {...settings};
  64. newSettings.projects = settings.projects ?? [
  65. TestStubs.Project({id: '1', firstTransactionEvent: false}),
  66. TestStubs.Project({id: '2', firstTransactionEvent: true}),
  67. ];
  68. if (options?.selectedProjectId) {
  69. const selectedProject = newSettings.projects.find(
  70. p => p.id === options.selectedProjectId
  71. );
  72. if (!selectedProject) {
  73. throw new Error("Test is selecting project that isn't loaded");
  74. } else {
  75. PageFiltersStore.updateProjects(
  76. settings.selectedProject ? [Number(selectedProject)] : [],
  77. []
  78. );
  79. }
  80. newSettings.selectedProject = selectedProject.id;
  81. }
  82. newSettings.selectedProject = settings.selectedProject ?? newSettings.projects[0].id;
  83. const data = initializeData(newSettings);
  84. // Modify page filters store to stop rerendering due to the test harness.
  85. PageFiltersStore.onInitializeUrlState(
  86. {
  87. projects: [],
  88. environments: [],
  89. datetime: {start: null, end: null, period: '24h', utc: null},
  90. },
  91. new Set()
  92. );
  93. PageFiltersStore.updateDateTime(defaultTrendsSelectionDate);
  94. if (!options?.selectedProjectId) {
  95. PageFiltersStore.updateProjects(
  96. settings.selectedProject ? [Number(newSettings.projects[0].id)] : [],
  97. []
  98. );
  99. }
  100. act(() => ProjectsStore.loadInitialData(data.projects));
  101. return data;
  102. }
  103. function initializeTrendsData(
  104. projects: null | any[] = null,
  105. query = {},
  106. includeDefaultQuery = true,
  107. extraFeatures?: string[]
  108. ) {
  109. const _projects = Array.isArray(projects)
  110. ? projects
  111. : [
  112. TestStubs.Project({id: '1', firstTransactionEvent: false}),
  113. TestStubs.Project({id: '2', firstTransactionEvent: true}),
  114. ];
  115. const features = extraFeatures
  116. ? ['transaction-event', 'performance-view', ...extraFeatures]
  117. : ['transaction-event', 'performance-view'];
  118. const organization = Organization({
  119. features,
  120. projects: _projects,
  121. });
  122. const newQuery = {...(includeDefaultQuery ? trendsViewQuery : {}), ...query};
  123. const initialData = initializeOrg({
  124. organization,
  125. router: {
  126. location: {
  127. query: newQuery,
  128. },
  129. },
  130. projects: _projects,
  131. project: projects ? projects[0] : undefined,
  132. });
  133. act(() => ProjectsStore.loadInitialData(initialData.organization.projects));
  134. return initialData;
  135. }
  136. describe('Performance > Trends', function () {
  137. let trendsStatsMock;
  138. beforeEach(function () {
  139. browserHistory.push = jest.fn();
  140. MockApiClient.addMockResponse({
  141. url: '/organizations/org-slug/projects/',
  142. body: [],
  143. });
  144. MockApiClient.addMockResponse({
  145. url: '/organizations/org-slug/tags/',
  146. body: [],
  147. });
  148. MockApiClient.addMockResponse({
  149. url: '/organizations/org-slug/users/',
  150. body: [],
  151. });
  152. MockApiClient.addMockResponse({
  153. url: '/organizations/org-slug/recent-searches/',
  154. body: [],
  155. });
  156. MockApiClient.addMockResponse({
  157. url: '/organizations/org-slug/recent-searches/',
  158. method: 'POST',
  159. body: [],
  160. });
  161. MockApiClient.addMockResponse({
  162. url: '/organizations/org-slug/sdk-updates/',
  163. body: [],
  164. });
  165. MockApiClient.addMockResponse({
  166. url: '/prompts-activity/',
  167. body: {},
  168. });
  169. MockApiClient.addMockResponse({
  170. url: '/organizations/org-slug/releases/stats/',
  171. body: [],
  172. });
  173. MockApiClient.addMockResponse({
  174. url: '/organizations/org-slug/tags/transaction.duration/values/',
  175. body: [],
  176. });
  177. trendsStatsMock = MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/events-trends-stats/',
  179. body: {
  180. stats: {
  181. 'internal,/organizations/:orgId/performance/': {
  182. data: [[123, []]],
  183. },
  184. order: 0,
  185. },
  186. events: {
  187. meta: {
  188. count_range_1: 'integer',
  189. count_range_2: 'integer',
  190. count_percentage: 'percentage',
  191. breakpoint: 'number',
  192. trend_percentage: 'percentage',
  193. trend_difference: 'number',
  194. aggregate_range_1: 'duration',
  195. aggregate_range_2: 'duration',
  196. transaction: 'string',
  197. },
  198. data: [
  199. {
  200. count: 8,
  201. project: 'internal',
  202. count_range_1: 2,
  203. count_range_2: 6,
  204. count_percentage: 3,
  205. breakpoint: 1686967200,
  206. trend_percentage: 1.9235225955967554,
  207. trend_difference: 797,
  208. aggregate_range_1: 863,
  209. aggregate_range_2: 1660,
  210. transaction: '/organizations/:orgId/performance/',
  211. },
  212. {
  213. count: 60,
  214. project: 'internal',
  215. count_range_1: 20,
  216. count_range_2: 40,
  217. count_percentage: 2,
  218. breakpoint: 1686967200,
  219. trend_percentage: 1.204968944099379,
  220. trend_difference: 66,
  221. aggregate_range_1: 322,
  222. aggregate_range_2: 388,
  223. transaction: '/api/0/internal/health/',
  224. },
  225. ],
  226. },
  227. },
  228. });
  229. MockApiClient.addMockResponse({
  230. url: '/organizations/org-slug/events/',
  231. body: {
  232. data: [
  233. {
  234. 'p95()': 1010.9232499999998,
  235. 'p50()': 47.34580982348902,
  236. 'tps()': 3.7226926286168966,
  237. 'count()': 34872349,
  238. 'failure_rate()': 0.43428379,
  239. 'examples()': ['djk3w308er', '3298a9ui3h'],
  240. },
  241. ],
  242. meta: {
  243. fields: {
  244. 'p95()': 'duration',
  245. '950()': 'duration',
  246. 'tps()': 'number',
  247. 'count()': 'number',
  248. 'failure_rate()': 'number',
  249. 'examples()': 'Array',
  250. },
  251. units: {
  252. 'p95()': 'millisecond',
  253. 'p50()': 'millisecond',
  254. 'tps()': null,
  255. 'count()': null,
  256. 'failure_rate()': null,
  257. 'examples()': null,
  258. },
  259. isMetricsData: true,
  260. tips: {},
  261. dataset: 'metrics',
  262. },
  263. },
  264. });
  265. MockApiClient.addMockResponse({
  266. url: '/organizations/org-slug/events-spans-performance/',
  267. body: [],
  268. });
  269. });
  270. afterEach(function () {
  271. MockApiClient.clearMockResponses();
  272. act(() => ProjectsStore.reset());
  273. });
  274. it('renders basic UI elements', async function () {
  275. const data = _initializeData({});
  276. render(
  277. <TrendsIndex location={data.router.location} organization={data.organization} />,
  278. {
  279. context: data.routerContext,
  280. organization: data.organization,
  281. }
  282. );
  283. expect(await getTrendDropdown()).toBeInTheDocument();
  284. expect(await getParameterDropdown()).toBeInTheDocument();
  285. expect(screen.getAllByTestId('changed-transactions')).toHaveLength(2);
  286. });
  287. it('transaction list items are rendered', async function () {
  288. const data = _initializeData({});
  289. render(
  290. <TrendsIndex location={data.router.location} organization={data.organization} />,
  291. {
  292. context: data.routerContext,
  293. organization: data.organization,
  294. }
  295. );
  296. expect(await screen.findAllByTestId('trends-list-item-regression')).toHaveLength(2);
  297. expect(await screen.findAllByTestId('trends-list-item-improved')).toHaveLength(2);
  298. });
  299. it('view summary menu action links to the correct view', async function () {
  300. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  301. const data = initializeTrendsData(projects, {project: ['1']});
  302. render(
  303. <TrendsIndex location={data.router.location} organization={data.organization} />,
  304. {
  305. context: data.routerContext,
  306. organization: data.organization,
  307. }
  308. );
  309. const transactions = await screen.findAllByTestId('trends-list-item-improved');
  310. expect(transactions).toHaveLength(2);
  311. const firstTransaction = transactions[0];
  312. const summaryLink = within(firstTransaction).getByTestId('item-transaction-name');
  313. expect(summaryLink.closest('a')).toHaveAttribute(
  314. 'href',
  315. '/organizations/org-slug/performance/summary/?display=trend&project=1&query=tpm%28%29%3A%3E0.01%20transaction.duration%3A%3E0%20transaction.duration%3A%3C15min%20count_percentage%28%29%3A%3E0.25%20count_percentage%28%29%3A%3C4%20trend_percentage%28%29%3A%3E0%25%20confidence%28%29%3A%3E6&referrer=performance-transaction-summary&statsPeriod=14d&transaction=%2Forganizations%2F%3AorgId%2Fperformance%2F&trendFunction=p50&unselectedSeries=p100%28%29&unselectedSeries=avg%28%29'
  316. );
  317. });
  318. it('view summary menu action opens performance change explorer with feature flag', async function () {
  319. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  320. const data = initializeTrendsData(projects, {project: ['1']}, true, [
  321. 'performance-change-explorer',
  322. ]);
  323. render(
  324. <TrendsIndex location={data.router.location} organization={data.organization} />,
  325. {
  326. context: data.routerContext,
  327. organization: data.organization,
  328. }
  329. );
  330. const transactions = await screen.findAllByTestId('trends-list-item-improved');
  331. expect(transactions).toHaveLength(2);
  332. const firstTransaction = transactions[0];
  333. const summaryLink = within(firstTransaction).getByTestId('item-transaction-name');
  334. expect(summaryLink.closest('a')).not.toHaveAttribute('href');
  335. await clickEl(summaryLink);
  336. await waitFor(() => {
  337. expect(screen.getByText('Ongoing Improvement')).toBeInTheDocument();
  338. expect(screen.getByText('Throughput')).toBeInTheDocument();
  339. expect(screen.getByText('P95')).toBeInTheDocument();
  340. expect(screen.getByText('P50')).toBeInTheDocument();
  341. expect(screen.getByText('Failure Rate')).toBeInTheDocument();
  342. });
  343. });
  344. it('hide from list menu action modifies query', async function () {
  345. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  346. const data = initializeTrendsData(projects, {project: ['1']});
  347. render(
  348. <TrendsIndex location={data.router.location} organization={data.organization} />,
  349. {
  350. context: data.routerContext,
  351. organization: data.organization,
  352. }
  353. );
  354. const transactions = await screen.findAllByTestId('trends-list-item-improved');
  355. expect(transactions).toHaveLength(2);
  356. const firstTransaction = transactions[0];
  357. const menuActions = within(firstTransaction).getAllByTestId('menu-action');
  358. expect(menuActions).toHaveLength(3);
  359. const menuAction = menuActions[2];
  360. await clickEl(menuAction);
  361. expect(browserHistory.push).toHaveBeenCalledWith({
  362. query: expect.objectContaining({
  363. project: expect.anything(),
  364. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION} !transaction:/organizations/:orgId/performance/`,
  365. }),
  366. });
  367. });
  368. it('Changing search causes cursors to be reset', async function () {
  369. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  370. const data = initializeTrendsData(projects, {project: ['1']});
  371. render(
  372. <TrendsIndex location={data.router.location} organization={data.organization} />,
  373. {
  374. context: data.routerContext,
  375. organization: data.organization,
  376. }
  377. );
  378. const input = await screen.findByTestId('smart-search-input');
  379. enterSearch(input, 'transaction.duration:>9000');
  380. expect(browserHistory.push).toHaveBeenCalledWith({
  381. pathname: undefined,
  382. query: expect.objectContaining({
  383. project: ['1'],
  384. query: 'transaction.duration:>9000',
  385. improvedCursor: undefined,
  386. regressionCursor: undefined,
  387. }),
  388. });
  389. });
  390. it('exclude greater than list menu action modifies query', async function () {
  391. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  392. const data = initializeTrendsData(projects, {project: ['1']});
  393. render(
  394. <TrendsIndex location={data.router.location} organization={data.organization} />,
  395. {
  396. context: data.routerContext,
  397. organization: data.organization,
  398. }
  399. );
  400. const transactions = await screen.findAllByTestId('trends-list-item-improved');
  401. expect(transactions).toHaveLength(2);
  402. const firstTransaction = transactions[0];
  403. const menuActions = within(firstTransaction).getAllByTestId('menu-action');
  404. expect(menuActions).toHaveLength(3);
  405. const menuAction = menuActions[0];
  406. await clickEl(menuAction);
  407. expect(browserHistory.push).toHaveBeenCalledWith({
  408. query: expect.objectContaining({
  409. project: expect.anything(),
  410. query: 'tpm():>0.01 transaction.duration:>0 transaction.duration:<=863',
  411. }),
  412. });
  413. });
  414. it('exclude less than list menu action modifies query', async function () {
  415. const projects = [TestStubs.Project({id: 1, slug: 'internal'}), TestStubs.Project()];
  416. const data = initializeTrendsData(projects, {project: ['1']});
  417. render(
  418. <TrendsIndex location={data.router.location} organization={data.organization} />,
  419. {
  420. context: data.routerContext,
  421. organization: data.organization,
  422. }
  423. );
  424. const transactions = await screen.findAllByTestId('trends-list-item-improved');
  425. expect(transactions).toHaveLength(2);
  426. const firstTransaction = transactions[0];
  427. const menuActions = within(firstTransaction).getAllByTestId('menu-action');
  428. expect(menuActions).toHaveLength(3);
  429. const menuAction = menuActions[1];
  430. await clickEl(menuAction);
  431. expect(browserHistory.push).toHaveBeenCalledWith({
  432. query: expect.objectContaining({
  433. project: expect.anything(),
  434. query: 'tpm():>0.01 transaction.duration:<15min transaction.duration:>=863',
  435. }),
  436. });
  437. });
  438. it('choosing a trend function changes location', async function () {
  439. const projects = [TestStubs.Project()];
  440. const data = initializeTrendsData(projects, {project: ['-1']});
  441. render(
  442. <TrendsIndex location={data.router.location} organization={data.organization} />,
  443. {
  444. context: data.routerContext,
  445. organization: data.organization,
  446. }
  447. );
  448. for (const trendFunction of TRENDS_FUNCTIONS) {
  449. // Open dropdown
  450. const dropdown = await getTrendDropdown();
  451. await clickEl(dropdown);
  452. // Select function
  453. const option = screen.getByRole('option', {name: trendFunction.label});
  454. await clickEl(option);
  455. expect(browserHistory.push).toHaveBeenCalledWith({
  456. query: expect.objectContaining({
  457. regressionCursor: undefined,
  458. improvedCursor: undefined,
  459. trendFunction: trendFunction.field,
  460. }),
  461. });
  462. }
  463. });
  464. it('sets LCP as a default trend parameter for frontend project if query does not specify trend parameter', async function () {
  465. const projects = [TestStubs.Project({id: 1, platform: 'javascript'})];
  466. const data = initializeTrendsData(projects, {project: [1]});
  467. render(
  468. <TrendsIndex location={data.router.location} organization={data.organization} />,
  469. {
  470. context: data.routerContext,
  471. organization: data.organization,
  472. }
  473. );
  474. const trendDropdownButton = await getTrendDropdown();
  475. expect(trendDropdownButton).toHaveTextContent('Percentilep50');
  476. });
  477. it('sets duration as a default trend parameter for backend project if query does not specify trend parameter', async function () {
  478. const projects = [TestStubs.Project({id: 1, platform: 'python'})];
  479. const data = initializeTrendsData(projects, {project: [1]});
  480. render(
  481. <TrendsIndex location={data.router.location} organization={data.organization} />,
  482. {
  483. context: data.routerContext,
  484. organization: data.organization,
  485. }
  486. );
  487. const parameterDropdownButton = await getParameterDropdown();
  488. expect(parameterDropdownButton).toHaveTextContent('ParameterDuration');
  489. });
  490. it('sets trend parameter from query and ignores default trend parameter', async function () {
  491. const projects = [TestStubs.Project({id: 1, platform: 'javascript'})];
  492. const data = initializeTrendsData(projects, {project: [1], trendParameter: 'FCP'});
  493. render(
  494. <TrendsIndex location={data.router.location} organization={data.organization} />,
  495. {
  496. context: data.routerContext,
  497. organization: data.organization,
  498. }
  499. );
  500. const parameterDropdownButton = await getParameterDropdown();
  501. expect(parameterDropdownButton).toHaveTextContent('ParameterFCP');
  502. });
  503. it('choosing a parameter changes location', async function () {
  504. const projects = [TestStubs.Project()];
  505. const data = initializeTrendsData(projects, {project: ['-1']});
  506. render(
  507. <TrendsIndex location={data.router.location} organization={data.organization} />,
  508. {
  509. context: data.routerContext,
  510. organization: data.organization,
  511. }
  512. );
  513. for (const parameter of TRENDS_PARAMETERS) {
  514. // Open dropdown
  515. const dropdown = await getParameterDropdown();
  516. await clickEl(dropdown);
  517. // Select parameter
  518. const option = screen.getByRole('option', {name: parameter.label});
  519. await clickEl(option);
  520. expect(browserHistory.push).toHaveBeenCalledWith({
  521. query: expect.objectContaining({
  522. trendParameter: parameter.label,
  523. }),
  524. });
  525. }
  526. });
  527. it('choosing a web vitals parameter adds it as an additional condition to the query', async function () {
  528. const projects = [TestStubs.Project()];
  529. const data = initializeTrendsData(projects, {project: ['-1']});
  530. const {rerender} = render(
  531. <TrendsIndex location={data.router.location} organization={data.organization} />,
  532. {
  533. context: data.routerContext,
  534. organization: data.organization,
  535. }
  536. );
  537. for (const parameter of TRENDS_PARAMETERS) {
  538. if (Object.values(WebVital).includes(parameter.column as string as WebVital)) {
  539. trendsStatsMock.mockReset();
  540. const newLocation = {
  541. query: {...trendsViewQuery, trendParameter: parameter.label},
  542. };
  543. rerender(
  544. <TrendsIndex
  545. location={newLocation as unknown as Location}
  546. organization={data.organization}
  547. />
  548. );
  549. await waitForMockCall(trendsStatsMock);
  550. expect(trendsStatsMock).toHaveBeenCalledTimes(2);
  551. // Improved transactions call
  552. expect(trendsStatsMock).toHaveBeenNthCalledWith(
  553. 1,
  554. expect.anything(),
  555. expect.objectContaining({
  556. query: expect.objectContaining({
  557. query: expect.stringContaining(`has:${parameter.column}`),
  558. }),
  559. })
  560. );
  561. // Regression transactions call
  562. expect(trendsStatsMock).toHaveBeenNthCalledWith(
  563. 2,
  564. expect.anything(),
  565. expect.objectContaining({
  566. query: expect.objectContaining({
  567. query: expect.stringContaining(`has:${parameter.column}`),
  568. }),
  569. })
  570. );
  571. }
  572. }
  573. });
  574. it('trend functions in location make api calls', async function () {
  575. const projects = [TestStubs.Project(), TestStubs.Project()];
  576. const data = initializeTrendsData(projects, {project: ['-1']});
  577. const {rerender} = render(
  578. <TrendsIndex location={data.router.location} organization={data.organization} />,
  579. {
  580. context: data.routerContext,
  581. organization: data.organization,
  582. }
  583. );
  584. for (const trendFunction of TRENDS_FUNCTIONS) {
  585. trendsStatsMock.mockReset();
  586. const newLocation = {
  587. query: {...trendsViewQuery, trendFunction: trendFunction.field},
  588. };
  589. rerender(
  590. <TrendsIndex
  591. location={newLocation as unknown as Location}
  592. organization={data.organization}
  593. />
  594. );
  595. await waitForMockCall(trendsStatsMock);
  596. expect(trendsStatsMock).toHaveBeenCalledTimes(2);
  597. const sort = 'trend_percentage()';
  598. const defaultTrendsFields = ['project'];
  599. const transactionFields = ['transaction', ...defaultTrendsFields];
  600. const projectFields = [...defaultTrendsFields];
  601. expect(transactionFields).toHaveLength(2);
  602. expect(projectFields).toHaveLength(transactionFields.length - 1);
  603. // Improved transactions call
  604. expect(trendsStatsMock).toHaveBeenNthCalledWith(
  605. 1,
  606. expect.anything(),
  607. expect.objectContaining({
  608. query: expect.objectContaining({
  609. trendFunction: `${trendFunction.field}(transaction.duration)`,
  610. sort,
  611. query: expect.stringContaining('trend_percentage():>0%'),
  612. interval: '1h',
  613. field: transactionFields,
  614. statsPeriod: '14d',
  615. }),
  616. })
  617. );
  618. // Regression transactions call
  619. expect(trendsStatsMock).toHaveBeenNthCalledWith(
  620. 2,
  621. expect.anything(),
  622. expect.objectContaining({
  623. query: expect.objectContaining({
  624. trendFunction: `${trendFunction.field}(transaction.duration)`,
  625. sort: '-' + sort,
  626. query: expect.stringContaining('trend_percentage():>0%'),
  627. interval: '1h',
  628. field: transactionFields,
  629. statsPeriod: '14d',
  630. }),
  631. })
  632. );
  633. }
  634. });
  635. it('Visiting trends with trends feature will update filters if none are set', function () {
  636. const data = initializeTrendsData(undefined, {}, false);
  637. render(
  638. <TrendsIndex location={data.router.location} organization={data.organization} />,
  639. {
  640. context: data.routerContext,
  641. organization: data.organization,
  642. }
  643. );
  644. expect(browserHistory.push).toHaveBeenNthCalledWith(
  645. 1,
  646. expect.objectContaining({
  647. query: {
  648. query: `tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  649. },
  650. })
  651. );
  652. });
  653. it('Navigating away from trends will remove extra tags from query', async function () {
  654. const data = initializeTrendsData(
  655. undefined,
  656. {
  657. query: `device.family:Mac tpm():>0.01 transaction.duration:>0 transaction.duration:<${DEFAULT_MAX_DURATION}`,
  658. },
  659. false
  660. );
  661. render(
  662. <TrendsIndex location={data.router.location} organization={data.organization} />,
  663. {
  664. context: data.routerContext,
  665. organization: data.organization,
  666. }
  667. );
  668. (browserHistory.push as any).mockReset();
  669. const byTransactionLink = await screen.findByTestId('breadcrumb-link');
  670. expect(byTransactionLink.closest('a')).toHaveAttribute(
  671. 'href',
  672. '/organizations/org-slug/performance/?query=device.family%3AMac'
  673. );
  674. });
  675. });