widgetBuilderDataset.spec.tsx 41 KB

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