utils.spec.tsx 13 KB

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