utils.spec.tsx 12 KB

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