widgetBuilderDataset.spec.tsx 41 KB

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