utils.spec.tsx 27 KB

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