utils.spec.tsx 11 KB

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