utils.spec.jsx 24 KB

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