utils.spec.tsx 12 KB

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