utils.spec.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. import {
  2. DashboardDetails,
  3. DisplayType,
  4. Widget,
  5. WidgetType,
  6. } from 'sentry/views/dashboards/types';
  7. import {
  8. constructWidgetFromQuery,
  9. eventViewFromWidget,
  10. flattenErrors,
  11. getDashboardsMEPQueryParams,
  12. getFieldsFromEquations,
  13. getNumEquations,
  14. getWidgetDiscoverUrl,
  15. getWidgetIssueUrl,
  16. hasUnsavedFilterChanges,
  17. isCustomMeasurementWidget,
  18. isWidgetUsingTransactionName,
  19. } from 'sentry/views/dashboards/utils';
  20. describe('Dashboards util', () => {
  21. const selection = {
  22. datetime: {
  23. period: '7d',
  24. utc: null,
  25. start: null,
  26. end: null,
  27. },
  28. environments: [],
  29. projects: [],
  30. };
  31. describe('constructWidgetFromQuery', () => {
  32. let baseQuery;
  33. beforeEach(() => {
  34. baseQuery = {
  35. displayType: 'line',
  36. interval: '5m',
  37. queryConditions: ['title:test', 'event.type:test'],
  38. queryFields: ['count()', 'failure_count()'],
  39. queryAggregates: ['count()', 'failure_count()'],
  40. queryColumns: [],
  41. queryNames: ['1', '2'],
  42. queryOrderby: '',
  43. title: 'Widget Title',
  44. };
  45. });
  46. it('returns a widget when given a valid query', () => {
  47. const widget = constructWidgetFromQuery(baseQuery);
  48. expect(widget?.displayType).toEqual(DisplayType.LINE);
  49. expect(widget?.interval).toEqual('5m');
  50. expect(widget?.title).toEqual('Widget Title');
  51. expect(widget?.queries).toEqual([
  52. {
  53. name: '1',
  54. fields: ['count()', 'failure_count()'],
  55. aggregates: ['count()', 'failure_count()'],
  56. columns: [],
  57. conditions: 'title:test',
  58. orderby: '',
  59. },
  60. {
  61. name: '2',
  62. fields: ['count()', 'failure_count()'],
  63. aggregates: ['count()', 'failure_count()'],
  64. columns: [],
  65. conditions: 'event.type:test',
  66. orderby: '',
  67. },
  68. ]);
  69. expect(widget?.widgetType).toEqual('discover');
  70. });
  71. it('returns undefined if query is missing title', () => {
  72. baseQuery.title = '';
  73. const widget = constructWidgetFromQuery(baseQuery);
  74. expect(widget).toBeUndefined();
  75. });
  76. it('returns undefined if query is missing interval', () => {
  77. baseQuery.interval = '';
  78. const widget = constructWidgetFromQuery(baseQuery);
  79. expect(widget).toBeUndefined();
  80. });
  81. it('returns undefined if query is missing displayType', () => {
  82. baseQuery.displayType = '';
  83. const widget = constructWidgetFromQuery(baseQuery);
  84. expect(widget).toBeUndefined();
  85. });
  86. it('returns a widget when given string fields and conditions', () => {
  87. baseQuery.queryConditions = 'title:test';
  88. baseQuery.queryFields = 'count()';
  89. baseQuery.queryAggregates = 'count()';
  90. const widget = constructWidgetFromQuery(baseQuery);
  91. expect(widget?.displayType).toEqual(DisplayType.LINE);
  92. expect(widget?.interval).toEqual('5m');
  93. expect(widget?.title).toEqual('Widget Title');
  94. expect(widget?.queries).toEqual([
  95. {
  96. name: '1',
  97. fields: ['count()'],
  98. aggregates: ['count()'],
  99. columns: [],
  100. conditions: 'title:test',
  101. orderby: '',
  102. },
  103. ]);
  104. });
  105. });
  106. describe('eventViewFromWidget', () => {
  107. let widget;
  108. beforeEach(() => {
  109. widget = {
  110. title: 'Test Query',
  111. displayType: DisplayType.WORLD_MAP,
  112. widgetType: WidgetType.DISCOVER,
  113. interval: '5m',
  114. queries: [
  115. {
  116. name: '',
  117. conditions: '',
  118. fields: ['count()'],
  119. aggregates: ['count()'],
  120. columns: [],
  121. orderby: '',
  122. },
  123. ],
  124. };
  125. });
  126. it('attaches a geo.country_code condition and field to a World Map widget if it does not already have one', () => {
  127. const eventView = eventViewFromWidget(
  128. widget.title,
  129. widget.queries[0],
  130. selection,
  131. widget.displayType
  132. );
  133. expect(eventView.fields[0].field).toEqual('geo.country_code');
  134. expect(eventView.fields[1].field).toEqual('count()');
  135. expect(eventView.query).toEqual('has:geo.country_code');
  136. });
  137. it('does not attach geo.country_code condition and field to a World Map widget if it already has one', () => {
  138. widget.queries.fields = ['geo.country_code', 'count()'];
  139. widget.conditions = 'has:geo.country_code';
  140. const eventView = eventViewFromWidget(
  141. widget.title,
  142. widget.queries[0],
  143. selection,
  144. widget.displayType
  145. );
  146. expect(eventView.fields[0].field).toEqual('geo.country_code');
  147. expect(eventView.fields[1].field).toEqual('count()');
  148. expect(eventView.query).toEqual('has:geo.country_code');
  149. });
  150. it('handles sorts in function format', () => {
  151. const query = {...widget.queries[0], orderby: '-count()'};
  152. const eventView = eventViewFromWidget(
  153. widget.title,
  154. query,
  155. selection,
  156. widget.displayType
  157. );
  158. expect(eventView.fields[0].field).toEqual('geo.country_code');
  159. expect(eventView.fields[1].field).toEqual('count()');
  160. expect(eventView.query).toEqual('has:geo.country_code');
  161. expect(eventView.sorts).toEqual([{field: 'count', kind: 'desc'}]);
  162. });
  163. });
  164. describe('getFieldsFromEquations', function () {
  165. it('returns a list of fields that includes individual terms of provided equations', () => {
  166. const fields = [
  167. 'equation|(count_if(transaction.duration,greater,300) / count()) * 100',
  168. 'equation|(count_if(transaction.duration,lessOrEquals,300) / count()) * 100',
  169. ];
  170. expect(getFieldsFromEquations(fields)).toEqual(
  171. expect.arrayContaining([
  172. 'count_if(transaction.duration,lessOrEquals,300)',
  173. 'count()',
  174. 'count_if(transaction.duration,greater,300)',
  175. ])
  176. );
  177. });
  178. });
  179. describe('getWidgetDiscoverUrl', function () {
  180. let widget;
  181. beforeEach(() => {
  182. widget = {
  183. title: 'Test Query',
  184. displayType: DisplayType.LINE,
  185. widgetType: WidgetType.DISCOVER,
  186. interval: '5m',
  187. queries: [
  188. {
  189. name: '',
  190. conditions: '',
  191. fields: ['count()'],
  192. aggregates: ['count()'],
  193. columns: [],
  194. orderby: '',
  195. },
  196. ],
  197. };
  198. });
  199. it('returns the discover url of the widget query', () => {
  200. const url = getWidgetDiscoverUrl(widget, selection, TestStubs.Organization());
  201. expect(url).toEqual(
  202. '/organizations/org-slug/discover/results/?field=count%28%29&name=Test%20Query&query=&statsPeriod=7d&yAxis=count%28%29'
  203. );
  204. });
  205. it('returns the discover url of a topn widget query', () => {
  206. widget = {
  207. ...widget,
  208. ...{
  209. displayType: DisplayType.TOP_N,
  210. queries: [
  211. {
  212. name: '',
  213. conditions: 'error.unhandled:true',
  214. fields: ['error.type', 'count()'],
  215. aggregates: ['count()'],
  216. columns: ['error.type'],
  217. orderby: '-count',
  218. },
  219. ],
  220. },
  221. };
  222. const url = getWidgetDiscoverUrl(widget, selection, TestStubs.Organization());
  223. expect(url).toEqual(
  224. '/organizations/org-slug/discover/results/?display=top5&field=error.type&field=count%28%29&name=Test%20Query&query=error.unhandled%3Atrue&sort=-count&statsPeriod=7d&yAxis=count%28%29'
  225. );
  226. });
  227. });
  228. describe('getWidgetIssueUrl', function () {
  229. let widget;
  230. beforeEach(() => {
  231. widget = {
  232. title: 'Test Query',
  233. displayType: DisplayType.TABLE,
  234. widgetType: WidgetType.ISSUE,
  235. interval: '5m',
  236. queries: [
  237. {
  238. name: '',
  239. conditions: 'is:unresolved',
  240. fields: ['events'],
  241. orderby: 'date',
  242. },
  243. ],
  244. };
  245. });
  246. it('returns the issue url of the widget query', () => {
  247. const url = getWidgetIssueUrl(widget, selection, TestStubs.Organization());
  248. expect(url).toEqual(
  249. '/organizations/org-slug/issues/?query=is%3Aunresolved&sort=date&statsPeriod=7d'
  250. );
  251. });
  252. });
  253. describe('flattenErrors', function () {
  254. it('flattens nested errors', () => {
  255. const errorResponse = {
  256. widgets: [
  257. {
  258. title: ['Ensure this field has no more than 3 characters.'],
  259. },
  260. ],
  261. };
  262. expect(flattenErrors(errorResponse, {})).toEqual({
  263. title: 'Ensure this field has no more than 3 characters.',
  264. });
  265. });
  266. it('does not spread error strings', () => {
  267. const errorResponse = 'Dashboard title already taken.';
  268. expect(flattenErrors(errorResponse, {})).toEqual({
  269. error: 'Dashboard title already taken.',
  270. });
  271. });
  272. });
  273. describe('getDashboardsMEPQueryParams', function () {
  274. it('returns correct params if enabled', function () {
  275. expect(getDashboardsMEPQueryParams(true)).toEqual({
  276. dataset: 'metricsEnhanced',
  277. });
  278. });
  279. it('returns empty object if disabled', function () {
  280. expect(getDashboardsMEPQueryParams(false)).toEqual({});
  281. });
  282. });
  283. describe('getNumEquations', function () {
  284. it('returns 0 if there are no equations', function () {
  285. expect(getNumEquations(['count()', 'epm()', 'count_unique(user)'])).toBe(0);
  286. });
  287. it('returns the count of equations if there are multiple', function () {
  288. expect(
  289. getNumEquations([
  290. 'count()',
  291. 'equation|count_unique(user) * 2',
  292. 'count_unique(user)',
  293. 'equation|count_unique(user) * 3',
  294. ])
  295. ).toBe(2);
  296. });
  297. it('returns 0 if the possible equations array is empty', function () {
  298. expect(getNumEquations([])).toBe(0);
  299. });
  300. });
  301. describe('isCustomMeasurementWidget', function () {
  302. it('returns false on a non custom measurement widget', function () {
  303. const widget: Widget = {
  304. title: 'Title',
  305. interval: '5m',
  306. displayType: DisplayType.LINE,
  307. widgetType: WidgetType.DISCOVER,
  308. queries: [
  309. {
  310. conditions: '',
  311. fields: [],
  312. aggregates: ['count()', 'p99(measurements.lcp)'],
  313. columns: [],
  314. name: 'widget',
  315. orderby: '',
  316. },
  317. ],
  318. };
  319. expect(isCustomMeasurementWidget(widget)).toBe(false);
  320. });
  321. it('returns true on a custom measurement widget', function () {
  322. const widget: Widget = {
  323. title: 'Title',
  324. interval: '5m',
  325. displayType: DisplayType.LINE,
  326. widgetType: WidgetType.DISCOVER,
  327. queries: [
  328. {
  329. conditions: '',
  330. fields: [],
  331. aggregates: ['p99(measurements.custom.measurement)'],
  332. columns: [],
  333. name: 'widget',
  334. orderby: '',
  335. },
  336. ],
  337. };
  338. expect(isCustomMeasurementWidget(widget)).toBe(true);
  339. });
  340. });
  341. describe('hasUnsavedFilterChanges', function () {
  342. it('ignores the order of projects', function () {
  343. const initialDashboard = {
  344. projects: [1, 2],
  345. } as DashboardDetails;
  346. const location = {
  347. ...TestStubs.location(),
  348. query: {
  349. project: ['2', '1'],
  350. },
  351. };
  352. expect(hasUnsavedFilterChanges(initialDashboard, location)).toBe(false);
  353. });
  354. it('ignores the order of environments', function () {
  355. const initialDashboard = {
  356. environment: ['alpha', 'beta'],
  357. } as DashboardDetails;
  358. const location = {
  359. ...TestStubs.location(),
  360. query: {
  361. environment: ['beta', 'alpha'],
  362. },
  363. };
  364. expect(hasUnsavedFilterChanges(initialDashboard, location)).toBe(false);
  365. });
  366. it('ignores the order of releases', function () {
  367. const initialDashboard = {
  368. filters: {
  369. release: ['v1', 'v2'],
  370. },
  371. } as DashboardDetails;
  372. expect(
  373. hasUnsavedFilterChanges(initialDashboard, {
  374. ...TestStubs.location(),
  375. query: {
  376. release: ['v2', 'v1'],
  377. },
  378. })
  379. ).toBe(false);
  380. });
  381. });
  382. });
  383. describe('isWidgetUsingTransactionName', () => {
  384. let baseQuery;
  385. beforeEach(() => {
  386. baseQuery = {
  387. displayType: 'line',
  388. interval: '5m',
  389. queryConditions: ['title:test', 'event.type:test'],
  390. queryFields: ['count()', 'failure_count()'],
  391. queryNames: ['1', '2'],
  392. queryOrderby: '',
  393. title: 'Widget Title',
  394. };
  395. });
  396. it('returns false when widget does not use transaction', () => {
  397. const widget = constructWidgetFromQuery(baseQuery)!;
  398. expect(isWidgetUsingTransactionName(widget)).toEqual(false);
  399. });
  400. it('returns true when widget uses transaction as a selected field', () => {
  401. baseQuery.queryFields.push('transaction');
  402. const widget = constructWidgetFromQuery(baseQuery)!;
  403. expect(isWidgetUsingTransactionName(widget)).toEqual(true);
  404. });
  405. it('returns true when widget uses transaction as part of the query filter', () => {
  406. baseQuery.queryConditions = ['transaction:test'];
  407. const widget = constructWidgetFromQuery(baseQuery)!;
  408. expect(isWidgetUsingTransactionName(widget)).toEqual(true);
  409. });
  410. });