utils.spec.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875
  1. import {browserHistory} from 'react-router';
  2. import {Event as EventFixture} from 'sentry-fixture/event';
  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).toEqual('duration');
  99. results = decodeColumnOrder([{field: 'p99()', width: 123}]);
  100. expect(results[0].type).toEqual('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 = TestStubs.location({
  210. query: {
  211. bestCountry: 'canada',
  212. },
  213. });
  214. it('correct query string object pushed to history', function () {
  215. const eventView = new EventView({...baseView, ...state});
  216. pushEventViewToLocation({
  217. location,
  218. nextEventView: eventView,
  219. });
  220. expect(browserHistory.push).toHaveBeenCalledWith(
  221. expect.objectContaining({
  222. query: expect.objectContaining({
  223. id: '1234',
  224. name: 'best query',
  225. field: ['count()', 'project.id'],
  226. widths: [420],
  227. sort: ['-count'],
  228. query: 'event.type:error',
  229. project: [42],
  230. start: '2019-10-01T00:00:00',
  231. end: '2019-10-02T00:00:00',
  232. statsPeriod: '14d',
  233. environment: ['staging'],
  234. yAxis: 'count()',
  235. }),
  236. })
  237. );
  238. });
  239. it('extra query params', function () {
  240. const eventView = new EventView({...baseView, ...state});
  241. pushEventViewToLocation({
  242. location,
  243. nextEventView: eventView,
  244. extraQuery: {
  245. cursor: 'some cursor',
  246. },
  247. });
  248. expect(browserHistory.push).toHaveBeenCalledWith(
  249. expect.objectContaining({
  250. query: expect.objectContaining({
  251. id: '1234',
  252. name: 'best query',
  253. field: ['count()', 'project.id'],
  254. widths: [420],
  255. sort: ['-count'],
  256. query: 'event.type:error',
  257. project: [42],
  258. start: '2019-10-01T00:00:00',
  259. end: '2019-10-02T00:00:00',
  260. statsPeriod: '14d',
  261. environment: ['staging'],
  262. cursor: 'some cursor',
  263. yAxis: 'count()',
  264. }),
  265. })
  266. );
  267. });
  268. });
  269. describe('getExpandedResults()', function () {
  270. const state: EventViewOptions = {
  271. ...baseView,
  272. id: '1234',
  273. name: 'best query',
  274. fields: [
  275. {field: 'count()'},
  276. {field: 'last_seen()'},
  277. {field: 'title'},
  278. {field: 'custom_tag'},
  279. ],
  280. sorts: [{field: 'count', kind: 'desc'}],
  281. query: 'event.type:error',
  282. project: [42],
  283. start: '2019-10-01T00:00:00',
  284. end: '2019-10-02T00:00:00',
  285. statsPeriod: '14d',
  286. environment: ['staging'],
  287. };
  288. it('id should be default column when drilldown results in no columns', () => {
  289. const view = new EventView({
  290. ...baseView,
  291. ...state,
  292. fields: [{field: 'count()'}, {field: 'epm()'}, {field: 'eps()'}],
  293. });
  294. const result = getExpandedResults(view, {}, EventFixture());
  295. expect(result.fields).toEqual([{field: 'id', width: -1}]);
  296. });
  297. it('preserves aggregated fields', () => {
  298. let view = new EventView(state);
  299. let result = getExpandedResults(view, {}, EventFixture());
  300. // id should be omitted as it is an implicit property on unaggregated results.
  301. expect(result.fields).toEqual([
  302. {field: 'timestamp', width: -1},
  303. {field: 'title'},
  304. {field: 'custom_tag'},
  305. ]);
  306. expect(result.query).toEqual('event.type:error title:ApiException');
  307. // de-duplicate transformed columns
  308. view = new EventView({
  309. ...baseView,
  310. ...state,
  311. fields: [
  312. {field: 'count()'},
  313. {field: 'last_seen()'},
  314. {field: 'title'},
  315. {field: 'custom_tag'},
  316. {field: 'count(id)'},
  317. ],
  318. });
  319. result = getExpandedResults(view, {}, EventFixture());
  320. // id should be omitted as it is an implicit property on unaggregated results.
  321. expect(result.fields).toEqual([
  322. {field: 'timestamp', width: -1},
  323. {field: 'title'},
  324. {field: 'custom_tag'},
  325. ]);
  326. // transform aliased fields, & de-duplicate any transforms
  327. view = new EventView({
  328. ...baseView,
  329. ...state,
  330. fields: [
  331. {field: 'last_seen()'}, // expect this to be transformed to timestamp
  332. {field: 'title'},
  333. {field: 'avg(transaction.duration)'}, // expect this to be dropped
  334. {field: 'p50()'},
  335. {field: 'p75()'},
  336. {field: 'p95()'},
  337. {field: 'p99()'},
  338. {field: 'p100()'},
  339. {field: 'p9001()'}, // it's over 9000
  340. {field: 'foobar()'}, // unknown function with no parameter
  341. {field: 'custom_tag'},
  342. {field: 'transaction.duration'}, // should be dropped
  343. {field: 'title'}, // should be dropped
  344. {field: 'unique_count(id)'},
  345. {field: 'apdex(300)'}, // should be dropped
  346. {field: 'user_misery(300)'}, // should be dropped
  347. {field: 'failure_count()'}, // expect this to be transformed to transaction.status
  348. ],
  349. });
  350. result = getExpandedResults(view, {}, EventFixture());
  351. expect(result.fields).toEqual([
  352. {field: 'timestamp', width: -1},
  353. {field: 'title'},
  354. {field: 'transaction.duration', width: -1},
  355. {field: 'custom_tag'},
  356. {field: 'transaction.status', width: -1},
  357. ]);
  358. // transforms pXX functions with optional arguments properly
  359. view = new EventView({
  360. ...baseView,
  361. ...state,
  362. fields: [
  363. {field: 'p50(transaction.duration)'},
  364. {field: 'p75(measurements.foo)'},
  365. {field: 'p95(measurements.bar)'},
  366. {field: 'p99(measurements.fcp)'},
  367. {field: 'p100(measurements.lcp)'},
  368. ],
  369. });
  370. result = getExpandedResults(view, {}, EventFixture());
  371. expect(result.fields).toEqual([
  372. {field: 'transaction.duration', width: -1},
  373. {field: 'measurements.foo', width: -1},
  374. {field: 'measurements.bar', width: -1},
  375. {field: 'measurements.fcp', width: -1},
  376. {field: 'measurements.lcp', width: -1},
  377. ]);
  378. });
  379. it('applies provided additional conditions', () => {
  380. const view = new EventView({
  381. ...baseView,
  382. ...state,
  383. fields: [...state.fields, {field: 'measurements.lcp'}, {field: 'measurements.fcp'}],
  384. });
  385. let result = getExpandedResults(view, {extra: 'condition'}, EventFixture());
  386. expect(result.query).toEqual('event.type:error extra:condition title:ApiException');
  387. // handles user tag values.
  388. result = getExpandedResults(view, {user: 'id:12735'}, EventFixture());
  389. expect(result.query).toEqual('event.type:error user:id:12735 title:ApiException');
  390. result = getExpandedResults(view, {user: 'name:uhoh'}, EventFixture());
  391. expect(result.query).toEqual('event.type:error user:name:uhoh title:ApiException');
  392. // quotes value
  393. result = getExpandedResults(view, {extra: 'has space'}, EventFixture());
  394. expect(result.query).toEqual('event.type:error extra:"has space" title:ApiException');
  395. // appends to existing conditions
  396. result = getExpandedResults(view, {'event.type': 'csp'}, EventFixture());
  397. expect(result.query).toEqual('event.type:csp title:ApiException');
  398. // Includes empty strings
  399. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: ''}));
  400. expect(result.query).toEqual('event.type:error title:ApiException custom_tag:""');
  401. // Includes 0
  402. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: 0}));
  403. expect(result.query).toEqual('event.type:error title:ApiException custom_tag:0');
  404. // Includes null
  405. result = getExpandedResults(view, {}, EventFixture({id: '0', custom_tag: null}));
  406. expect(result.query).toEqual('event.type:error title:ApiException custom_tag:""');
  407. // Handles measurements while ignoring null values
  408. result = getExpandedResults(
  409. view,
  410. {},
  411. // The type on this is wrong, the actual type is ReactText which is just string|number
  412. // however we seem to have tests that test for null values as well, hence the expect error
  413. // @ts-expect-error
  414. {'measurements.lcp': 2, 'measurements.fcp': null}
  415. );
  416. expect(result.query).toEqual('event.type:error measurements.lcp:2');
  417. });
  418. it('removes any aggregates in either search conditions or extra conditions', () => {
  419. const view = new EventView({...state, query: 'event.type:error count(id):<10'});
  420. const result = getExpandedResults(view, {'count(id)': '>2'}, EventFixture());
  421. expect(result.query).toEqual('event.type:error title:ApiException');
  422. });
  423. it('applies conditions from dataRow map structure based on fields', () => {
  424. const view = new EventView(state);
  425. const result = getExpandedResults(
  426. view,
  427. {extra: 'condition'},
  428. EventFixture({title: 'Event title'})
  429. );
  430. expect(result.query).toEqual('event.type:error extra:condition title:"Event title"');
  431. });
  432. it('applies tag key conditions from event data', () => {
  433. const view = new EventView(state);
  434. const event = EventFixture({
  435. type: 'error',
  436. tags: [
  437. {key: 'nope', value: 'nope'},
  438. {key: 'custom_tag', value: 'tag_value'},
  439. ],
  440. });
  441. const result = getExpandedResults(view, {}, event);
  442. expect(result.query).toEqual(
  443. 'event.type:error title:ApiException custom_tag:tag_value'
  444. );
  445. });
  446. it('generate eventview from an empty eventview', () => {
  447. const view = EventView.fromLocation(TestStubs.location());
  448. const result = getExpandedResults(view, {some_tag: 'value'}, EventFixture());
  449. expect(result.fields).toEqual([]);
  450. expect(result.query).toEqual('some_tag:value');
  451. });
  452. it('removes equations on aggregates', () => {
  453. const view = new EventView({
  454. ...baseView,
  455. ...state,
  456. fields: [
  457. {field: 'count()'},
  458. {field: 'equation|count() / 2'},
  459. {field: 'equation|(count() - count()) + 5'},
  460. ],
  461. });
  462. const result = getExpandedResults(view, {});
  463. expect(result.fields).toEqual([
  464. {
  465. field: 'id',
  466. width: -1,
  467. },
  468. ]);
  469. });
  470. it('keeps equations without aggregates', () => {
  471. const view = new EventView({
  472. ...baseView,
  473. ...state,
  474. fields: [{field: 'count()'}, {field: 'equation|transaction.duration / 2'}],
  475. });
  476. const result = getExpandedResults(view, {});
  477. expect(result.fields).toEqual([
  478. {
  479. field: 'equation|transaction.duration / 2',
  480. width: -1,
  481. },
  482. ]);
  483. });
  484. it('applies array value conditions from event data', () => {
  485. const view = new EventView({
  486. ...baseView,
  487. ...state,
  488. fields: [...state.fields, {field: 'error.type'}],
  489. });
  490. const event = EventFixture({
  491. type: 'error',
  492. tags: [
  493. {key: 'nope', value: 'nope'},
  494. {key: 'custom_tag', value: 'tag_value'},
  495. ],
  496. 'error.type': ['DeadSystem Exception', 'RuntimeException', 'RuntimeException'],
  497. });
  498. const result = getExpandedResults(view, {}, event);
  499. expect(result.query).toEqual(
  500. 'event.type:error title:ApiException custom_tag:tag_value error.type:"DeadSystem Exception" error.type:RuntimeException error.type:RuntimeException'
  501. );
  502. });
  503. it('applies project condition to project property', () => {
  504. const view = new EventView(state);
  505. const result = getExpandedResults(view, {'project.id': '1'});
  506. expect(result.query.includes('event.type:error')).toBeTruthy();
  507. expect(result.project).toEqual([42, 1]);
  508. });
  509. it('applies environment condition to environment property', () => {
  510. const view = new EventView(state);
  511. const result = getExpandedResults(view, {environment: 'dev'});
  512. expect(result.query).toEqual('event.type:error');
  513. expect(result.environment).toEqual(['staging', 'dev']);
  514. });
  515. it('applies tags that overlap PageFilters state', () => {
  516. const view = new EventView({
  517. ...baseView,
  518. ...state,
  519. fields: [{field: 'project'}, {field: 'environment'}, {field: 'title'}],
  520. });
  521. const event = EventFixture({
  522. title: 'something bad',
  523. timestamp: '2020-02-13T17:05:46+00:00',
  524. tags: [
  525. {key: 'project', value: '12345'},
  526. {key: 'environment', value: 'earth'},
  527. ],
  528. });
  529. const result = getExpandedResults(view, {}, event);
  530. expect(result.query).toEqual(
  531. 'event.type:error tags[project]:12345 tags[environment]:earth title:"something bad"'
  532. );
  533. expect(result.project).toEqual([42]);
  534. expect(result.environment).toEqual(['staging']);
  535. });
  536. it('applies the normalized user tag', function () {
  537. const view = new EventView({
  538. ...baseView,
  539. ...state,
  540. fields: [{field: 'user'}, {field: 'title'}],
  541. });
  542. let event = EventFixture({
  543. title: 'something bad',
  544. // user context should be ignored.
  545. user: {
  546. id: 1234,
  547. username: 'uhoh',
  548. },
  549. tags: [{key: 'user', value: 'id:1234'}],
  550. });
  551. let result = getExpandedResults(view, {}, event);
  552. expect(result.query).toEqual('event.type:error user:id:1234 title:"something bad"');
  553. event = EventFixture({
  554. title: 'something bad',
  555. tags: [{key: 'user', value: '1234'}],
  556. });
  557. result = getExpandedResults(view, {}, event);
  558. expect(result.query).toEqual('event.type:error user:1234 title:"something bad"');
  559. });
  560. it('applies the user field in a table row', function () {
  561. const view = new EventView({
  562. ...state,
  563. fields: [{field: 'user'}, {field: 'title'}],
  564. });
  565. const event = EventFixture({
  566. title: 'something bad',
  567. user: 'id:1234',
  568. });
  569. const result = getExpandedResults(view, {}, event);
  570. expect(result.query).toEqual('event.type:error user:id:1234 title:"something bad"');
  571. });
  572. it('normalizes the timestamp field', () => {
  573. const view = new EventView({
  574. ...state,
  575. fields: [{field: 'timestamp'}],
  576. sorts: [{field: 'timestamp', kind: 'desc'}],
  577. });
  578. const event = EventFixture({
  579. type: 'error',
  580. timestamp: '2020-02-13T17:05:46+00:00',
  581. });
  582. const result = getExpandedResults(view, {}, event);
  583. expect(result.query).toEqual('event.type:error timestamp:2020-02-13T17:05:46');
  584. });
  585. it('does not duplicate conditions', () => {
  586. const view = new EventView({
  587. ...baseView,
  588. ...state,
  589. query: 'event.type:error title:bogus',
  590. });
  591. const event = EventFixture({
  592. title: 'bogus',
  593. });
  594. const result = getExpandedResults(view, {trace: 'abc123'}, event);
  595. expect(result.query).toEqual('event.type:error trace:abc123 title:bogus');
  596. });
  597. it('applies project as condition if present', () => {
  598. const view = new EventView({
  599. ...baseView,
  600. ...state,
  601. query: '',
  602. fields: [{field: 'project'}],
  603. });
  604. const event = EventFixture({project: 'whoosh'});
  605. const result = getExpandedResults(view, {}, event);
  606. expect(result.query).toEqual('project:whoosh');
  607. });
  608. it('applies project name as condition if present', () => {
  609. const view = new EventView({
  610. ...baseView,
  611. ...state,
  612. query: '',
  613. fields: [{field: 'project.name'}],
  614. });
  615. const event = EventFixture({'project.name': 'whoosh'});
  616. const result = getExpandedResults(view, {}, event);
  617. expect(result.query).toEqual('project.name:whoosh');
  618. });
  619. it('should not trim values that need to be quoted', () => {
  620. const view = new EventView({
  621. ...baseView,
  622. ...state,
  623. query: '',
  624. fields: [{field: 'title'}],
  625. });
  626. // needs to be quoted because of whitespace in middle
  627. const event = EventFixture({title: 'hello there '});
  628. const result = getExpandedResults(view, {}, event);
  629. expect(result.query).toEqual('title:"hello there "');
  630. });
  631. it('should add environment from the data row', () => {
  632. const view = new EventView({
  633. ...baseView,
  634. ...state,
  635. environment: [],
  636. query: '',
  637. fields: [{field: 'environment'}],
  638. });
  639. expect(view.environment).toEqual([]);
  640. const event = EventFixture({environment: 'staging'});
  641. const result = getExpandedResults(view, {}, event);
  642. expect(result.environment).toEqual(['staging']);
  643. });
  644. it('should not add duplicate environment', () => {
  645. const view = new EventView({
  646. ...baseView,
  647. ...state,
  648. query: '',
  649. fields: [{field: 'environment'}],
  650. });
  651. expect(view.environment).toEqual(['staging']);
  652. const event = EventFixture({environment: 'staging'});
  653. const result = getExpandedResults(view, {}, event);
  654. expect(result.environment).toEqual(['staging']);
  655. });
  656. });
  657. describe('downloadAsCsv', function () {
  658. const messageColumn = {key: 'message', name: 'message'};
  659. const environmentColumn = {key: 'environment', name: 'environment'};
  660. const countColumn = {key: 'count', name: 'count'};
  661. const userColumn = {key: 'user', name: 'user'};
  662. const equationColumn = {key: 'equation| count() + count()', name: 'count() + count()'};
  663. it('handles raw data', function () {
  664. const result = {
  665. data: [
  666. {message: 'test 1', environment: 'prod'},
  667. {message: 'test 2', environment: 'test'},
  668. ],
  669. };
  670. expect(
  671. downloadAsCsv(result, [messageColumn, environmentColumn], 'filename.csv')
  672. ).toContain(encodeURIComponent('message,environment\r\ntest 1,prod\r\ntest 2,test'));
  673. });
  674. it('handles aggregations', function () {
  675. const result = {
  676. data: [{count: 3}],
  677. };
  678. expect(downloadAsCsv(result, [countColumn], 'filename.csv')).toContain(
  679. encodeURI('count\r\n3')
  680. );
  681. });
  682. it('quotes unsafe strings', function () {
  683. const result = {
  684. data: [{message: '=HYPERLINK(http://some-bad-website#)'}],
  685. };
  686. expect(downloadAsCsv(result, [messageColumn], 'filename.csv')).toContain(
  687. encodeURIComponent("message\r\n'=HYPERLINK(http://some-bad-website#)")
  688. );
  689. });
  690. it('handles the user column', function () {
  691. const result = {
  692. data: [
  693. {message: 'test 0', user: 'name:baz'},
  694. {message: 'test 1', user: 'id:123'},
  695. {message: 'test 2', user: 'email:test@example.com'},
  696. {message: 'test 3', user: 'ip:127.0.0.1'},
  697. ],
  698. };
  699. expect(downloadAsCsv(result, [messageColumn, userColumn], 'filename.csv')).toContain(
  700. encodeURIComponent(
  701. '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'
  702. )
  703. );
  704. });
  705. it('handles equations', function () {
  706. const result = {
  707. data: [{'equation| count() + count()': 3}],
  708. };
  709. expect(downloadAsCsv(result, [equationColumn], 'filename.csv')).toContain(
  710. encodeURIComponent('count() + count()\r\n3')
  711. );
  712. });
  713. });
  714. describe('eventViewToWidgetQuery', function () {
  715. const state: EventViewOptions = {
  716. ...baseView,
  717. id: '1234',
  718. name: 'best query',
  719. fields: [{field: 'count()', width: 420}, {field: 'project.id'}],
  720. sorts: [{field: 'count', kind: 'desc'}],
  721. query: 'event.type:error',
  722. project: [42],
  723. start: '2019-10-01T00:00:00',
  724. end: '2019-10-02T00:00:00',
  725. statsPeriod: '14d',
  726. environment: ['staging'],
  727. };
  728. it('updates orderby to function format for top N query', function () {
  729. const view = new EventView({...baseView, ...state});
  730. const widgetQuery = eventViewToWidgetQuery({
  731. eventView: view,
  732. displayType: DisplayType.TOP_N,
  733. yAxis: ['count()'],
  734. });
  735. expect(widgetQuery.orderby).toEqual('-count()');
  736. });
  737. it('updates orderby to function format for complex function', function () {
  738. const view = new EventView({
  739. ...baseView,
  740. ...state,
  741. fields: [{field: 'count_unique(device.locale)', width: 420}, {field: 'project.id'}],
  742. sorts: [{field: 'count_unique_device_locale', kind: 'desc'}],
  743. });
  744. const widgetQuery = eventViewToWidgetQuery({
  745. eventView: view,
  746. displayType: DisplayType.TABLE,
  747. });
  748. expect(widgetQuery.orderby).toEqual('-count_unique(device.locale)');
  749. });
  750. it('updates orderby to field', function () {
  751. const view = new EventView({
  752. ...baseView,
  753. ...state,
  754. sorts: [{field: 'project.id', kind: 'desc'}],
  755. });
  756. const widgetQuery = eventViewToWidgetQuery({
  757. eventView: view,
  758. displayType: DisplayType.TABLE,
  759. });
  760. expect(widgetQuery.orderby).toEqual('-project.id');
  761. });
  762. });
  763. describe('generateFieldOptions', function () {
  764. it('generates custom measurement field options', function () {
  765. expect(
  766. generateFieldOptions({
  767. organization: initializeOrg().organization,
  768. customMeasurements: [
  769. {functions: ['p99'], key: 'measurements.custom.measurement'},
  770. ],
  771. })['measurement:measurements.custom.measurement']
  772. ).toEqual({
  773. label: 'measurements.custom.measurement',
  774. value: {
  775. kind: 'custom_measurement',
  776. meta: {
  777. dataType: 'number',
  778. functions: ['p99'],
  779. name: 'measurements.custom.measurement',
  780. },
  781. },
  782. });
  783. });
  784. });