utils.spec.tsx 27 KB

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