utils.spec.tsx 12 KB

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