utils.spec.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. import {EventFixture} from 'sentry-fixture/event';
  2. import {LocationFixture} from 'sentry-fixture/locationFixture';
  3. import {initializeOrg} from 'sentry-test/initializeOrg';
  4. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  5. import type {EventViewOptions} from 'sentry/utils/discover/eventView';
  6. import EventView from 'sentry/utils/discover/eventView';
  7. import {DisplayType} from 'sentry/views/dashboards/types';
  8. import {
  9. decodeColumnOrder,
  10. downloadAsCsv,
  11. eventViewToWidgetQuery,
  12. generateFieldOptions,
  13. getExpandedResults,
  14. pushEventViewToLocation,
  15. } from 'sentry/views/discover/utils';
  16. const baseView: EventViewOptions = {
  17. display: undefined,
  18. start: undefined,
  19. end: undefined,
  20. id: '0',
  21. name: undefined,
  22. fields: [],
  23. createdBy: undefined,
  24. environment: [],
  25. project: [],
  26. query: '',
  27. sorts: [],
  28. statsPeriod: undefined,
  29. team: [],
  30. topEvents: undefined,
  31. };
  32. describe('decodeColumnOrder', function () {
  33. it('can decode 0 elements', function () {
  34. const results = decodeColumnOrder([]);
  35. expect(Array.isArray(results)).toBeTruthy();
  36. expect(results).toHaveLength(0);
  37. });
  38. it('can decode fields', function () {
  39. const results = decodeColumnOrder([{field: 'title', width: 123}]);
  40. expect(Array.isArray(results)).toBeTruthy();
  41. expect(results[0]).toEqual({
  42. key: 'title',
  43. name: 'title',
  44. column: {
  45. kind: 'field',
  46. field: 'title',
  47. },
  48. width: 123,
  49. isSortable: false,
  50. type: 'string',
  51. });
  52. });
  53. it('can decode measurement fields', function () {
  54. const results = decodeColumnOrder([{field: 'measurements.foo', width: 123}]);
  55. expect(Array.isArray(results)).toBeTruthy();
  56. expect(results[0]).toEqual({
  57. key: 'measurements.foo',
  58. name: 'measurements.foo',
  59. column: {
  60. kind: 'field',
  61. field: 'measurements.foo',
  62. },
  63. width: 123,
  64. isSortable: false,
  65. type: 'number',
  66. });
  67. });
  68. it('can decode span op breakdown fields', function () {
  69. const results = decodeColumnOrder([{field: 'spans.foo', width: 123}]);
  70. expect(Array.isArray(results)).toBeTruthy();
  71. expect(results[0]).toEqual({
  72. key: 'spans.foo',
  73. name: 'spans.foo',
  74. column: {
  75. kind: 'field',
  76. field: 'spans.foo',
  77. },
  78. width: 123,
  79. isSortable: false,
  80. type: 'duration',
  81. });
  82. });
  83. it('can decode aggregate functions with no arguments', function () {
  84. let results = decodeColumnOrder([{field: 'count()', width: 123}]);
  85. expect(Array.isArray(results)).toBeTruthy();
  86. expect(results[0]).toEqual({
  87. key: 'count()',
  88. name: 'count()',
  89. column: {
  90. kind: 'function',
  91. function: ['count', '', undefined, undefined],
  92. },
  93. width: 123,
  94. isSortable: true,
  95. type: 'number',
  96. });
  97. results = decodeColumnOrder([{field: 'p75()', width: 123}]);
  98. expect(results[0]!.type).toBe('duration');
  99. results = decodeColumnOrder([{field: 'p99()', width: 123}]);
  100. expect(results[0]!.type).toBe('duration');
  101. });
  102. it('can decode elements with aggregate functions with arguments', function () {
  103. const results = decodeColumnOrder([{field: 'avg(transaction.duration)'}]);
  104. expect(Array.isArray(results)).toBeTruthy();
  105. expect(results[0]).toEqual({
  106. key: 'avg(transaction.duration)',
  107. name: 'avg(transaction.duration)',
  108. column: {
  109. kind: 'function',
  110. function: ['avg', 'transaction.duration', undefined, undefined],
  111. },
  112. width: COL_WIDTH_UNDEFINED,
  113. isSortable: true,
  114. type: 'duration',
  115. });
  116. });
  117. it('can decode elements with aggregate functions with multiple arguments', function () {
  118. const results = decodeColumnOrder([
  119. {field: 'percentile(transaction.duration, 0.65)'},
  120. ]);
  121. expect(Array.isArray(results)).toBeTruthy();
  122. expect(results[0]).toEqual({
  123. key: 'percentile(transaction.duration, 0.65)',
  124. name: 'percentile(transaction.duration, 0.65)',
  125. column: {
  126. kind: 'function',
  127. function: ['percentile', 'transaction.duration', '0.65', undefined],
  128. },
  129. width: COL_WIDTH_UNDEFINED,
  130. isSortable: true,
  131. type: 'duration',
  132. });
  133. });
  134. it('can decode elements with aggregate functions using measurements', function () {
  135. const results = decodeColumnOrder([{field: 'avg(measurements.foo)'}]);
  136. expect(Array.isArray(results)).toBeTruthy();
  137. expect(results[0]).toEqual({
  138. key: 'avg(measurements.foo)',
  139. name: 'avg(measurements.foo)',
  140. column: {
  141. kind: 'function',
  142. function: ['avg', 'measurements.foo', undefined, undefined],
  143. },
  144. width: COL_WIDTH_UNDEFINED,
  145. isSortable: true,
  146. type: 'number',
  147. });
  148. });
  149. it('can decode elements with aggregate functions with multiple arguments using measurements', function () {
  150. const results = decodeColumnOrder([{field: 'percentile(measurements.lcp, 0.65)'}]);
  151. expect(Array.isArray(results)).toBeTruthy();
  152. expect(results[0]).toEqual({
  153. key: 'percentile(measurements.lcp, 0.65)',
  154. name: 'percentile(measurements.lcp, 0.65)',
  155. column: {
  156. kind: 'function',
  157. function: ['percentile', 'measurements.lcp', '0.65', undefined],
  158. },
  159. width: COL_WIDTH_UNDEFINED,
  160. isSortable: true,
  161. type: 'duration',
  162. });
  163. });
  164. it('can decode elements with aggregate functions using span op breakdowns', function () {
  165. const results = decodeColumnOrder([{field: 'avg(spans.foo)'}]);
  166. expect(Array.isArray(results)).toBeTruthy();
  167. expect(results[0]).toEqual({
  168. key: 'avg(spans.foo)',
  169. name: 'avg(spans.foo)',
  170. column: {
  171. kind: 'function',
  172. function: ['avg', 'spans.foo', undefined, undefined],
  173. },
  174. width: COL_WIDTH_UNDEFINED,
  175. isSortable: true,
  176. type: 'duration',
  177. });
  178. });
  179. it('can decode elements with aggregate functions with multiple arguments using span op breakdowns', function () {
  180. const results = decodeColumnOrder([{field: 'percentile(spans.lcp, 0.65)'}]);
  181. expect(Array.isArray(results)).toBeTruthy();
  182. expect(results[0]).toEqual({
  183. key: 'percentile(spans.lcp, 0.65)',
  184. name: 'percentile(spans.lcp, 0.65)',
  185. column: {
  186. kind: 'function',
  187. function: ['percentile', 'spans.lcp', '0.65', undefined],
  188. },
  189. width: COL_WIDTH_UNDEFINED,
  190. isSortable: true,
  191. type: 'duration',
  192. });
  193. });
  194. });
  195. describe('pushEventViewToLocation', function () {
  196. const state: EventViewOptions = {
  197. ...baseView,
  198. id: '1234',
  199. name: 'best query',
  200. fields: [{field: 'count()', width: 420}, {field: 'project.id'}],
  201. sorts: [{field: 'count', kind: 'desc'}],
  202. query: 'event.type:error',
  203. project: [42],
  204. start: '2019-10-01T00:00:00',
  205. end: '2019-10-02T00:00:00',
  206. statsPeriod: '14d',
  207. environment: ['staging'],
  208. };
  209. const location = LocationFixture({
  210. query: {
  211. bestCountry: 'canada',
  212. },
  213. });
  214. it('correct query string object pushed to history', function () {
  215. const navigate = jest.fn();
  216. const eventView = new EventView({...baseView, ...state});
  217. pushEventViewToLocation({
  218. navigate,
  219. location,
  220. nextEventView: eventView,
  221. });
  222. expect(navigate).toHaveBeenCalledWith(
  223. expect.objectContaining({
  224. query: expect.objectContaining({
  225. id: '1234',
  226. name: 'best query',
  227. field: ['count()', 'project.id'],
  228. widths: '420',
  229. sort: '-count',
  230. query: 'event.type:error',
  231. project: '42',
  232. start: '2019-10-01T00:00:00',
  233. end: '2019-10-02T00:00:00',
  234. statsPeriod: '14d',
  235. environment: 'staging',
  236. yAxis: 'count()',
  237. }),
  238. })
  239. );
  240. });
  241. it('extra query params', function () {
  242. const navigate = jest.fn();
  243. const eventView = new EventView({...baseView, ...state});
  244. pushEventViewToLocation({
  245. navigate,
  246. location,
  247. nextEventView: eventView,
  248. extraQuery: {
  249. cursor: 'some cursor',
  250. },
  251. });
  252. expect(navigate).toHaveBeenCalledWith(
  253. expect.objectContaining({
  254. query: expect.objectContaining({
  255. id: '1234',
  256. name: 'best query',
  257. field: ['count()', 'project.id'],
  258. widths: '420',
  259. sort: '-count',
  260. query: 'event.type:error',
  261. project: '42',
  262. start: '2019-10-01T00:00:00',
  263. end: '2019-10-02T00:00:00',
  264. statsPeriod: '14d',
  265. environment: 'staging',
  266. cursor: 'some cursor',
  267. yAxis: 'count()',
  268. }),
  269. })
  270. );
  271. });
  272. });
  273. describe('getExpandedResults()', function () {
  274. const state: EventViewOptions = {
  275. ...baseView,
  276. id: '1234',
  277. name: 'best query',
  278. fields: [
  279. {field: 'count()'},
  280. {field: 'last_seen()'},
  281. {field: 'title'},
  282. {field: 'custom_tag'},
  283. ],
  284. sorts: [{field: 'count', kind: 'desc'}],
  285. query: 'event.type:error',
  286. project: [42],
  287. start: '2019-10-01T00:00:00',
  288. end: '2019-10-02T00:00:00',
  289. statsPeriod: '14d',
  290. environment: ['staging'],
  291. };
  292. it('id should be default column when drilldown results in no columns', () => {
  293. const view = new EventView({
  294. ...baseView,
  295. ...state,
  296. fields: [{field: 'count()'}, {field: 'epm()'}, {field: 'eps()'}],
  297. });
  298. const result = getExpandedResults(view, {}, EventFixture());
  299. expect(result.fields).toEqual([{field: 'id', width: -1}]);
  300. });
  301. it('preserves aggregated fields', () => {
  302. let view = new EventView(state);
  303. let result = getExpandedResults(view, {}, EventFixture());
  304. // id should be omitted as it is an implicit property on unaggregated results.
  305. expect(result.fields).toEqual([
  306. {field: 'timestamp', width: -1},
  307. {field: 'title'},
  308. {field: 'custom_tag'},
  309. ]);
  310. expect(result.query).toBe('event.type:error title:ApiException');
  311. // de-duplicate transformed columns
  312. view = new EventView({
  313. ...baseView,
  314. ...state,
  315. fields: [
  316. {field: 'count()'},
  317. {field: 'last_seen()'},
  318. {field: 'title'},
  319. {field: 'custom_tag'},
  320. {field: 'count(id)'},
  321. ],
  322. });
  323. result = getExpandedResults(view, {}, EventFixture());
  324. // id should be omitted as it is an implicit property on unaggregated results.
  325. expect(result.fields).toEqual([
  326. {field: 'timestamp', width: -1},
  327. {field: 'title'},
  328. {field: 'custom_tag'},
  329. ]);
  330. // transform aliased fields, & de-duplicate any transforms
  331. view = new EventView({
  332. ...baseView,
  333. ...state,
  334. fields: [
  335. {field: 'last_seen()'}, // expect this to be transformed to timestamp
  336. {field: 'title'},
  337. {field: 'avg(transaction.duration)'}, // expect this to be dropped
  338. {field: 'p50()'},
  339. {field: 'p75()'},
  340. {field: 'p95()'},
  341. {field: 'p99()'},
  342. {field: 'p100()'},
  343. {field: 'p9001()'}, // it's over 9000
  344. {field: 'foobar()'}, // unknown function with no parameter
  345. {field: 'custom_tag'},
  346. {field: 'transaction.duration'}, // should be dropped
  347. {field: 'title'}, // should be dropped
  348. {field: 'unique_count(id)'},
  349. {field: 'apdex(300)'}, // should be dropped
  350. {field: 'user_misery(300)'}, // should be dropped
  351. {field: 'failure_count()'}, // expect this to be transformed to transaction.status
  352. ],
  353. });
  354. result = getExpandedResults(view, {}, EventFixture());
  355. expect(result.fields).toEqual([
  356. {field: 'timestamp', width: -1},
  357. {field: 'title'},
  358. {field: 'transaction.duration', width: -1},
  359. {field: 'custom_tag'},
  360. {field: 'transaction.status', width: -1},
  361. ]);
  362. // transforms pXX functions with optional arguments properly
  363. view = new EventView({
  364. ...baseView,
  365. ...state,
  366. fields: [
  367. {field: 'p50(transaction.duration)'},
  368. {field: 'p75(measurements.foo)'},
  369. {field: 'p95(measurements.bar)'},
  370. {field: 'p99(measurements.fcp)'},
  371. {field: 'p100(measurements.lcp)'},
  372. ],
  373. });
  374. result = getExpandedResults(view, {}, EventFixture());
  375. expect(result.fields).toEqual([
  376. {field: 'transaction.duration', width: -1},
  377. {field: 'measurements.foo', width: -1},
  378. {field: 'measurements.bar', width: -1},
  379. {field: 'measurements.fcp', width: -1},
  380. {field: 'measurements.lcp', width: -1},
  381. ]);
  382. });
  383. it('applies provided additional conditions', () => {
  384. const view = new EventView({
  385. ...baseView,
  386. ...state,
  387. fields: [...state.fields, {field: 'measurements.lcp'}, {field: 'measurements.fcp'}],
  388. });
  389. let result = getExpandedResults(view, {extra: 'condition'}, EventFixture());
  390. expect(result.query).toBe('event.type:error extra:condition title:ApiException');
  391. // handles user tag values.
  392. result = getExpandedResults(view, {user: 'id:12735'}, EventFixture());
  393. expect(result.query).toBe('event.type:error user:id:12735 title:ApiException');
  394. result = getExpandedResults(view, {user: 'name:uhoh'}, EventFixture());
  395. expect(result.query).toBe('event.type:error user:name:uhoh title:ApiException');
  396. // quotes value
  397. result = getExpandedResults(view, {extra: 'has space'}, EventFixture());
  398. expect(result.query).toBe('event.type:error extra:"has space" title:ApiException');
  399. // appends to existing conditions
  400. result = getExpandedResults(view, {'event.type': 'csp'}, EventFixture());
  401. expect(result.query).toBe('event.type:csp title:ApiException');
  402. // Includes empty strings
  403. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: ''}));
  404. expect(result.query).toBe('event.type:error title:ApiException custom_tag:""');
  405. // Includes 0
  406. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: 0}));
  407. expect(result.query).toBe('event.type:error title:ApiException custom_tag:0');
  408. // Includes null
  409. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: null}));
  410. expect(result.query).toBe('event.type:error title:ApiException custom_tag:""');
  411. // Handles measurements while ignoring null values
  412. result = getExpandedResults(
  413. view,
  414. {},
  415. // The type on this is wrong, the actual type is ReactText which is just string|number
  416. // however we seem to have tests that test for null values as well, hence the expect error
  417. // @ts-expect-error
  418. {'measurements.lcp': 2, 'measurements.fcp': null}
  419. );
  420. expect(result.query).toBe('event.type:error measurements.lcp:2');
  421. });
  422. it('removes any aggregates in either search conditions or extra conditions', () => {
  423. const view = new EventView({...state, query: 'event.type:error count(id):<10'});
  424. const result = getExpandedResults(view, {'count(id)': '>2'}, EventFixture());
  425. expect(result.query).toBe('event.type:error title:ApiException');
  426. });
  427. it('applies conditions from dataRow map structure based on fields', () => {
  428. const view = new EventView(state);
  429. const result = getExpandedResults(
  430. view,
  431. {extra: 'condition'},
  432. EventFixture({title: 'Event title'})
  433. );
  434. expect(result.query).toBe('event.type:error extra:condition title:"Event title"');
  435. });
  436. it('applies tag key conditions from event data', () => {
  437. const view = new EventView(state);
  438. const event = EventFixture({
  439. type: 'error',
  440. tags: [
  441. {key: 'nope', value: 'nope'},
  442. {key: 'custom_tag', value: 'tag_value'},
  443. ],
  444. });
  445. const result = getExpandedResults(view, {}, event);
  446. expect(result.query).toBe('event.type:error title:ApiException custom_tag:tag_value');
  447. });
  448. it('generate eventview from an empty eventview', () => {
  449. const view = EventView.fromLocation(LocationFixture());
  450. const result = getExpandedResults(view, {some_tag: 'value'}, EventFixture());
  451. expect(result.fields).toEqual([]);
  452. expect(result.query).toBe('some_tag:value');
  453. });
  454. it('removes equations on aggregates', () => {
  455. const view = new EventView({
  456. ...baseView,
  457. ...state,
  458. fields: [
  459. {field: 'count()'},
  460. {field: 'equation|count() / 2'},
  461. {field: 'equation|(count() - count()) + 5'},
  462. ],
  463. });
  464. const result = getExpandedResults(view, {});
  465. expect(result.fields).toEqual([
  466. {
  467. field: 'id',
  468. width: -1,
  469. },
  470. ]);
  471. });
  472. it('keeps equations without aggregates', () => {
  473. const view = new EventView({
  474. ...baseView,
  475. ...state,
  476. fields: [{field: 'count()'}, {field: 'equation|transaction.duration / 2'}],
  477. });
  478. const result = getExpandedResults(view, {});
  479. expect(result.fields).toEqual([
  480. {
  481. field: 'equation|transaction.duration / 2',
  482. width: -1,
  483. },
  484. ]);
  485. });
  486. it('applies array value conditions from event data', () => {
  487. const view = new EventView({
  488. ...baseView,
  489. ...state,
  490. fields: [...state.fields, {field: 'error.type'}],
  491. });
  492. const event = EventFixture({
  493. type: 'error',
  494. tags: [
  495. {key: 'nope', value: 'nope'},
  496. {key: 'custom_tag', value: 'tag_value'},
  497. ],
  498. 'error.type': ['DeadSystem Exception', 'RuntimeException', 'RuntimeException'],
  499. });
  500. const result = getExpandedResults(view, {}, event);
  501. expect(result.query).toBe(
  502. 'event.type:error title:ApiException custom_tag:tag_value error.type:"DeadSystem Exception" error.type:RuntimeException error.type:RuntimeException'
  503. );
  504. });
  505. it('applies project condition to project property', () => {
  506. const view = new EventView(state);
  507. const result = getExpandedResults(view, {'project.id': '1'});
  508. expect(result.query.includes('event.type:error')).toBeTruthy();
  509. expect(result.project).toEqual([42, 1]);
  510. });
  511. it('applies environment condition to environment property', () => {
  512. const view = new EventView(state);
  513. const result = getExpandedResults(view, {environment: 'dev'});
  514. expect(result.query).toBe('event.type:error');
  515. expect(result.environment).toEqual(['staging', 'dev']);
  516. });
  517. it('applies tags that overlap PageFilters state', () => {
  518. const view = new EventView({
  519. ...baseView,
  520. ...state,
  521. fields: [{field: 'project'}, {field: 'environment'}, {field: 'title'}],
  522. });
  523. const event = EventFixture({
  524. title: 'something bad',
  525. timestamp: '2020-02-13T17:05:46+00:00',
  526. tags: [
  527. {key: 'project', value: '12345'},
  528. {key: 'environment', value: 'earth'},
  529. ],
  530. });
  531. const result = getExpandedResults(view, {}, event);
  532. expect(result.query).toBe(
  533. 'event.type:error tags[project]:12345 tags[environment]:earth title:"something bad"'
  534. );
  535. expect(result.project).toEqual([42]);
  536. expect(result.environment).toEqual(['staging']);
  537. });
  538. it('applies the normalized user tag', function () {
  539. const view = new EventView({
  540. ...baseView,
  541. ...state,
  542. fields: [{field: 'user'}, {field: 'title'}],
  543. });
  544. let event = EventFixture({
  545. title: 'something bad',
  546. // user context should be ignored.
  547. user: {
  548. id: 1234,
  549. username: 'uhoh',
  550. },
  551. tags: [{key: 'user', value: 'id:1234'}],
  552. });
  553. let result = getExpandedResults(view, {}, event);
  554. expect(result.query).toBe('event.type:error user:id:1234 title:"something bad"');
  555. event = EventFixture({
  556. title: 'something bad',
  557. tags: [{key: 'user', value: '1234'}],
  558. });
  559. result = getExpandedResults(view, {}, event);
  560. expect(result.query).toBe('event.type:error user:1234 title:"something bad"');
  561. });
  562. it('applies the user field in a table row', function () {
  563. const view = new EventView({
  564. ...state,
  565. fields: [{field: 'user'}, {field: 'title'}],
  566. });
  567. const event = EventFixture({
  568. title: 'something bad',
  569. user: 'id:1234',
  570. });
  571. const result = getExpandedResults(view, {}, event);
  572. expect(result.query).toBe('event.type:error user:id:1234 title:"something bad"');
  573. });
  574. it('normalizes the timestamp field', () => {
  575. const view = new EventView({
  576. ...state,
  577. fields: [{field: 'timestamp'}],
  578. sorts: [{field: 'timestamp', kind: 'desc'}],
  579. });
  580. const event = EventFixture({
  581. type: 'error',
  582. timestamp: '2020-02-13T17:05:46+00:00',
  583. });
  584. const result = getExpandedResults(view, {}, event);
  585. expect(result.query).toBe('event.type:error timestamp:2020-02-13T17:05:46');
  586. });
  587. it('does not duplicate conditions', () => {
  588. const view = new EventView({
  589. ...baseView,
  590. ...state,
  591. query: 'event.type:error title:bogus',
  592. });
  593. const event = EventFixture({
  594. title: 'bogus',
  595. });
  596. const result = getExpandedResults(view, {trace: 'abc123'}, event);
  597. expect(result.query).toBe('event.type:error trace:abc123 title:bogus');
  598. });
  599. it('applies project as condition if present', () => {
  600. const view = new EventView({
  601. ...baseView,
  602. ...state,
  603. query: '',
  604. fields: [{field: 'project'}],
  605. });
  606. const event = EventFixture({project: 'whoosh'});
  607. const result = getExpandedResults(view, {}, event);
  608. expect(result.query).toBe('project:whoosh');
  609. });
  610. it('applies project name as condition if present', () => {
  611. const view = new EventView({
  612. ...baseView,
  613. ...state,
  614. query: '',
  615. fields: [{field: 'project.name'}],
  616. });
  617. const event = EventFixture({'project.name': 'whoosh'});
  618. const result = getExpandedResults(view, {}, event);
  619. expect(result.query).toBe('project.name:whoosh');
  620. });
  621. it('should not trim values that need to be quoted', () => {
  622. const view = new EventView({
  623. ...baseView,
  624. ...state,
  625. query: '',
  626. fields: [{field: 'title'}],
  627. });
  628. // needs to be quoted because of whitespace in middle
  629. const event = EventFixture({title: 'hello there '});
  630. const result = getExpandedResults(view, {}, event);
  631. expect(result.query).toBe('title:"hello there "');
  632. });
  633. it('should add environment from the data row', () => {
  634. const view = new EventView({
  635. ...baseView,
  636. ...state,
  637. environment: [],
  638. query: '',
  639. fields: [{field: 'environment'}],
  640. });
  641. expect(view.environment).toEqual([]);
  642. const event = EventFixture({environment: 'staging'});
  643. const result = getExpandedResults(view, {}, event);
  644. expect(result.environment).toEqual(['staging']);
  645. });
  646. it('should not add duplicate environment', () => {
  647. const view = new EventView({
  648. ...baseView,
  649. ...state,
  650. query: '',
  651. fields: [{field: 'environment'}],
  652. });
  653. expect(view.environment).toEqual(['staging']);
  654. const event = EventFixture({environment: 'staging'});
  655. const result = getExpandedResults(view, {}, event);
  656. expect(result.environment).toEqual(['staging']);
  657. });
  658. });
  659. describe('downloadAsCsv', function () {
  660. const messageColumn = {key: 'message', name: 'message'};
  661. const environmentColumn = {key: 'environment', name: 'environment'};
  662. const countColumn = {key: 'count', name: 'count'};
  663. const userColumn = {key: 'user', name: 'user'};
  664. const equationColumn = {key: 'equation| count() + count()', name: 'count() + count()'};
  665. it('handles raw data', function () {
  666. const result = {
  667. data: [
  668. {message: 'test 1', environment: 'prod'},
  669. {message: 'test 2', environment: 'test'},
  670. ],
  671. };
  672. expect(
  673. downloadAsCsv(result, [messageColumn, environmentColumn], 'filename.csv')
  674. ).toContain(encodeURIComponent('message,environment\r\ntest 1,prod\r\ntest 2,test'));
  675. });
  676. it('handles aggregations', function () {
  677. const result = {
  678. data: [{count: 3}],
  679. };
  680. expect(downloadAsCsv(result, [countColumn], 'filename.csv')).toContain(
  681. encodeURI('count\r\n3')
  682. );
  683. });
  684. it('quotes unsafe strings', function () {
  685. const result = {
  686. data: [{message: '=HYPERLINK(http://some-bad-website#)'}],
  687. };
  688. expect(downloadAsCsv(result, [messageColumn], 'filename.csv')).toContain(
  689. encodeURIComponent("message\r\n'=HYPERLINK(http://some-bad-website#)")
  690. );
  691. });
  692. it('handles the user column', function () {
  693. const result = {
  694. data: [
  695. {message: 'test 0', user: 'name:baz'},
  696. {message: 'test 1', user: 'id:123'},
  697. {message: 'test 2', user: 'email:test@example.com'},
  698. {message: 'test 3', user: 'ip:127.0.0.1'},
  699. ],
  700. };
  701. expect(downloadAsCsv(result, [messageColumn, userColumn], 'filename.csv')).toContain(
  702. encodeURIComponent(
  703. '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'
  704. )
  705. );
  706. });
  707. it('handles equations', function () {
  708. const result = {
  709. data: [{'equation| count() + count()': 3}],
  710. };
  711. expect(downloadAsCsv(result, [equationColumn], 'filename.csv')).toContain(
  712. encodeURIComponent('count() + count()\r\n3')
  713. );
  714. });
  715. });
  716. describe('eventViewToWidgetQuery', function () {
  717. const state: EventViewOptions = {
  718. ...baseView,
  719. id: '1234',
  720. name: 'best query',
  721. fields: [{field: 'count()', width: 420}, {field: 'project.id'}],
  722. sorts: [{field: 'count', kind: 'desc'}],
  723. query: 'event.type:error',
  724. project: [42],
  725. start: '2019-10-01T00:00:00',
  726. end: '2019-10-02T00:00:00',
  727. statsPeriod: '14d',
  728. environment: ['staging'],
  729. };
  730. it('updates orderby to function format for top N query', function () {
  731. const view = new EventView({...baseView, ...state});
  732. const widgetQuery = eventViewToWidgetQuery({
  733. eventView: view,
  734. displayType: DisplayType.TOP_N,
  735. yAxis: ['count()'],
  736. });
  737. expect(widgetQuery.orderby).toBe('-count()');
  738. });
  739. it('updates orderby to function format for complex function', function () {
  740. const view = new EventView({
  741. ...baseView,
  742. ...state,
  743. fields: [{field: 'count_unique(device.locale)', width: 420}, {field: 'project.id'}],
  744. sorts: [{field: 'count_unique_device_locale', kind: 'desc'}],
  745. });
  746. const widgetQuery = eventViewToWidgetQuery({
  747. eventView: view,
  748. displayType: DisplayType.TABLE,
  749. });
  750. expect(widgetQuery.orderby).toBe('-count_unique(device.locale)');
  751. });
  752. it('updates orderby to field', function () {
  753. const view = new EventView({
  754. ...baseView,
  755. ...state,
  756. sorts: [{field: 'project.id', kind: 'desc'}],
  757. });
  758. const widgetQuery = eventViewToWidgetQuery({
  759. eventView: view,
  760. displayType: DisplayType.TABLE,
  761. });
  762. expect(widgetQuery.orderby).toBe('-project.id');
  763. });
  764. });
  765. describe('generateFieldOptions', function () {
  766. it('generates custom measurement field options', function () {
  767. expect(
  768. generateFieldOptions({
  769. organization: initializeOrg().organization,
  770. customMeasurements: [
  771. {functions: ['p99'], key: 'measurements.custom.measurement'},
  772. ],
  773. })['measurement:measurements.custom.measurement']
  774. ).toEqual({
  775. label: 'measurements.custom.measurement',
  776. value: {
  777. kind: 'custom_measurement',
  778. meta: {
  779. dataType: 'number',
  780. functions: ['p99'],
  781. name: 'measurements.custom.measurement',
  782. },
  783. },
  784. });
  785. });
  786. it('disambiguates tags that are also fields', function () {
  787. expect(
  788. generateFieldOptions({
  789. organization: initializeOrg().organization,
  790. tagKeys: ['environment'],
  791. fieldKeys: ['environment'],
  792. aggregations: {},
  793. })
  794. ).toEqual({
  795. 'field:environment': {
  796. label: 'environment',
  797. value: {
  798. kind: 'field',
  799. meta: {
  800. dataType: 'string',
  801. name: 'environment',
  802. },
  803. },
  804. },
  805. 'tag:environment': {
  806. label: 'environment',
  807. value: {
  808. kind: 'tag',
  809. meta: {
  810. dataType: 'string',
  811. name: 'tags[environment]',
  812. },
  813. },
  814. },
  815. });
  816. });
  817. });