widgetBuilderDataset.spec.tsx 42 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315
  1. import selectEvent from 'react-select-event';
  2. import {urlEncode} from '@sentry/utils';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
  5. import TagStore from 'sentry/stores/tagStore';
  6. import {
  7. DashboardDetails,
  8. DashboardWidgetSource,
  9. DisplayType,
  10. Widget,
  11. WidgetType,
  12. } from 'sentry/views/dashboardsV2/types';
  13. import WidgetBuilder, {WidgetBuilderProps} from 'sentry/views/dashboardsV2/widgetBuilder';
  14. const defaultOrgFeatures = [
  15. 'performance-view',
  16. 'dashboards-edit',
  17. 'global-views',
  18. 'dashboards-mep',
  19. ];
  20. // Mocking worldMapChart to avoid act warnings
  21. jest.mock('sentry/components/charts/worldMapChart', () => ({
  22. WorldMapChart: () => null,
  23. }));
  24. function mockDashboard(dashboard: Partial<DashboardDetails>): DashboardDetails {
  25. return {
  26. id: '1',
  27. title: 'Dashboard',
  28. createdBy: undefined,
  29. dateCreated: '2020-01-01T00:00:00.000Z',
  30. widgets: [],
  31. projects: [],
  32. filters: {},
  33. ...dashboard,
  34. };
  35. }
  36. function renderTestComponent({
  37. dashboard,
  38. query,
  39. orgFeatures,
  40. onSave,
  41. params,
  42. }: {
  43. dashboard?: WidgetBuilderProps['dashboard'];
  44. onSave?: WidgetBuilderProps['onSave'];
  45. orgFeatures?: string[];
  46. params?: Partial<WidgetBuilderProps['params']>;
  47. query?: Record<string, any>;
  48. } = {}) {
  49. const {organization, router, routerContext} = initializeOrg({
  50. ...initializeOrg(),
  51. organization: {
  52. features: orgFeatures ?? defaultOrgFeatures,
  53. },
  54. router: {
  55. location: {
  56. query: {
  57. source: DashboardWidgetSource.DASHBOARDS,
  58. ...query,
  59. },
  60. },
  61. },
  62. });
  63. render(
  64. <WidgetBuilder
  65. route={{}}
  66. router={router}
  67. routes={router.routes}
  68. routeParams={router.params}
  69. location={router.location}
  70. dashboard={{
  71. id: 'new',
  72. title: 'Dashboard',
  73. createdBy: undefined,
  74. dateCreated: '2020-01-01T00:00:00.000Z',
  75. widgets: [],
  76. projects: [],
  77. filters: {},
  78. ...dashboard,
  79. }}
  80. onSave={onSave ?? jest.fn()}
  81. params={{
  82. orgId: organization.slug,
  83. dashboardId: dashboard?.id ?? 'new',
  84. ...params,
  85. }}
  86. />,
  87. {
  88. context: routerContext,
  89. organization,
  90. }
  91. );
  92. return {router};
  93. }
  94. describe('WidgetBuilder', function () {
  95. const untitledDashboard: DashboardDetails = {
  96. id: '1',
  97. title: 'Untitled Dashboard',
  98. createdBy: undefined,
  99. dateCreated: '2020-01-01T00:00:00.000Z',
  100. widgets: [],
  101. projects: [],
  102. filters: {},
  103. };
  104. const testDashboard: DashboardDetails = {
  105. id: '2',
  106. title: 'Test Dashboard',
  107. createdBy: undefined,
  108. dateCreated: '2020-01-01T00:00:00.000Z',
  109. widgets: [],
  110. projects: [],
  111. filters: {},
  112. };
  113. let eventsMock: jest.Mock | undefined;
  114. let sessionsDataMock: jest.Mock | undefined;
  115. let metricsDataMock: jest.Mock | undefined;
  116. let measurementsMetaMock: jest.Mock | undefined;
  117. beforeEach(function () {
  118. MockApiClient.addMockResponse({
  119. url: '/organizations/org-slug/dashboards/',
  120. body: [
  121. {...untitledDashboard, widgetDisplay: [DisplayType.TABLE]},
  122. {...testDashboard, widgetDisplay: [DisplayType.AREA]},
  123. ],
  124. });
  125. MockApiClient.addMockResponse({
  126. url: '/organizations/org-slug/dashboards/widgets/',
  127. method: 'POST',
  128. statusCode: 200,
  129. body: [],
  130. });
  131. MockApiClient.addMockResponse({
  132. url: '/organizations/org-slug/eventsv2/',
  133. method: 'GET',
  134. statusCode: 200,
  135. body: {
  136. meta: {},
  137. data: [],
  138. },
  139. });
  140. eventsMock = MockApiClient.addMockResponse({
  141. url: '/organizations/org-slug/events/',
  142. method: 'GET',
  143. statusCode: 200,
  144. body: {
  145. meta: {fields: {}},
  146. data: [],
  147. },
  148. });
  149. MockApiClient.addMockResponse({
  150. url: '/organizations/org-slug/projects/',
  151. method: 'GET',
  152. body: [],
  153. });
  154. MockApiClient.addMockResponse({
  155. url: '/organizations/org-slug/recent-searches/',
  156. method: 'GET',
  157. body: [],
  158. });
  159. MockApiClient.addMockResponse({
  160. url: '/organizations/org-slug/recent-searches/',
  161. method: 'POST',
  162. body: [],
  163. });
  164. MockApiClient.addMockResponse({
  165. url: '/organizations/org-slug/issues/',
  166. method: 'GET',
  167. body: [],
  168. });
  169. MockApiClient.addMockResponse({
  170. url: '/organizations/org-slug/events-stats/',
  171. body: [],
  172. });
  173. MockApiClient.addMockResponse({
  174. url: '/organizations/org-slug/tags/event.type/values/',
  175. body: [{count: 2, name: 'Nvidia 1080ti'}],
  176. });
  177. MockApiClient.addMockResponse({
  178. url: '/organizations/org-slug/events-geo/',
  179. body: {data: [], meta: {}},
  180. });
  181. MockApiClient.addMockResponse({
  182. url: '/organizations/org-slug/users/',
  183. body: [],
  184. });
  185. sessionsDataMock = MockApiClient.addMockResponse({
  186. method: 'GET',
  187. url: '/organizations/org-slug/sessions/',
  188. body: TestStubs.SessionsField({
  189. field: `sum(session)`,
  190. }),
  191. });
  192. metricsDataMock = MockApiClient.addMockResponse({
  193. method: 'GET',
  194. url: '/organizations/org-slug/metrics/data/',
  195. body: TestStubs.MetricsField({
  196. field: 'sum(sentry.sessions.session)',
  197. }),
  198. });
  199. MockApiClient.addMockResponse({
  200. url: '/organizations/org-slug/tags/',
  201. method: 'GET',
  202. body: TestStubs.Tags(),
  203. });
  204. measurementsMetaMock = MockApiClient.addMockResponse({
  205. url: '/organizations/org-slug/measurements-meta/',
  206. method: 'GET',
  207. body: {},
  208. });
  209. MockApiClient.addMockResponse({
  210. url: '/organizations/org-slug/tags/is/values/',
  211. method: 'GET',
  212. body: [],
  213. });
  214. MockApiClient.addMockResponse({
  215. url: '/organizations/org-slug/metrics-compatibility/',
  216. method: 'GET',
  217. body: {
  218. incompatible_projects: [],
  219. compatible_projects: [1],
  220. dynamic_sampling_projects: [1],
  221. },
  222. });
  223. MockApiClient.addMockResponse({
  224. url: '/organizations/org-slug/metrics-compatibility-sums/',
  225. method: 'GET',
  226. body: {
  227. sum: {
  228. metrics: 988803,
  229. metrics_null: 0,
  230. metrics_unparam: 132,
  231. },
  232. },
  233. });
  234. TagStore.reset();
  235. });
  236. afterEach(function () {
  237. MockApiClient.clearMockResponses();
  238. jest.clearAllMocks();
  239. jest.useRealTimers();
  240. });
  241. describe('Release Widgets', function () {
  242. const releaseHealthFeatureFlags = [...defaultOrgFeatures, 'dashboards-releases'];
  243. it('does not show the Release Health dataset if there is no dashboards-releases flag', async function () {
  244. renderTestComponent({
  245. orgFeatures: [...defaultOrgFeatures],
  246. });
  247. expect(await screen.findByText('Errors and Transactions')).toBeInTheDocument();
  248. expect(
  249. screen.queryByText('Releases (Sessions, Crash rates)')
  250. ).not.toBeInTheDocument();
  251. });
  252. it('shows the Release Health dataset if there is the dashboards-releases flag', async function () {
  253. renderTestComponent({
  254. orgFeatures: releaseHealthFeatureFlags,
  255. });
  256. expect(await screen.findByText('Errors and Transactions')).toBeInTheDocument();
  257. expect(screen.getByText('Releases (Sessions, Crash rates)')).toBeInTheDocument();
  258. });
  259. it('maintains the selected dataset when display type is changed', async function () {
  260. renderTestComponent({
  261. orgFeatures: releaseHealthFeatureFlags,
  262. });
  263. expect(
  264. await screen.findByText('Releases (Sessions, Crash rates)')
  265. ).toBeInTheDocument();
  266. expect(screen.getByLabelText(/releases/i)).not.toBeChecked();
  267. userEvent.click(screen.getByLabelText(/releases/i));
  268. await waitFor(() => expect(screen.getByLabelText(/releases/i)).toBeChecked());
  269. userEvent.click(screen.getByText('Table'));
  270. userEvent.click(screen.getByText('Line Chart'));
  271. await waitFor(() => expect(screen.getByLabelText(/releases/i)).toBeChecked());
  272. });
  273. it('displays releases tags', async function () {
  274. renderTestComponent({
  275. orgFeatures: releaseHealthFeatureFlags,
  276. });
  277. expect(
  278. await screen.findByText('Releases (Sessions, Crash rates)')
  279. ).toBeInTheDocument();
  280. userEvent.click(screen.getByLabelText(/releases/i));
  281. expect(screen.getByText('crash_free_rate(…)')).toBeInTheDocument();
  282. expect(screen.getByText('session')).toBeInTheDocument();
  283. userEvent.click(screen.getByText('crash_free_rate(…)'));
  284. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  285. expect(screen.getByText('release')).toBeInTheDocument();
  286. expect(screen.getByText('environment')).toBeInTheDocument();
  287. expect(screen.getByText('session.status')).toBeInTheDocument();
  288. userEvent.click(screen.getByText('count_unique(…)'));
  289. expect(screen.getByText('user')).toBeInTheDocument();
  290. });
  291. it('does not display tags as params', async function () {
  292. renderTestComponent({
  293. orgFeatures: releaseHealthFeatureFlags,
  294. });
  295. expect(
  296. await screen.findByText('Releases (Sessions, Crash rates)')
  297. ).toBeInTheDocument();
  298. userEvent.click(screen.getByLabelText(/releases/i));
  299. expect(screen.getByText('crash_free_rate(…)')).toBeInTheDocument();
  300. await selectEvent.select(screen.getByText('crash_free_rate(…)'), 'count_unique(…)');
  301. userEvent.click(screen.getByText('user'));
  302. expect(screen.queryByText('release')).not.toBeInTheDocument();
  303. expect(screen.queryByText('environment')).not.toBeInTheDocument();
  304. expect(screen.queryByText('session.status')).not.toBeInTheDocument();
  305. });
  306. it('does not allow sort by when session.status is selected', async function () {
  307. renderTestComponent({
  308. orgFeatures: releaseHealthFeatureFlags,
  309. });
  310. expect(
  311. await screen.findByText('Releases (Sessions, Crash rates)')
  312. ).toBeInTheDocument();
  313. userEvent.click(screen.getByLabelText(/releases/i));
  314. expect(screen.getByText('High to low')).toBeEnabled();
  315. expect(screen.getByText('crash_free_rate(session)')).toBeInTheDocument();
  316. userEvent.click(screen.getByLabelText('Add a Column'));
  317. await selectEvent.select(screen.getByText('(Required)'), 'session.status');
  318. expect(screen.getByRole('textbox', {name: 'Sort direction'})).toBeDisabled();
  319. expect(screen.getByRole('textbox', {name: 'Sort by'})).toBeDisabled();
  320. });
  321. it('does not allow sort on tags except release', async function () {
  322. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  323. renderTestComponent({
  324. orgFeatures: releaseHealthFeatureFlags,
  325. });
  326. expect(
  327. await screen.findByText('Releases (Sessions, Crash rates)')
  328. ).toBeInTheDocument();
  329. userEvent.click(screen.getByLabelText(/releases/i));
  330. expect(screen.getByText('High to low')).toBeEnabled();
  331. expect(screen.getByText('crash_free_rate(session)')).toBeInTheDocument();
  332. userEvent.click(screen.getByLabelText('Add a Column'));
  333. await selectEvent.select(screen.getByText('(Required)'), 'release');
  334. userEvent.click(screen.getByLabelText('Add a Column'));
  335. await selectEvent.select(screen.getByText('(Required)'), 'environment');
  336. expect(await screen.findByText('Sort by a column')).toBeInTheDocument();
  337. // Selector "sortDirection"
  338. expect(screen.getByText('High to low')).toBeInTheDocument();
  339. // Selector "sortBy"
  340. userEvent.click(screen.getAllByText('crash_free_rate(session)')[1]);
  341. // release exists in sort by selector
  342. expect(screen.getAllByText('release')).toHaveLength(3);
  343. // environment does not exist in sort by selector
  344. expect(screen.getAllByText('environment')).toHaveLength(2);
  345. });
  346. it('makes the appropriate sessions call', async function () {
  347. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  348. renderTestComponent({
  349. orgFeatures: releaseHealthFeatureFlags,
  350. });
  351. expect(
  352. await screen.findByText('Releases (Sessions, Crash rates)')
  353. ).toBeInTheDocument();
  354. userEvent.click(screen.getByLabelText(/releases/i));
  355. userEvent.click(screen.getByText('Table'));
  356. userEvent.click(screen.getByText('Line Chart'));
  357. await waitFor(() =>
  358. expect(metricsDataMock).toHaveBeenLastCalledWith(
  359. `/organizations/org-slug/metrics/data/`,
  360. expect.objectContaining({
  361. query: expect.objectContaining({
  362. environment: [],
  363. field: [`session.crash_free_rate`],
  364. groupBy: [],
  365. interval: '5m',
  366. project: [],
  367. statsPeriod: '24h',
  368. }),
  369. })
  370. )
  371. );
  372. });
  373. it('calls the session endpoint with the right limit', async function () {
  374. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  375. renderTestComponent({
  376. orgFeatures: releaseHealthFeatureFlags,
  377. });
  378. expect(
  379. await screen.findByText('Releases (Sessions, Crash rates)')
  380. ).toBeInTheDocument();
  381. userEvent.click(screen.getByLabelText(/releases/i));
  382. userEvent.click(screen.getByText('Table'));
  383. userEvent.click(screen.getByText('Line Chart'));
  384. await selectEvent.select(await screen.findByText('Select group'), 'project');
  385. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  386. await waitFor(() =>
  387. expect(metricsDataMock).toHaveBeenLastCalledWith(
  388. `/organizations/org-slug/metrics/data/`,
  389. expect.objectContaining({
  390. query: expect.objectContaining({
  391. environment: [],
  392. field: ['session.crash_free_rate'],
  393. groupBy: ['project_id'],
  394. interval: '5m',
  395. orderBy: '-session.crash_free_rate',
  396. per_page: 5,
  397. project: [],
  398. statsPeriod: '24h',
  399. }),
  400. })
  401. )
  402. );
  403. });
  404. it('calls sessions api when session.status is selected as a groupby', async function () {
  405. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  406. renderTestComponent({
  407. orgFeatures: releaseHealthFeatureFlags,
  408. });
  409. expect(
  410. await screen.findByText('Releases (Sessions, Crash rates)')
  411. ).toBeInTheDocument();
  412. userEvent.click(screen.getByLabelText(/releases/i));
  413. userEvent.click(screen.getByText('Table'));
  414. userEvent.click(screen.getByText('Line Chart'));
  415. await selectEvent.select(await screen.findByText('Select group'), 'session.status');
  416. expect(screen.getByText('Limit to 5 results')).toBeInTheDocument();
  417. await waitFor(() =>
  418. expect(sessionsDataMock).toHaveBeenLastCalledWith(
  419. `/organizations/org-slug/sessions/`,
  420. expect.objectContaining({
  421. query: expect.objectContaining({
  422. environment: [],
  423. field: ['crash_free_rate(session)'],
  424. groupBy: ['session.status'],
  425. interval: '5m',
  426. project: [],
  427. statsPeriod: '24h',
  428. }),
  429. })
  430. )
  431. );
  432. });
  433. it('displays the correct options for area chart', async function () {
  434. renderTestComponent({
  435. orgFeatures: releaseHealthFeatureFlags,
  436. });
  437. expect(
  438. await screen.findByText('Releases (Sessions, Crash rates)')
  439. ).toBeInTheDocument();
  440. // change dataset to releases
  441. userEvent.click(screen.getByLabelText(/releases/i));
  442. userEvent.click(screen.getByText('Table'));
  443. userEvent.click(screen.getByText('Line Chart'));
  444. expect(screen.getByText('crash_free_rate(…)')).toBeInTheDocument();
  445. expect(screen.getByText(`session`)).toBeInTheDocument();
  446. userEvent.click(screen.getByText('crash_free_rate(…)'));
  447. expect(screen.getByText('count_unique(…)')).toBeInTheDocument();
  448. userEvent.click(screen.getByText('count_unique(…)'));
  449. expect(screen.getByText('user')).toBeInTheDocument();
  450. });
  451. it('sets widgetType to release', async function () {
  452. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  453. renderTestComponent({
  454. orgFeatures: releaseHealthFeatureFlags,
  455. });
  456. userEvent.click(await screen.findByText('Releases (Sessions, Crash rates)'));
  457. expect(metricsDataMock).toHaveBeenCalled();
  458. expect(screen.getByLabelText(/Releases/i)).toBeChecked();
  459. });
  460. it('does not display "add an equation" button', async function () {
  461. const widget: Widget = {
  462. title: 'Release Widget',
  463. displayType: DisplayType.TABLE,
  464. widgetType: WidgetType.RELEASE,
  465. queries: [
  466. {
  467. name: 'errors',
  468. conditions: 'event.type:error',
  469. fields: ['sdk.name', 'count()'],
  470. columns: ['sdk.name'],
  471. aggregates: ['count()'],
  472. orderby: '-sdk.name',
  473. },
  474. ],
  475. interval: '1d',
  476. id: '1',
  477. };
  478. const dashboard = mockDashboard({widgets: [widget]});
  479. renderTestComponent({
  480. orgFeatures: releaseHealthFeatureFlags,
  481. dashboard,
  482. params: {
  483. widgetIndex: '0',
  484. },
  485. });
  486. // Select line chart display
  487. userEvent.click(await screen.findByText('Table'));
  488. userEvent.click(screen.getByText('Line Chart'));
  489. await waitFor(() =>
  490. expect(screen.queryByLabelText('Add an Equation')).not.toBeInTheDocument()
  491. );
  492. });
  493. it('render release dataset disabled when the display type is world map', async function () {
  494. renderTestComponent({
  495. query: {
  496. source: DashboardWidgetSource.DISCOVERV2,
  497. },
  498. orgFeatures: releaseHealthFeatureFlags,
  499. });
  500. userEvent.click(await screen.findByText('Table'));
  501. userEvent.click(screen.getByText('World Map'));
  502. await waitFor(() => expect(screen.getByLabelText(/Releases/)).toBeDisabled());
  503. expect(
  504. screen.getByRole('radio', {
  505. name: 'Select Errors and Transactions',
  506. })
  507. ).toBeEnabled();
  508. expect(
  509. screen.getByRole('radio', {
  510. name: 'Select Issues (States, Assignment, Time, etc.)',
  511. })
  512. ).toBeDisabled();
  513. });
  514. it('renders with a release search bar', async function () {
  515. renderTestComponent({
  516. orgFeatures: releaseHealthFeatureFlags,
  517. });
  518. userEvent.type(
  519. await screen.findByPlaceholderText('Search for events, users, tags, and more'),
  520. 'session.status:'
  521. );
  522. await waitFor(() => {
  523. expect(screen.getByText("isn't supported here.")).toBeInTheDocument();
  524. });
  525. userEvent.click(screen.getByText('Releases (Sessions, Crash rates)'));
  526. userEvent.click(
  527. screen.getByPlaceholderText(
  528. 'Search for release version, session status, and more'
  529. )
  530. );
  531. expect(await screen.findByText('environment')).toBeInTheDocument();
  532. expect(screen.getByText('project')).toBeInTheDocument();
  533. expect(screen.getByText('release')).toBeInTheDocument();
  534. });
  535. it('adds a function when the only column chosen in a table is a tag', async function () {
  536. jest.useFakeTimers().setSystemTime(new Date('2022-08-02'));
  537. renderTestComponent({
  538. orgFeatures: releaseHealthFeatureFlags,
  539. });
  540. userEvent.click(await screen.findByText('Releases (Sessions, Crash rates)'));
  541. await selectEvent.select(screen.getByText('crash_free_rate(…)'), 'environment');
  542. // 1 in the table header, 1 in the column selector, and 1 in the sort by
  543. expect(screen.getAllByText(/crash_free_rate/)).toHaveLength(3);
  544. expect(screen.getAllByText('environment')).toHaveLength(2);
  545. });
  546. });
  547. describe('Issue Widgets', function () {
  548. it('sets widgetType to issues', async function () {
  549. const handleSave = jest.fn();
  550. renderTestComponent({onSave: handleSave});
  551. userEvent.click(await screen.findByText('Issues (States, Assignment, Time, etc.)'));
  552. userEvent.click(screen.getByLabelText('Add Widget'));
  553. await waitFor(() => {
  554. expect(handleSave).toHaveBeenCalledWith([
  555. expect.objectContaining({
  556. title: 'Custom Widget',
  557. displayType: DisplayType.TABLE,
  558. interval: '5m',
  559. widgetType: WidgetType.ISSUE,
  560. queries: [
  561. {
  562. conditions: '',
  563. fields: ['issue', 'assignee', 'title'],
  564. columns: ['issue', 'assignee', 'title'],
  565. aggregates: [],
  566. fieldAliases: [],
  567. name: '',
  568. orderby: 'date',
  569. },
  570. ],
  571. }),
  572. ]);
  573. });
  574. expect(handleSave).toHaveBeenCalledTimes(1);
  575. });
  576. it('render issues dataset disabled when the display type is not set to table', async function () {
  577. renderTestComponent({
  578. query: {
  579. source: DashboardWidgetSource.DISCOVERV2,
  580. },
  581. });
  582. userEvent.click(await screen.findByText('Table'));
  583. userEvent.click(screen.getByText('Line Chart'));
  584. expect(
  585. screen.getByRole('radio', {
  586. name: 'Select Errors and Transactions',
  587. })
  588. ).toBeEnabled();
  589. expect(
  590. screen.getByRole('radio', {
  591. name: 'Select Issues (States, Assignment, Time, etc.)',
  592. })
  593. ).toBeDisabled();
  594. });
  595. it('disables moving and deleting issue column', async function () {
  596. renderTestComponent();
  597. userEvent.click(await screen.findByText('Issues (States, Assignment, Time, etc.)'));
  598. expect(screen.getByText('issue')).toBeInTheDocument();
  599. expect(screen.getByText('assignee')).toBeInTheDocument();
  600. expect(screen.getByText('title')).toBeInTheDocument();
  601. expect(screen.getAllByLabelText('Remove column')).toHaveLength(2);
  602. expect(screen.getAllByLabelText('Drag to reorder')).toHaveLength(3);
  603. userEvent.click(screen.getAllByLabelText('Remove column')[1]);
  604. userEvent.click(screen.getAllByLabelText('Remove column')[0]);
  605. expect(screen.getByText('issue')).toBeInTheDocument();
  606. expect(screen.queryByText('assignee')).not.toBeInTheDocument();
  607. expect(screen.queryByText('title')).not.toBeInTheDocument();
  608. expect(screen.queryByLabelText('Remove column')).not.toBeInTheDocument();
  609. expect(screen.queryByLabelText('Drag to reorder')).not.toBeInTheDocument();
  610. });
  611. it('issue query does not work on default search bar', async function () {
  612. renderTestComponent();
  613. const input = (await screen.findByPlaceholderText(
  614. 'Search for events, users, tags, and more'
  615. )) as HTMLTextAreaElement;
  616. userEvent.paste(input, 'bookmarks', {
  617. clipboardData: {getData: () => ''},
  618. } as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
  619. input.setSelectionRange(9, 9);
  620. expect(await screen.findByText('No items found')).toBeInTheDocument();
  621. });
  622. it('renders with an issues search bar when selected in dataset selection', async function () {
  623. renderTestComponent();
  624. userEvent.click(await screen.findByText('Issues (States, Assignment, Time, etc.)'));
  625. const input = (await screen.findByPlaceholderText(
  626. 'Search for issues, status, assigned, and more'
  627. )) as HTMLTextAreaElement;
  628. userEvent.paste(input, 'is:', {
  629. clipboardData: {getData: () => ''},
  630. } as unknown as React.ClipboardEvent<HTMLTextAreaElement>);
  631. input.setSelectionRange(3, 3);
  632. expect(await screen.findByText('resolved')).toBeInTheDocument();
  633. });
  634. it('Update table header values (field alias)', async function () {
  635. const handleSave = jest.fn();
  636. renderTestComponent({
  637. onSave: handleSave,
  638. orgFeatures: [...defaultOrgFeatures],
  639. });
  640. await screen.findByText('Table');
  641. userEvent.click(screen.getByText('Issues (States, Assignment, Time, etc.)'));
  642. userEvent.paste(screen.getAllByPlaceholderText('Alias')[0], 'First Alias');
  643. userEvent.click(screen.getByText('Add Widget'));
  644. await waitFor(() => {
  645. expect(handleSave).toHaveBeenCalledWith([
  646. expect.objectContaining({
  647. queries: [
  648. expect.objectContaining({
  649. fieldAliases: ['First Alias', '', ''],
  650. }),
  651. ],
  652. }),
  653. ]);
  654. });
  655. });
  656. });
  657. describe('Events Widgets', function () {
  658. describe('Custom Performance Metrics', function () {
  659. it('can choose a custom measurement', async function () {
  660. measurementsMetaMock = MockApiClient.addMockResponse({
  661. url: '/organizations/org-slug/measurements-meta/',
  662. method: 'GET',
  663. body: {'measurements.custom.measurement': {functions: ['p99']}},
  664. });
  665. eventsMock = MockApiClient.addMockResponse({
  666. url: '/organizations/org-slug/events/',
  667. method: 'GET',
  668. statusCode: 200,
  669. body: {
  670. meta: {
  671. fields: {'p99(measurements.total.db.calls)': 'duration'},
  672. isMetricsData: true,
  673. },
  674. data: [{'p99(measurements.total.db.calls)': 10}],
  675. },
  676. });
  677. const {router} = renderTestComponent({
  678. query: {source: DashboardWidgetSource.DISCOVERV2},
  679. dashboard: testDashboard,
  680. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  681. });
  682. expect(await screen.findAllByText('Custom Widget')).toHaveLength(2);
  683. // 1 in the table header, 1 in the column selector, 1 in the sort field
  684. const countFields = screen.getAllByText('count()');
  685. expect(countFields).toHaveLength(3);
  686. await selectEvent.select(countFields[1], ['p99(…)']);
  687. await selectEvent.select(screen.getByText('transaction.duration'), [
  688. 'measurements.custom.measurement',
  689. ]);
  690. userEvent.click(screen.getByText('Add Widget'));
  691. await waitFor(() => {
  692. expect(router.push).toHaveBeenCalledWith(
  693. expect.objectContaining({
  694. pathname: '/organizations/org-slug/dashboard/2/',
  695. query: {
  696. displayType: 'table',
  697. interval: '5m',
  698. title: 'Custom Widget',
  699. queryNames: [''],
  700. queryConditions: [''],
  701. queryFields: ['p99(measurements.custom.measurement)'],
  702. queryOrderby: '-p99(measurements.custom.measurement)',
  703. start: null,
  704. end: null,
  705. statsPeriod: '24h',
  706. utc: null,
  707. project: [],
  708. environment: [],
  709. },
  710. })
  711. );
  712. });
  713. });
  714. it('raises an alert banner but allows saving widget if widget result is not metrics data and widget is using custom measurements', async function () {
  715. eventsMock = MockApiClient.addMockResponse({
  716. url: '/organizations/org-slug/events/',
  717. method: 'GET',
  718. statusCode: 200,
  719. body: {
  720. meta: {
  721. fields: {'p99(measurements.custom.measurement)': 'duration'},
  722. isMetricsData: false,
  723. },
  724. data: [{'p99(measurements.custom.measurement)': 10}],
  725. },
  726. });
  727. const defaultWidgetQuery = {
  728. name: '',
  729. fields: ['p99(measurements.custom.measurement)'],
  730. columns: [],
  731. aggregates: ['p99(measurements.custom.measurement)'],
  732. conditions: 'user:test.user@sentry.io',
  733. orderby: '',
  734. };
  735. const defaultTableColumns = ['p99(measurements.custom.measurement)'];
  736. renderTestComponent({
  737. query: {
  738. source: DashboardWidgetSource.DISCOVERV2,
  739. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  740. displayType: DisplayType.TABLE,
  741. defaultTableColumns,
  742. },
  743. orgFeatures: [
  744. ...defaultOrgFeatures,
  745. 'discover-frontend-use-events-endpoint',
  746. 'dashboards-mep',
  747. 'server-side-sampling',
  748. 'mep-rollout-flag',
  749. ],
  750. });
  751. await waitFor(() => {
  752. expect(measurementsMetaMock).toHaveBeenCalled();
  753. });
  754. await waitFor(() => {
  755. expect(eventsMock).toHaveBeenCalled();
  756. });
  757. screen.getByText('Your selection is only applicable to');
  758. expect(screen.getByText('Add Widget').closest('button')).toBeEnabled();
  759. });
  760. it('raises an alert banner if widget result is not metrics data', async function () {
  761. eventsMock = MockApiClient.addMockResponse({
  762. url: '/organizations/org-slug/events/',
  763. method: 'GET',
  764. statusCode: 200,
  765. body: {
  766. meta: {
  767. fields: {'p99(measurements.lcp)': 'duration'},
  768. isMetricsData: false,
  769. },
  770. data: [{'p99(measurements.lcp)': 10}],
  771. },
  772. });
  773. const defaultWidgetQuery = {
  774. name: '',
  775. fields: ['p99(measurements.lcp)'],
  776. columns: [],
  777. aggregates: ['p99(measurements.lcp)'],
  778. conditions: 'user:test.user@sentry.io',
  779. orderby: '',
  780. };
  781. const defaultTableColumns = ['p99(measurements.lcp)'];
  782. renderTestComponent({
  783. query: {
  784. source: DashboardWidgetSource.DISCOVERV2,
  785. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  786. displayType: DisplayType.TABLE,
  787. defaultTableColumns,
  788. },
  789. orgFeatures: [
  790. ...defaultOrgFeatures,
  791. 'discover-frontend-use-events-endpoint',
  792. 'dashboards-mep',
  793. 'server-side-sampling',
  794. 'mep-rollout-flag',
  795. ],
  796. });
  797. await waitFor(() => {
  798. expect(measurementsMetaMock).toHaveBeenCalled();
  799. });
  800. await waitFor(() => {
  801. expect(eventsMock).toHaveBeenCalled();
  802. });
  803. screen.getByText('Your selection is only applicable to');
  804. });
  805. it('does not raise an alert banner if widget result is not metrics data but widget contains error fields', async function () {
  806. eventsMock = MockApiClient.addMockResponse({
  807. url: '/organizations/org-slug/events/',
  808. method: 'GET',
  809. statusCode: 200,
  810. body: {
  811. meta: {
  812. fields: {'p99(measurements.lcp)': 'duration'},
  813. isMetricsData: false,
  814. },
  815. data: [{'p99(measurements.lcp)': 10}],
  816. },
  817. });
  818. const defaultWidgetQuery = {
  819. name: '',
  820. fields: ['p99(measurements.lcp)'],
  821. columns: ['error.handled'],
  822. aggregates: ['p99(measurements.lcp)'],
  823. conditions: 'user:test.user@sentry.io',
  824. orderby: '',
  825. };
  826. const defaultTableColumns = ['p99(measurements.lcp)'];
  827. renderTestComponent({
  828. query: {
  829. source: DashboardWidgetSource.DISCOVERV2,
  830. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  831. displayType: DisplayType.TABLE,
  832. defaultTableColumns,
  833. },
  834. orgFeatures: [
  835. ...defaultOrgFeatures,
  836. 'discover-frontend-use-events-endpoint',
  837. 'dashboards-mep',
  838. ],
  839. });
  840. await waitFor(() => {
  841. expect(measurementsMetaMock).toHaveBeenCalled();
  842. });
  843. await waitFor(() => {
  844. expect(eventsMock).toHaveBeenCalled();
  845. });
  846. expect(
  847. screen.queryByText('Your selection is only applicable to')
  848. ).not.toBeInTheDocument();
  849. });
  850. it('only displays custom measurements in supported functions', async function () {
  851. measurementsMetaMock = MockApiClient.addMockResponse({
  852. url: '/organizations/org-slug/measurements-meta/',
  853. method: 'GET',
  854. body: {
  855. 'measurements.custom.measurement': {functions: ['p99']},
  856. 'measurements.another.custom.measurement': {functions: ['p95']},
  857. },
  858. });
  859. renderTestComponent({
  860. query: {source: DashboardWidgetSource.DISCOVERV2},
  861. dashboard: testDashboard,
  862. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  863. });
  864. expect(await screen.findAllByText('Custom Widget')).toHaveLength(2);
  865. await selectEvent.select(screen.getAllByText('count()')[1], ['p99(…)']);
  866. userEvent.click(screen.getByText('transaction.duration'));
  867. screen.getByText('measurements.custom.measurement');
  868. expect(
  869. screen.queryByText('measurements.another.custom.measurement')
  870. ).not.toBeInTheDocument();
  871. await selectEvent.select(screen.getAllByText('p99(…)')[0], ['p95(…)']);
  872. userEvent.click(screen.getByText('transaction.duration'));
  873. screen.getByText('measurements.another.custom.measurement');
  874. expect(
  875. screen.queryByText('measurements.custom.measurement')
  876. ).not.toBeInTheDocument();
  877. });
  878. it('renders custom performance metric using duration units from events meta', async function () {
  879. eventsMock = MockApiClient.addMockResponse({
  880. url: '/organizations/org-slug/events/',
  881. method: 'GET',
  882. statusCode: 200,
  883. body: {
  884. meta: {
  885. fields: {'p99(measurements.custom.measurement)': 'duration'},
  886. isMetricsData: true,
  887. units: {'p99(measurements.custom.measurement)': 'hour'},
  888. },
  889. data: [{'p99(measurements.custom.measurement)': 12}],
  890. },
  891. });
  892. renderTestComponent({
  893. query: {source: DashboardWidgetSource.DISCOVERV2},
  894. dashboard: {
  895. ...testDashboard,
  896. widgets: [
  897. {
  898. title: 'Custom Measurement Widget',
  899. interval: '1d',
  900. id: '1',
  901. widgetType: WidgetType.DISCOVER,
  902. displayType: DisplayType.TABLE,
  903. queries: [
  904. {
  905. conditions: '',
  906. name: '',
  907. fields: ['p99(measurements.custom.measurement)'],
  908. columns: [],
  909. aggregates: ['p99(measurements.custom.measurement)'],
  910. orderby: '-p99(measurements.custom.measurement)',
  911. },
  912. ],
  913. },
  914. ],
  915. },
  916. params: {
  917. widgetIndex: '0',
  918. },
  919. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  920. });
  921. await screen.findByText('12.00hr');
  922. });
  923. it('renders custom performance metric using size units from events meta', async function () {
  924. eventsMock = MockApiClient.addMockResponse({
  925. url: '/organizations/org-slug/events/',
  926. method: 'GET',
  927. statusCode: 200,
  928. body: {
  929. meta: {
  930. fields: {'p99(measurements.custom.measurement)': 'size'},
  931. isMetricsData: true,
  932. units: {'p99(measurements.custom.measurement)': 'kibibyte'},
  933. },
  934. data: [{'p99(measurements.custom.measurement)': 12}],
  935. },
  936. });
  937. renderTestComponent({
  938. query: {source: DashboardWidgetSource.DISCOVERV2},
  939. dashboard: {
  940. ...testDashboard,
  941. widgets: [
  942. {
  943. title: 'Custom Measurement Widget',
  944. interval: '1d',
  945. id: '1',
  946. widgetType: WidgetType.DISCOVER,
  947. displayType: DisplayType.TABLE,
  948. queries: [
  949. {
  950. conditions: '',
  951. name: '',
  952. fields: ['p99(measurements.custom.measurement)'],
  953. columns: [],
  954. aggregates: ['p99(measurements.custom.measurement)'],
  955. orderby: '-p99(measurements.custom.measurement)',
  956. },
  957. ],
  958. },
  959. ],
  960. },
  961. params: {
  962. widgetIndex: '0',
  963. },
  964. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  965. });
  966. await screen.findByText('12.0 KiB');
  967. });
  968. it('renders custom performance metric using abyte format size units from events meta', async function () {
  969. eventsMock = MockApiClient.addMockResponse({
  970. url: '/organizations/org-slug/events/',
  971. method: 'GET',
  972. statusCode: 200,
  973. body: {
  974. meta: {
  975. fields: {'p99(measurements.custom.measurement)': 'size'},
  976. isMetricsData: true,
  977. units: {'p99(measurements.custom.measurement)': 'kilobyte'},
  978. },
  979. data: [{'p99(measurements.custom.measurement)': 12000}],
  980. },
  981. });
  982. renderTestComponent({
  983. query: {source: DashboardWidgetSource.DISCOVERV2},
  984. dashboard: {
  985. ...testDashboard,
  986. widgets: [
  987. {
  988. title: 'Custom Measurement Widget',
  989. interval: '1d',
  990. id: '1',
  991. widgetType: WidgetType.DISCOVER,
  992. displayType: DisplayType.TABLE,
  993. queries: [
  994. {
  995. conditions: '',
  996. name: '',
  997. fields: ['p99(measurements.custom.measurement)'],
  998. columns: [],
  999. aggregates: ['p99(measurements.custom.measurement)'],
  1000. orderby: '-p99(measurements.custom.measurement)',
  1001. },
  1002. ],
  1003. },
  1004. ],
  1005. },
  1006. params: {
  1007. widgetIndex: '0',
  1008. },
  1009. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  1010. });
  1011. await screen.findByText('12 MB');
  1012. });
  1013. it('displays saved custom performance metric in column select', async function () {
  1014. renderTestComponent({
  1015. query: {source: DashboardWidgetSource.DISCOVERV2},
  1016. dashboard: {
  1017. ...testDashboard,
  1018. widgets: [
  1019. {
  1020. title: 'Custom Measurement Widget',
  1021. interval: '1d',
  1022. id: '1',
  1023. widgetType: WidgetType.DISCOVER,
  1024. displayType: DisplayType.TABLE,
  1025. queries: [
  1026. {
  1027. conditions: '',
  1028. name: '',
  1029. fields: ['p99(measurements.custom.measurement)'],
  1030. columns: [],
  1031. aggregates: ['p99(measurements.custom.measurement)'],
  1032. orderby: '-p99(measurements.custom.measurement)',
  1033. },
  1034. ],
  1035. },
  1036. ],
  1037. },
  1038. params: {
  1039. widgetIndex: '0',
  1040. },
  1041. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  1042. });
  1043. await screen.findByText('measurements.custom.measurement');
  1044. });
  1045. it('displays custom performance metric in column select dropdown', async function () {
  1046. measurementsMetaMock = MockApiClient.addMockResponse({
  1047. url: '/organizations/org-slug/measurements-meta/',
  1048. method: 'GET',
  1049. body: {'measurements.custom.measurement': {functions: ['p99']}},
  1050. });
  1051. renderTestComponent({
  1052. query: {source: DashboardWidgetSource.DISCOVERV2},
  1053. dashboard: {
  1054. ...testDashboard,
  1055. widgets: [
  1056. {
  1057. title: 'Custom Measurement Widget',
  1058. interval: '1d',
  1059. id: '1',
  1060. widgetType: WidgetType.DISCOVER,
  1061. displayType: DisplayType.TABLE,
  1062. queries: [
  1063. {
  1064. conditions: '',
  1065. name: '',
  1066. fields: ['transaction', 'count()'],
  1067. columns: ['transaction'],
  1068. aggregates: ['count()'],
  1069. orderby: '-count()',
  1070. },
  1071. ],
  1072. },
  1073. ],
  1074. },
  1075. params: {
  1076. widgetIndex: '0',
  1077. },
  1078. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  1079. });
  1080. await screen.findByText('transaction');
  1081. userEvent.click(screen.getAllByText('count()')[1]);
  1082. expect(screen.getByText('measurements.custom.measurement')).toBeInTheDocument();
  1083. });
  1084. it('does not default to sorting by transaction when columns change', async function () {
  1085. renderTestComponent({
  1086. query: {source: DashboardWidgetSource.DISCOVERV2},
  1087. dashboard: {
  1088. ...testDashboard,
  1089. widgets: [
  1090. {
  1091. title: 'Custom Measurement Widget',
  1092. interval: '1d',
  1093. id: '1',
  1094. widgetType: WidgetType.DISCOVER,
  1095. displayType: DisplayType.TABLE,
  1096. queries: [
  1097. {
  1098. conditions: '',
  1099. name: '',
  1100. fields: [
  1101. 'p99(measurements.custom.measurement)',
  1102. 'transaction',
  1103. 'count()',
  1104. ],
  1105. columns: ['transaction'],
  1106. aggregates: ['p99(measurements.custom.measurement)', 'count()'],
  1107. orderby: '-p99(measurements.custom.measurement)',
  1108. },
  1109. ],
  1110. },
  1111. ],
  1112. },
  1113. params: {
  1114. widgetIndex: '0',
  1115. },
  1116. orgFeatures: [...defaultOrgFeatures, 'discover-frontend-use-events-endpoint'],
  1117. });
  1118. expect(
  1119. await screen.findByText('p99(measurements.custom.measurement)')
  1120. ).toBeInTheDocument();
  1121. // Delete p99(measurements.custom.measurement) column
  1122. userEvent.click(screen.getAllByLabelText('Remove column')[0]);
  1123. expect(
  1124. screen.queryByText('p99(measurements.custom.measurement)')
  1125. ).not.toBeInTheDocument();
  1126. expect(screen.getAllByText('transaction').length).toEqual(1);
  1127. expect(screen.getAllByText('count()').length).toEqual(2);
  1128. });
  1129. });
  1130. });
  1131. });