utils.spec.tsx 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266
  1. import type {Location} from 'history';
  2. import {EventFixture} from 'sentry-fixture/event';
  3. import {LocationFixture} from 'sentry-fixture/locationFixture';
  4. import {OrganizationFixture} from 'sentry-fixture/organization';
  5. import {RouterFixture} from 'sentry-fixture/routerFixture';
  6. import {initializeOrg} from 'sentry-test/initializeOrg';
  7. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  8. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  9. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  10. import type {Organization} from 'sentry/types/organization';
  11. import type {EventViewOptions} from 'sentry/utils/discover/eventView';
  12. import EventView from 'sentry/utils/discover/eventView';
  13. import {DisplayModes} from 'sentry/utils/discover/types';
  14. import {DisplayType, WidgetType} from 'sentry/views/dashboards/types';
  15. import {
  16. constructAddQueryToDashboardLink,
  17. decodeColumnOrder,
  18. downloadAsCsv,
  19. eventViewToWidgetQuery,
  20. generateFieldOptions,
  21. getExpandedResults,
  22. handleAddQueryToDashboard,
  23. pushEventViewToLocation,
  24. } from 'sentry/views/discover/utils';
  25. jest.mock('sentry/actionCreators/modal');
  26. const baseView: EventViewOptions = {
  27. display: undefined,
  28. start: undefined,
  29. end: undefined,
  30. id: '0',
  31. name: undefined,
  32. fields: [],
  33. createdBy: undefined,
  34. environment: [],
  35. project: [],
  36. query: '',
  37. sorts: [],
  38. statsPeriod: undefined,
  39. team: [],
  40. topEvents: undefined,
  41. };
  42. describe('decodeColumnOrder', function () {
  43. it('can decode 0 elements', function () {
  44. const results = decodeColumnOrder([]);
  45. expect(Array.isArray(results)).toBeTruthy();
  46. expect(results).toHaveLength(0);
  47. });
  48. it('can decode fields', function () {
  49. const results = decodeColumnOrder([{field: 'title', width: 123}]);
  50. expect(Array.isArray(results)).toBeTruthy();
  51. expect(results[0]).toEqual({
  52. key: 'title',
  53. name: 'title',
  54. column: {
  55. kind: 'field',
  56. field: 'title',
  57. },
  58. width: 123,
  59. isSortable: false,
  60. type: 'string',
  61. });
  62. });
  63. it('can decode measurement fields', function () {
  64. const results = decodeColumnOrder([{field: 'measurements.foo', width: 123}]);
  65. expect(Array.isArray(results)).toBeTruthy();
  66. expect(results[0]).toEqual({
  67. key: 'measurements.foo',
  68. name: 'measurements.foo',
  69. column: {
  70. kind: 'field',
  71. field: 'measurements.foo',
  72. },
  73. width: 123,
  74. isSortable: false,
  75. type: 'number',
  76. });
  77. });
  78. it('can decode span op breakdown fields', function () {
  79. const results = decodeColumnOrder([{field: 'spans.foo', width: 123}]);
  80. expect(Array.isArray(results)).toBeTruthy();
  81. expect(results[0]).toEqual({
  82. key: 'spans.foo',
  83. name: 'spans.foo',
  84. column: {
  85. kind: 'field',
  86. field: 'spans.foo',
  87. },
  88. width: 123,
  89. isSortable: false,
  90. type: 'duration',
  91. });
  92. });
  93. it('can decode aggregate functions with no arguments', function () {
  94. let results = decodeColumnOrder([{field: 'count()', width: 123}]);
  95. expect(Array.isArray(results)).toBeTruthy();
  96. expect(results[0]).toEqual({
  97. key: 'count()',
  98. name: 'count()',
  99. column: {
  100. kind: 'function',
  101. function: ['count', '', undefined, undefined],
  102. },
  103. width: 123,
  104. isSortable: true,
  105. type: 'number',
  106. });
  107. results = decodeColumnOrder([{field: 'p75()', width: 123}]);
  108. expect(results[0]!.type).toBe('duration');
  109. results = decodeColumnOrder([{field: 'p99()', width: 123}]);
  110. expect(results[0]!.type).toBe('duration');
  111. });
  112. it('can decode elements with aggregate functions with arguments', function () {
  113. const results = decodeColumnOrder([{field: 'avg(transaction.duration)'}]);
  114. expect(Array.isArray(results)).toBeTruthy();
  115. expect(results[0]).toEqual({
  116. key: 'avg(transaction.duration)',
  117. name: 'avg(transaction.duration)',
  118. column: {
  119. kind: 'function',
  120. function: ['avg', 'transaction.duration', undefined, undefined],
  121. },
  122. width: COL_WIDTH_UNDEFINED,
  123. isSortable: true,
  124. type: 'duration',
  125. });
  126. });
  127. it('can decode elements with aggregate functions with multiple arguments', function () {
  128. const results = decodeColumnOrder([
  129. {field: 'percentile(transaction.duration, 0.65)'},
  130. ]);
  131. expect(Array.isArray(results)).toBeTruthy();
  132. expect(results[0]).toEqual({
  133. key: 'percentile(transaction.duration, 0.65)',
  134. name: 'percentile(transaction.duration, 0.65)',
  135. column: {
  136. kind: 'function',
  137. function: ['percentile', 'transaction.duration', '0.65', undefined],
  138. },
  139. width: COL_WIDTH_UNDEFINED,
  140. isSortable: true,
  141. type: 'duration',
  142. });
  143. });
  144. it('can decode elements with aggregate functions using measurements', function () {
  145. const results = decodeColumnOrder([{field: 'avg(measurements.foo)'}]);
  146. expect(Array.isArray(results)).toBeTruthy();
  147. expect(results[0]).toEqual({
  148. key: 'avg(measurements.foo)',
  149. name: 'avg(measurements.foo)',
  150. column: {
  151. kind: 'function',
  152. function: ['avg', 'measurements.foo', undefined, undefined],
  153. },
  154. width: COL_WIDTH_UNDEFINED,
  155. isSortable: true,
  156. type: 'number',
  157. });
  158. });
  159. it('can decode elements with aggregate functions with multiple arguments using measurements', function () {
  160. const results = decodeColumnOrder([{field: 'percentile(measurements.lcp, 0.65)'}]);
  161. expect(Array.isArray(results)).toBeTruthy();
  162. expect(results[0]).toEqual({
  163. key: 'percentile(measurements.lcp, 0.65)',
  164. name: 'percentile(measurements.lcp, 0.65)',
  165. column: {
  166. kind: 'function',
  167. function: ['percentile', 'measurements.lcp', '0.65', undefined],
  168. },
  169. width: COL_WIDTH_UNDEFINED,
  170. isSortable: true,
  171. type: 'duration',
  172. });
  173. });
  174. it('can decode elements with aggregate functions using span op breakdowns', function () {
  175. const results = decodeColumnOrder([{field: 'avg(spans.foo)'}]);
  176. expect(Array.isArray(results)).toBeTruthy();
  177. expect(results[0]).toEqual({
  178. key: 'avg(spans.foo)',
  179. name: 'avg(spans.foo)',
  180. column: {
  181. kind: 'function',
  182. function: ['avg', 'spans.foo', undefined, undefined],
  183. },
  184. width: COL_WIDTH_UNDEFINED,
  185. isSortable: true,
  186. type: 'duration',
  187. });
  188. });
  189. it('can decode elements with aggregate functions with multiple arguments using span op breakdowns', function () {
  190. const results = decodeColumnOrder([{field: 'percentile(spans.lcp, 0.65)'}]);
  191. expect(Array.isArray(results)).toBeTruthy();
  192. expect(results[0]).toEqual({
  193. key: 'percentile(spans.lcp, 0.65)',
  194. name: 'percentile(spans.lcp, 0.65)',
  195. column: {
  196. kind: 'function',
  197. function: ['percentile', 'spans.lcp', '0.65', undefined],
  198. },
  199. width: COL_WIDTH_UNDEFINED,
  200. isSortable: true,
  201. type: 'duration',
  202. });
  203. });
  204. });
  205. describe('pushEventViewToLocation', function () {
  206. const state: EventViewOptions = {
  207. ...baseView,
  208. id: '1234',
  209. name: 'best query',
  210. fields: [{field: 'count()', width: 420}, {field: 'project.id'}],
  211. sorts: [{field: 'count', kind: 'desc'}],
  212. query: 'event.type:error',
  213. project: [42],
  214. start: '2019-10-01T00:00:00',
  215. end: '2019-10-02T00:00:00',
  216. statsPeriod: '14d',
  217. environment: ['staging'],
  218. };
  219. const location = LocationFixture({
  220. query: {
  221. bestCountry: 'canada',
  222. },
  223. });
  224. it('correct query string object pushed to history', function () {
  225. const navigate = jest.fn();
  226. const eventView = new EventView({...baseView, ...state});
  227. pushEventViewToLocation({
  228. navigate,
  229. location,
  230. nextEventView: eventView,
  231. });
  232. expect(navigate).toHaveBeenCalledWith(
  233. expect.objectContaining({
  234. query: expect.objectContaining({
  235. id: '1234',
  236. name: 'best query',
  237. field: ['count()', 'project.id'],
  238. widths: '420',
  239. sort: '-count',
  240. query: 'event.type:error',
  241. project: '42',
  242. start: '2019-10-01T00:00:00',
  243. end: '2019-10-02T00:00:00',
  244. statsPeriod: '14d',
  245. environment: 'staging',
  246. yAxis: 'count()',
  247. }),
  248. })
  249. );
  250. });
  251. it('extra query params', function () {
  252. const navigate = jest.fn();
  253. const eventView = new EventView({...baseView, ...state});
  254. pushEventViewToLocation({
  255. navigate,
  256. location,
  257. nextEventView: eventView,
  258. extraQuery: {
  259. cursor: 'some cursor',
  260. },
  261. });
  262. expect(navigate).toHaveBeenCalledWith(
  263. expect.objectContaining({
  264. query: expect.objectContaining({
  265. id: '1234',
  266. name: 'best query',
  267. field: ['count()', 'project.id'],
  268. widths: '420',
  269. sort: '-count',
  270. query: 'event.type:error',
  271. project: '42',
  272. start: '2019-10-01T00:00:00',
  273. end: '2019-10-02T00:00:00',
  274. statsPeriod: '14d',
  275. environment: 'staging',
  276. cursor: 'some cursor',
  277. yAxis: 'count()',
  278. }),
  279. })
  280. );
  281. });
  282. });
  283. describe('getExpandedResults()', function () {
  284. const state: EventViewOptions = {
  285. ...baseView,
  286. id: '1234',
  287. name: 'best query',
  288. fields: [
  289. {field: 'count()'},
  290. {field: 'last_seen()'},
  291. {field: 'title'},
  292. {field: 'custom_tag'},
  293. ],
  294. sorts: [{field: 'count', kind: 'desc'}],
  295. query: 'event.type:error',
  296. project: [42],
  297. start: '2019-10-01T00:00:00',
  298. end: '2019-10-02T00:00:00',
  299. statsPeriod: '14d',
  300. environment: ['staging'],
  301. };
  302. it('id should be default column when drilldown results in no columns', () => {
  303. const view = new EventView({
  304. ...baseView,
  305. ...state,
  306. fields: [{field: 'count()'}, {field: 'epm()'}, {field: 'eps()'}],
  307. });
  308. const result = getExpandedResults(view, {}, EventFixture());
  309. expect(result.fields).toEqual([{field: 'id', width: -1}]);
  310. });
  311. it('preserves aggregated fields', () => {
  312. let view = new EventView(state);
  313. let result = getExpandedResults(view, {}, EventFixture());
  314. // id should be omitted as it is an implicit property on unaggregated results.
  315. expect(result.fields).toEqual([
  316. {field: 'timestamp', width: -1},
  317. {field: 'title'},
  318. {field: 'custom_tag'},
  319. ]);
  320. expect(result.query).toBe('event.type:error title:ApiException');
  321. // de-duplicate transformed columns
  322. view = new EventView({
  323. ...baseView,
  324. ...state,
  325. fields: [
  326. {field: 'count()'},
  327. {field: 'last_seen()'},
  328. {field: 'title'},
  329. {field: 'custom_tag'},
  330. {field: 'count(id)'},
  331. ],
  332. });
  333. result = getExpandedResults(view, {}, EventFixture());
  334. // id should be omitted as it is an implicit property on unaggregated results.
  335. expect(result.fields).toEqual([
  336. {field: 'timestamp', width: -1},
  337. {field: 'title'},
  338. {field: 'custom_tag'},
  339. ]);
  340. // transform aliased fields, & de-duplicate any transforms
  341. view = new EventView({
  342. ...baseView,
  343. ...state,
  344. fields: [
  345. {field: 'last_seen()'}, // expect this to be transformed to timestamp
  346. {field: 'title'},
  347. {field: 'avg(transaction.duration)'}, // expect this to be dropped
  348. {field: 'p50()'},
  349. {field: 'p75()'},
  350. {field: 'p95()'},
  351. {field: 'p99()'},
  352. {field: 'p100()'},
  353. {field: 'p9001()'}, // it's over 9000
  354. {field: 'foobar()'}, // unknown function with no parameter
  355. {field: 'custom_tag'},
  356. {field: 'transaction.duration'}, // should be dropped
  357. {field: 'title'}, // should be dropped
  358. {field: 'unique_count(id)'},
  359. {field: 'apdex(300)'}, // should be dropped
  360. {field: 'user_misery(300)'}, // should be dropped
  361. {field: 'failure_count()'}, // expect this to be transformed to transaction.status
  362. ],
  363. });
  364. result = getExpandedResults(view, {}, EventFixture());
  365. expect(result.fields).toEqual([
  366. {field: 'timestamp', width: -1},
  367. {field: 'title'},
  368. {field: 'transaction.duration', width: -1},
  369. {field: 'custom_tag'},
  370. {field: 'transaction.status', width: -1},
  371. ]);
  372. // transforms pXX functions with optional arguments properly
  373. view = new EventView({
  374. ...baseView,
  375. ...state,
  376. fields: [
  377. {field: 'p50(transaction.duration)'},
  378. {field: 'p75(measurements.foo)'},
  379. {field: 'p95(measurements.bar)'},
  380. {field: 'p99(measurements.fcp)'},
  381. {field: 'p100(measurements.lcp)'},
  382. ],
  383. });
  384. result = getExpandedResults(view, {}, EventFixture());
  385. expect(result.fields).toEqual([
  386. {field: 'transaction.duration', width: -1},
  387. {field: 'measurements.foo', width: -1},
  388. {field: 'measurements.bar', width: -1},
  389. {field: 'measurements.fcp', width: -1},
  390. {field: 'measurements.lcp', width: -1},
  391. ]);
  392. });
  393. it('applies provided additional conditions', () => {
  394. const view = new EventView({
  395. ...baseView,
  396. ...state,
  397. fields: [...state.fields, {field: 'measurements.lcp'}, {field: 'measurements.fcp'}],
  398. });
  399. let result = getExpandedResults(view, {extra: 'condition'}, EventFixture());
  400. expect(result.query).toBe('event.type:error extra:condition title:ApiException');
  401. // handles user tag values.
  402. result = getExpandedResults(view, {user: 'id:12735'}, EventFixture());
  403. expect(result.query).toBe('event.type:error user:id:12735 title:ApiException');
  404. result = getExpandedResults(view, {user: 'name:uhoh'}, EventFixture());
  405. expect(result.query).toBe('event.type:error user:name:uhoh title:ApiException');
  406. // quotes value
  407. result = getExpandedResults(view, {extra: 'has space'}, EventFixture());
  408. expect(result.query).toBe('event.type:error extra:"has space" title:ApiException');
  409. // appends to existing conditions
  410. result = getExpandedResults(view, {'event.type': 'csp'}, EventFixture());
  411. expect(result.query).toBe('event.type:csp title:ApiException');
  412. // Includes empty strings
  413. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: ''}));
  414. expect(result.query).toBe('event.type:error title:ApiException custom_tag:""');
  415. // Includes 0
  416. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: 0}));
  417. expect(result.query).toBe('event.type:error title:ApiException custom_tag:0');
  418. // Includes null
  419. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: null}));
  420. expect(result.query).toBe('event.type:error title:ApiException custom_tag:""');
  421. // Handles measurements while ignoring null values
  422. result = getExpandedResults(
  423. view,
  424. {},
  425. // @ts-expect-error The type on this is wrong, the actual type is ReactText which is just string|number
  426. // however we seem to have tests that test for null values as well, hence the expect error
  427. {'measurements.lcp': 2, 'measurements.fcp': null}
  428. );
  429. expect(result.query).toBe('event.type:error measurements.lcp:2');
  430. });
  431. it('removes any aggregates in either search conditions or extra conditions', () => {
  432. const view = new EventView({...state, query: 'event.type:error count(id):<10'});
  433. const result = getExpandedResults(view, {'count(id)': '>2'}, EventFixture());
  434. expect(result.query).toBe('event.type:error title:ApiException');
  435. });
  436. it('applies conditions from dataRow map structure based on fields', () => {
  437. const view = new EventView(state);
  438. const result = getExpandedResults(
  439. view,
  440. {extra: 'condition'},
  441. EventFixture({title: 'Event title'})
  442. );
  443. expect(result.query).toBe('event.type:error extra:condition title:"Event title"');
  444. });
  445. it('applies tag key conditions from event data', () => {
  446. const view = new EventView(state);
  447. const event = EventFixture({
  448. type: 'error',
  449. tags: [
  450. {key: 'nope', value: 'nope'},
  451. {key: 'custom_tag', value: 'tag_value'},
  452. ],
  453. });
  454. const result = getExpandedResults(view, {}, event);
  455. expect(result.query).toBe('event.type:error title:ApiException custom_tag:tag_value');
  456. });
  457. it('generate eventview from an empty eventview', () => {
  458. const view = EventView.fromLocation(LocationFixture());
  459. const result = getExpandedResults(view, {some_tag: 'value'}, EventFixture());
  460. expect(result.fields).toEqual([]);
  461. expect(result.query).toBe('some_tag:value');
  462. });
  463. it('removes equations on aggregates', () => {
  464. const view = new EventView({
  465. ...baseView,
  466. ...state,
  467. fields: [
  468. {field: 'count()'},
  469. {field: 'equation|count() / 2'},
  470. {field: 'equation|(count() - count()) + 5'},
  471. ],
  472. });
  473. const result = getExpandedResults(view, {});
  474. expect(result.fields).toEqual([
  475. {
  476. field: 'id',
  477. width: -1,
  478. },
  479. ]);
  480. });
  481. it('keeps equations without aggregates', () => {
  482. const view = new EventView({
  483. ...baseView,
  484. ...state,
  485. fields: [{field: 'count()'}, {field: 'equation|transaction.duration / 2'}],
  486. });
  487. const result = getExpandedResults(view, {});
  488. expect(result.fields).toEqual([
  489. {
  490. field: 'equation|transaction.duration / 2',
  491. width: -1,
  492. },
  493. ]);
  494. });
  495. it('applies array value conditions from event data', () => {
  496. const view = new EventView({
  497. ...baseView,
  498. ...state,
  499. fields: [...state.fields, {field: 'error.type'}],
  500. });
  501. const event = EventFixture({
  502. type: 'error',
  503. tags: [
  504. {key: 'nope', value: 'nope'},
  505. {key: 'custom_tag', value: 'tag_value'},
  506. ],
  507. 'error.type': ['DeadSystem Exception', 'RuntimeException', 'RuntimeException'],
  508. });
  509. const result = getExpandedResults(view, {}, event);
  510. expect(result.query).toBe(
  511. 'event.type:error title:ApiException custom_tag:tag_value error.type:"DeadSystem Exception" error.type:RuntimeException error.type:RuntimeException'
  512. );
  513. });
  514. it('applies project condition to project property', () => {
  515. const view = new EventView(state);
  516. const result = getExpandedResults(view, {'project.id': '1'});
  517. expect(result.query.includes('event.type:error')).toBeTruthy();
  518. expect(result.project).toEqual([42, 1]);
  519. });
  520. it('applies environment condition to environment property', () => {
  521. const view = new EventView(state);
  522. const result = getExpandedResults(view, {environment: 'dev'});
  523. expect(result.query).toBe('event.type:error');
  524. expect(result.environment).toEqual(['staging', 'dev']);
  525. });
  526. it('applies tags that overlap PageFilters state', () => {
  527. const view = new EventView({
  528. ...baseView,
  529. ...state,
  530. fields: [{field: 'project'}, {field: 'environment'}, {field: 'title'}],
  531. });
  532. const event = EventFixture({
  533. title: 'something bad',
  534. timestamp: '2020-02-13T17:05:46+00:00',
  535. tags: [
  536. {key: 'project', value: '12345'},
  537. {key: 'environment', value: 'earth'},
  538. ],
  539. });
  540. const result = getExpandedResults(view, {}, event);
  541. expect(result.query).toBe(
  542. 'event.type:error tags[project]:12345 tags[environment]:earth title:"something bad"'
  543. );
  544. expect(result.project).toEqual([42]);
  545. expect(result.environment).toEqual(['staging']);
  546. });
  547. it('applies the normalized user tag', function () {
  548. const view = new EventView({
  549. ...baseView,
  550. ...state,
  551. fields: [{field: 'user'}, {field: 'title'}],
  552. });
  553. let event = EventFixture({
  554. title: 'something bad',
  555. // user context should be ignored.
  556. user: {
  557. id: 1234,
  558. username: 'uhoh',
  559. },
  560. tags: [{key: 'user', value: 'id:1234'}],
  561. });
  562. let result = getExpandedResults(view, {}, event);
  563. expect(result.query).toBe('event.type:error user:id:1234 title:"something bad"');
  564. event = EventFixture({
  565. title: 'something bad',
  566. tags: [{key: 'user', value: '1234'}],
  567. });
  568. result = getExpandedResults(view, {}, event);
  569. expect(result.query).toBe('event.type:error user:1234 title:"something bad"');
  570. });
  571. it('applies the user field in a table row', function () {
  572. const view = new EventView({
  573. ...state,
  574. fields: [{field: 'user'}, {field: 'title'}],
  575. });
  576. const event = EventFixture({
  577. title: 'something bad',
  578. user: 'id:1234',
  579. });
  580. const result = getExpandedResults(view, {}, event);
  581. expect(result.query).toBe('event.type:error user:id:1234 title:"something bad"');
  582. });
  583. it('normalizes the timestamp field', () => {
  584. const view = new EventView({
  585. ...state,
  586. fields: [{field: 'timestamp'}],
  587. sorts: [{field: 'timestamp', kind: 'desc'}],
  588. });
  589. const event = EventFixture({
  590. type: 'error',
  591. timestamp: '2020-02-13T17:05:46+00:00',
  592. });
  593. const result = getExpandedResults(view, {}, event);
  594. expect(result.query).toBe('event.type:error timestamp:2020-02-13T17:05:46');
  595. });
  596. it('does not duplicate conditions', () => {
  597. const view = new EventView({
  598. ...baseView,
  599. ...state,
  600. query: 'event.type:error title:bogus',
  601. });
  602. const event = EventFixture({
  603. title: 'bogus',
  604. });
  605. const result = getExpandedResults(view, {trace: 'abc123'}, event);
  606. expect(result.query).toBe('event.type:error trace:abc123 title:bogus');
  607. });
  608. it('applies project as condition if present', () => {
  609. const view = new EventView({
  610. ...baseView,
  611. ...state,
  612. query: '',
  613. fields: [{field: 'project'}],
  614. });
  615. const event = EventFixture({project: 'whoosh'});
  616. const result = getExpandedResults(view, {}, event);
  617. expect(result.query).toBe('project:whoosh');
  618. });
  619. it('applies project name as condition if present', () => {
  620. const view = new EventView({
  621. ...baseView,
  622. ...state,
  623. query: '',
  624. fields: [{field: 'project.name'}],
  625. });
  626. const event = EventFixture({'project.name': 'whoosh'});
  627. const result = getExpandedResults(view, {}, event);
  628. expect(result.query).toBe('project.name:whoosh');
  629. });
  630. it('should not trim values that need to be quoted', () => {
  631. const view = new EventView({
  632. ...baseView,
  633. ...state,
  634. query: '',
  635. fields: [{field: 'title'}],
  636. });
  637. // needs to be quoted because of whitespace in middle
  638. const event = EventFixture({title: 'hello there '});
  639. const result = getExpandedResults(view, {}, event);
  640. expect(result.query).toBe('title:"hello there "');
  641. });
  642. it('should add environment from the data row', () => {
  643. const view = new EventView({
  644. ...baseView,
  645. ...state,
  646. environment: [],
  647. query: '',
  648. fields: [{field: 'environment'}],
  649. });
  650. expect(view.environment).toEqual([]);
  651. const event = EventFixture({environment: 'staging'});
  652. const result = getExpandedResults(view, {}, event);
  653. expect(result.environment).toEqual(['staging']);
  654. });
  655. it('should not add duplicate environment', () => {
  656. const view = new EventView({
  657. ...baseView,
  658. ...state,
  659. query: '',
  660. fields: [{field: 'environment'}],
  661. });
  662. expect(view.environment).toEqual(['staging']);
  663. const event = EventFixture({environment: 'staging'});
  664. const result = getExpandedResults(view, {}, event);
  665. expect(result.environment).toEqual(['staging']);
  666. });
  667. });
  668. describe('downloadAsCsv', function () {
  669. const messageColumn = {key: 'message', name: 'message'};
  670. const environmentColumn = {key: 'environment', name: 'environment'};
  671. const countColumn = {key: 'count', name: 'count'};
  672. const userColumn = {key: 'user', name: 'user'};
  673. const equationColumn = {key: 'equation| count() + count()', name: 'count() + count()'};
  674. it('handles raw data', function () {
  675. const result = {
  676. data: [
  677. {message: 'test 1', environment: 'prod'},
  678. {message: 'test 2', environment: 'test'},
  679. ],
  680. };
  681. expect(
  682. downloadAsCsv(result, [messageColumn, environmentColumn], 'filename.csv')
  683. ).toContain(encodeURIComponent('message,environment\r\ntest 1,prod\r\ntest 2,test'));
  684. });
  685. it('handles aggregations', function () {
  686. const result = {
  687. data: [{count: 3}],
  688. };
  689. expect(downloadAsCsv(result, [countColumn], 'filename.csv')).toContain(
  690. encodeURI('count\r\n3')
  691. );
  692. });
  693. it('quotes unsafe strings', function () {
  694. const result = {
  695. data: [{message: '=HYPERLINK(http://some-bad-website#)'}],
  696. };
  697. expect(downloadAsCsv(result, [messageColumn], 'filename.csv')).toContain(
  698. encodeURIComponent("message\r\n'=HYPERLINK(http://some-bad-website#)")
  699. );
  700. });
  701. it('handles the user column', function () {
  702. const result = {
  703. data: [
  704. {message: 'test 0', user: 'name:baz'},
  705. {message: 'test 1', user: 'id:123'},
  706. {message: 'test 2', user: 'email:test@example.com'},
  707. {message: 'test 3', user: 'ip:127.0.0.1'},
  708. ],
  709. };
  710. expect(downloadAsCsv(result, [messageColumn, userColumn], 'filename.csv')).toContain(
  711. encodeURIComponent(
  712. 'message,user\r\ntest 0,name:baz\r\ntest 1,id:123\r\ntest 2,email:test@example.com\r\ntest 3,ip:127.0.0.1'
  713. )
  714. );
  715. });
  716. it('handles equations', function () {
  717. const result = {
  718. data: [{'equation| count() + count()': 3}],
  719. };
  720. expect(downloadAsCsv(result, [equationColumn], 'filename.csv')).toContain(
  721. encodeURIComponent('count() + count()\r\n3')
  722. );
  723. });
  724. });
  725. describe('eventViewToWidgetQuery', function () {
  726. const state: EventViewOptions = {
  727. ...baseView,
  728. id: '1234',
  729. name: 'best query',
  730. fields: [{field: 'count()', width: 420}, {field: 'project.id'}],
  731. sorts: [{field: 'count', kind: 'desc'}],
  732. query: 'event.type:error',
  733. project: [42],
  734. start: '2019-10-01T00:00:00',
  735. end: '2019-10-02T00:00:00',
  736. statsPeriod: '14d',
  737. environment: ['staging'],
  738. };
  739. it('updates orderby to function format for top N query', function () {
  740. const view = new EventView({...baseView, ...state});
  741. const widgetQuery = eventViewToWidgetQuery({
  742. eventView: view,
  743. displayType: DisplayType.TOP_N,
  744. yAxis: ['count()'],
  745. });
  746. expect(widgetQuery.orderby).toBe('-count()');
  747. });
  748. it('updates orderby to function format for complex function', function () {
  749. const view = new EventView({
  750. ...baseView,
  751. ...state,
  752. fields: [{field: 'count_unique(device.locale)', width: 420}, {field: 'project.id'}],
  753. sorts: [{field: 'count_unique_device_locale', kind: 'desc'}],
  754. });
  755. const widgetQuery = eventViewToWidgetQuery({
  756. eventView: view,
  757. displayType: DisplayType.TABLE,
  758. });
  759. expect(widgetQuery.orderby).toBe('-count_unique(device.locale)');
  760. });
  761. it('updates orderby to field', function () {
  762. const view = new EventView({
  763. ...baseView,
  764. ...state,
  765. sorts: [{field: 'project.id', kind: 'desc'}],
  766. });
  767. const widgetQuery = eventViewToWidgetQuery({
  768. eventView: view,
  769. displayType: DisplayType.TABLE,
  770. });
  771. expect(widgetQuery.orderby).toBe('-project.id');
  772. });
  773. });
  774. describe('generateFieldOptions', function () {
  775. it('generates custom measurement field options', function () {
  776. expect(
  777. generateFieldOptions({
  778. organization: initializeOrg().organization,
  779. customMeasurements: [
  780. {functions: ['p99'], key: 'measurements.custom.measurement'},
  781. ],
  782. })['measurement:measurements.custom.measurement']
  783. ).toEqual({
  784. label: 'measurements.custom.measurement',
  785. value: {
  786. kind: 'custom_measurement',
  787. meta: {
  788. dataType: 'number',
  789. functions: ['p99'],
  790. name: 'measurements.custom.measurement',
  791. },
  792. },
  793. });
  794. });
  795. it('disambiguates tags that are also fields', function () {
  796. expect(
  797. generateFieldOptions({
  798. organization: initializeOrg().organization,
  799. tagKeys: ['environment'],
  800. fieldKeys: ['environment'],
  801. aggregations: {},
  802. })
  803. ).toEqual({
  804. 'field:environment': {
  805. label: 'environment',
  806. value: {
  807. kind: 'field',
  808. meta: {
  809. dataType: 'string',
  810. name: 'environment',
  811. },
  812. },
  813. },
  814. 'tag:environment': {
  815. label: 'environment',
  816. value: {
  817. kind: 'tag',
  818. meta: {
  819. dataType: 'string',
  820. name: 'tags[environment]',
  821. },
  822. },
  823. },
  824. });
  825. });
  826. });
  827. describe('constructAddQueryToDashboardLink', function () {
  828. let organization: Organization;
  829. let location: Location;
  830. describe('new widget builder', function () {
  831. beforeEach(() => {
  832. organization = OrganizationFixture({
  833. features: ['dashboards-widget-builder-redesign'],
  834. });
  835. location = LocationFixture();
  836. });
  837. it('should construct a link with the correct params - total period', function () {
  838. const eventView = new EventView({
  839. ...baseView,
  840. display: DisplayModes.DEFAULT,
  841. name: 'best query',
  842. });
  843. const {query} = constructAddQueryToDashboardLink({
  844. eventView,
  845. organization,
  846. location,
  847. yAxis: ['count()', 'count_unique(user)'],
  848. widgetType: WidgetType.TRANSACTIONS,
  849. });
  850. expect(query).toEqual({
  851. start: undefined,
  852. end: undefined,
  853. description: '',
  854. limit: undefined,
  855. query: [''],
  856. sort: [''],
  857. legendAlias: [''],
  858. field: [],
  859. title: 'best query',
  860. dataset: WidgetType.TRANSACTIONS,
  861. displayType: DisplayType.AREA,
  862. yAxis: ['count()', 'count_unique(user)'],
  863. });
  864. });
  865. it('should construct a link with the correct params - topN', function () {
  866. // This test assigns a grouping through the fields array in the event view
  867. const eventView = new EventView({
  868. ...baseView,
  869. display: DisplayModes.TOP5,
  870. name: 'best query',
  871. fields: [{field: 'transaction', width: 420}, {field: 'project.id'}],
  872. });
  873. const {query} = constructAddQueryToDashboardLink({
  874. eventView,
  875. organization,
  876. location,
  877. yAxis: ['count()'],
  878. widgetType: WidgetType.TRANSACTIONS,
  879. });
  880. expect(query).toEqual({
  881. start: undefined,
  882. end: undefined,
  883. description: '',
  884. query: [''],
  885. sort: [''],
  886. legendAlias: [''],
  887. field: ['transaction', 'project.id'],
  888. title: 'best query',
  889. dataset: WidgetType.TRANSACTIONS,
  890. displayType: DisplayType.AREA,
  891. yAxis: ['count()'],
  892. limit: 5,
  893. });
  894. });
  895. it('should construct a link with the correct params - daily top N', function () {
  896. // This test assigns a grouping through the fields array in the event view
  897. const eventView = new EventView({
  898. ...baseView,
  899. display: DisplayModes.DAILYTOP5,
  900. name: 'best query',
  901. fields: [{field: 'transaction', width: 420}, {field: 'project.id'}],
  902. });
  903. const {query} = constructAddQueryToDashboardLink({
  904. eventView,
  905. organization,
  906. location,
  907. yAxis: ['count()'],
  908. widgetType: WidgetType.TRANSACTIONS,
  909. });
  910. expect(query).toEqual({
  911. start: undefined,
  912. end: undefined,
  913. description: '',
  914. query: [''],
  915. sort: [''],
  916. legendAlias: [''],
  917. field: ['transaction', 'project.id'],
  918. title: 'best query',
  919. dataset: WidgetType.TRANSACTIONS,
  920. displayType: DisplayType.BAR,
  921. yAxis: ['count()'],
  922. limit: 5,
  923. });
  924. });
  925. });
  926. });
  927. describe('handleAddQueryToDashboard', function () {
  928. let organization: Organization;
  929. let location: Location;
  930. let router: InjectedRouter;
  931. let mockedOpenAddToDashboardModal: jest.Mock;
  932. beforeEach(() => {
  933. organization = OrganizationFixture({});
  934. location = LocationFixture();
  935. router = RouterFixture();
  936. mockedOpenAddToDashboardModal = jest.mocked(openAddToDashboardModal);
  937. });
  938. it('constructs the correct widget queries for the modal with single yAxis', function () {
  939. const eventView = new EventView({
  940. ...baseView,
  941. display: DisplayModes.DEFAULT,
  942. name: 'best query',
  943. });
  944. handleAddQueryToDashboard({
  945. eventView,
  946. organization,
  947. location,
  948. router,
  949. widgetType: WidgetType.TRANSACTIONS,
  950. yAxis: ['count()'],
  951. });
  952. expect(mockedOpenAddToDashboardModal).toHaveBeenCalledWith(
  953. expect.objectContaining({
  954. widget: {
  955. title: 'best query',
  956. displayType: DisplayType.AREA,
  957. queries: [
  958. {
  959. name: '',
  960. fields: ['count()'],
  961. aggregates: ['count()'],
  962. columns: [],
  963. orderby: '',
  964. conditions: '',
  965. },
  966. ],
  967. interval: undefined,
  968. limit: undefined,
  969. widgetType: WidgetType.TRANSACTIONS,
  970. },
  971. })
  972. );
  973. });
  974. it('constructs the correct widget queries for the modal with single yAxis top N', function () {
  975. const eventView = new EventView({
  976. ...baseView,
  977. display: DisplayModes.TOP5,
  978. name: 'best query',
  979. fields: [{field: 'transaction'}],
  980. });
  981. handleAddQueryToDashboard({
  982. eventView,
  983. organization,
  984. location,
  985. router,
  986. widgetType: WidgetType.TRANSACTIONS,
  987. yAxis: ['count()'],
  988. });
  989. expect(mockedOpenAddToDashboardModal).toHaveBeenCalledWith(
  990. expect.objectContaining({
  991. widget: {
  992. title: 'best query',
  993. displayType: DisplayType.AREA,
  994. queries: [
  995. {
  996. name: '',
  997. aggregates: ['count()'],
  998. columns: ['transaction'],
  999. fields: ['transaction', 'count()'],
  1000. orderby: '',
  1001. conditions: '',
  1002. },
  1003. ],
  1004. interval: undefined,
  1005. limit: 5,
  1006. widgetType: WidgetType.TRANSACTIONS,
  1007. },
  1008. })
  1009. );
  1010. });
  1011. it('constructs the correct widget queries for the modal with multiple yAxes', function () {
  1012. const eventView = new EventView({
  1013. ...baseView,
  1014. display: DisplayModes.DEFAULT,
  1015. name: 'best query',
  1016. });
  1017. handleAddQueryToDashboard({
  1018. eventView,
  1019. organization,
  1020. location,
  1021. router,
  1022. widgetType: WidgetType.TRANSACTIONS,
  1023. yAxis: ['count()', 'count_unique(user)'],
  1024. });
  1025. expect(mockedOpenAddToDashboardModal).toHaveBeenCalledWith(
  1026. expect.objectContaining({
  1027. widget: {
  1028. title: 'best query',
  1029. displayType: DisplayType.AREA,
  1030. queries: [
  1031. {
  1032. name: '',
  1033. aggregates: ['count()', 'count_unique(user)'],
  1034. columns: [],
  1035. fields: ['count()', 'count_unique(user)'],
  1036. orderby: '',
  1037. conditions: '',
  1038. },
  1039. ],
  1040. interval: undefined,
  1041. limit: undefined,
  1042. widgetType: WidgetType.TRANSACTIONS,
  1043. },
  1044. })
  1045. );
  1046. });
  1047. describe('new widget builder', function () {
  1048. beforeEach(() => {
  1049. organization = OrganizationFixture({
  1050. features: ['dashboards-widget-builder-redesign'],
  1051. });
  1052. });
  1053. it('constructs the correct widget queries for the modal with single yAxis', function () {
  1054. const eventView = new EventView({
  1055. ...baseView,
  1056. display: DisplayModes.DEFAULT,
  1057. name: 'best query',
  1058. });
  1059. handleAddQueryToDashboard({
  1060. eventView,
  1061. organization,
  1062. location,
  1063. router,
  1064. widgetType: WidgetType.TRANSACTIONS,
  1065. yAxis: ['count()'],
  1066. });
  1067. expect(mockedOpenAddToDashboardModal).toHaveBeenCalledWith(
  1068. expect.objectContaining({
  1069. widget: {
  1070. title: 'best query',
  1071. displayType: DisplayType.AREA,
  1072. queries: [
  1073. {
  1074. name: '',
  1075. aggregates: ['count()'],
  1076. columns: [],
  1077. fields: [],
  1078. orderby: '',
  1079. conditions: '',
  1080. },
  1081. ],
  1082. interval: undefined,
  1083. limit: undefined,
  1084. widgetType: WidgetType.TRANSACTIONS,
  1085. },
  1086. })
  1087. );
  1088. });
  1089. it('constructs the correct widget queries for the modal with single yAxis top N', function () {
  1090. const eventView = new EventView({
  1091. ...baseView,
  1092. display: DisplayModes.TOP5,
  1093. name: 'best query',
  1094. fields: [{field: 'transaction'}],
  1095. });
  1096. handleAddQueryToDashboard({
  1097. eventView,
  1098. organization,
  1099. location,
  1100. router,
  1101. widgetType: WidgetType.TRANSACTIONS,
  1102. yAxis: ['count()'],
  1103. });
  1104. expect(mockedOpenAddToDashboardModal).toHaveBeenCalledWith(
  1105. expect.objectContaining({
  1106. widget: {
  1107. title: 'best query',
  1108. displayType: DisplayType.AREA,
  1109. queries: [
  1110. {
  1111. name: '',
  1112. aggregates: ['count()'],
  1113. columns: ['transaction'],
  1114. fields: ['transaction'],
  1115. orderby: '',
  1116. conditions: '',
  1117. },
  1118. ],
  1119. interval: undefined,
  1120. limit: 5,
  1121. widgetType: WidgetType.TRANSACTIONS,
  1122. },
  1123. })
  1124. );
  1125. });
  1126. it('constructs the correct widget queries for the modal with multiple yAxes', function () {
  1127. const eventView = new EventView({
  1128. ...baseView,
  1129. display: DisplayModes.DEFAULT,
  1130. name: 'best query',
  1131. });
  1132. handleAddQueryToDashboard({
  1133. eventView,
  1134. organization,
  1135. location,
  1136. router,
  1137. widgetType: WidgetType.TRANSACTIONS,
  1138. yAxis: ['count()', 'count_unique(user)'],
  1139. });
  1140. expect(mockedOpenAddToDashboardModal).toHaveBeenCalledWith(
  1141. expect.objectContaining({
  1142. widget: {
  1143. title: 'best query',
  1144. displayType: DisplayType.AREA,
  1145. queries: [
  1146. {
  1147. name: '',
  1148. aggregates: ['count()', 'count_unique(user)'],
  1149. columns: [],
  1150. fields: [],
  1151. orderby: '',
  1152. conditions: '',
  1153. },
  1154. ],
  1155. interval: undefined,
  1156. limit: undefined,
  1157. widgetType: WidgetType.TRANSACTIONS,
  1158. },
  1159. })
  1160. );
  1161. });
  1162. });
  1163. });