utils.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905
  1. import type {Location, Query} from 'history';
  2. import * as Papa from 'papaparse';
  3. import {openAddToDashboardModal} from 'sentry/actionCreators/modal';
  4. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  5. import {URL_PARAM} from 'sentry/constants/pageFilters';
  6. import {t} from 'sentry/locale';
  7. import type {SelectValue} from 'sentry/types/core';
  8. import type {Event} from 'sentry/types/event';
  9. import type {InjectedRouter} from 'sentry/types/legacyReactRouter';
  10. import type {
  11. NewQuery,
  12. Organization,
  13. OrganizationSummary,
  14. } from 'sentry/types/organization';
  15. import type {Project} from 'sentry/types/project';
  16. import {defined, urlEncode} from 'sentry/utils';
  17. import {getUtcDateString} from 'sentry/utils/dates';
  18. import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
  19. import type {EventData} from 'sentry/utils/discover/eventView';
  20. import type EventView from 'sentry/utils/discover/eventView';
  21. import type {
  22. Aggregation,
  23. Column,
  24. ColumnType,
  25. ColumnValueType,
  26. Field,
  27. } from 'sentry/utils/discover/fields';
  28. import {
  29. aggregateFunctionOutputType,
  30. AGGREGATIONS,
  31. explodeFieldString,
  32. getAggregateAlias,
  33. getAggregateArg,
  34. getColumnsAndAggregates,
  35. getEquation,
  36. isAggregateEquation,
  37. isAggregateFieldOrEquation,
  38. isEquation,
  39. isMeasurement,
  40. isSpanOperationBreakdownField,
  41. measurementType,
  42. PROFILING_FIELDS,
  43. TRACING_FIELDS,
  44. } from 'sentry/utils/discover/fields';
  45. import {DisplayModes, SavedQueryDatasets, TOP_N} from 'sentry/utils/discover/types';
  46. import {getTitle} from 'sentry/utils/events';
  47. import {DISCOVER_FIELDS, FieldValueType, getFieldDefinition} from 'sentry/utils/fields';
  48. import localStorage from 'sentry/utils/localStorage';
  49. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  50. import type {ReactRouter3Navigate} from 'sentry/utils/useNavigate';
  51. import {convertWidgetToBuilderStateParams} from 'sentry/views/dashboards/widgetBuilder/utils/convertWidgetToBuilderStateParams';
  52. import {
  53. type DashboardWidgetSource,
  54. DisplayType,
  55. type Widget,
  56. type WidgetQuery,
  57. WidgetType,
  58. } from '../dashboards/types';
  59. import {transactionSummaryRouteWithQuery} from '../performance/transactionSummary/utils';
  60. import {displayModeToDisplayType} from './savedQuery/utils';
  61. import type {FieldValue, TableColumn} from './table/types';
  62. import {FieldValueKind} from './table/types';
  63. import {getAllViews, getTransactionViews, getWebVitalsViews} from './data';
  64. export type QueryWithColumnState =
  65. | Query
  66. | {
  67. field: string | string[] | null | undefined;
  68. sort: string | string[] | null | undefined;
  69. };
  70. const TEMPLATE_TABLE_COLUMN: TableColumn<string> = {
  71. key: '',
  72. name: '',
  73. type: 'never',
  74. isSortable: false,
  75. column: Object.freeze({kind: 'field', field: ''}),
  76. width: COL_WIDTH_UNDEFINED,
  77. };
  78. export function decodeColumnOrder(fields: readonly Field[]): Array<TableColumn<string>> {
  79. return fields.map((f: Field) => {
  80. const column: TableColumn<string> = {...TEMPLATE_TABLE_COLUMN};
  81. const col = explodeFieldString(f.field, f.alias);
  82. const columnName = f.field;
  83. if (isEquation(f.field)) {
  84. column.key = f.field;
  85. column.name = getEquation(columnName);
  86. column.type = 'number';
  87. } else {
  88. column.key = columnName;
  89. column.name = columnName;
  90. }
  91. column.width = f.width || COL_WIDTH_UNDEFINED;
  92. if (col.kind === 'function') {
  93. // Aggregations can have a strict outputType or they can inherit from their field.
  94. // Otherwise use the FIELDS data to infer types.
  95. const outputType = aggregateFunctionOutputType(col.function[0], col.function[1]);
  96. if (outputType !== null) {
  97. column.type = outputType;
  98. }
  99. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  100. const aggregate = AGGREGATIONS[col.function[0]];
  101. column.isSortable = aggregate?.isSortable;
  102. } else if (col.kind === 'field') {
  103. if (getFieldDefinition(col.field) !== null) {
  104. column.type = getFieldDefinition(col.field)?.valueType as ColumnValueType;
  105. } else if (isMeasurement(col.field)) {
  106. column.type = measurementType(col.field);
  107. } else if (isSpanOperationBreakdownField(col.field)) {
  108. column.type = 'duration';
  109. }
  110. }
  111. column.column = col;
  112. return column;
  113. });
  114. }
  115. export function pushEventViewToLocation(props: {
  116. location: Location;
  117. navigate: ReactRouter3Navigate;
  118. nextEventView: EventView;
  119. extraQuery?: Query;
  120. }) {
  121. const {navigate, location, nextEventView} = props;
  122. const extraQuery = props.extraQuery || {};
  123. const queryStringObject = nextEventView.generateQueryStringObject();
  124. navigate({
  125. ...location,
  126. query: {
  127. ...extraQuery,
  128. ...queryStringObject,
  129. },
  130. });
  131. }
  132. export function generateTitle({
  133. eventView,
  134. event,
  135. isHomepage,
  136. }: {
  137. eventView: EventView;
  138. event?: Event;
  139. isHomepage?: boolean;
  140. }) {
  141. const titles = [t('Discover')];
  142. if (isHomepage) {
  143. return t('Discover');
  144. }
  145. const eventViewName = eventView.name;
  146. if (typeof eventViewName === 'string' && String(eventViewName).trim().length > 0) {
  147. titles.push(String(eventViewName).trim());
  148. }
  149. const eventTitle = event ? getTitle(event).title : undefined;
  150. if (eventTitle) {
  151. titles.push(eventTitle);
  152. }
  153. titles.reverse();
  154. return titles.join(' — ');
  155. }
  156. export function getPrebuiltQueries(organization: Organization) {
  157. const views = [...getAllViews(organization)];
  158. if (organization.features.includes('performance-view')) {
  159. // insert transactions queries at index 2
  160. views.splice(2, 0, ...getTransactionViews(organization));
  161. views.push(...getWebVitalsViews(organization));
  162. }
  163. return views;
  164. }
  165. function disableMacros(value: string | null | boolean | number) {
  166. const unsafeCharacterRegex = /^[\=\+\-\@]/;
  167. if (typeof value === 'string' && `${value}`.match(unsafeCharacterRegex)) {
  168. return `'${value}`;
  169. }
  170. return value;
  171. }
  172. export function downloadAsCsv(tableData: any, columnOrder: any, filename: any) {
  173. const {data} = tableData;
  174. const headings = columnOrder.map((column: any) => column.name);
  175. const keys = columnOrder.map((column: any) => column.key);
  176. const csvContent = Papa.unparse({
  177. fields: headings,
  178. data: data.map((row: any) =>
  179. keys.map((key: any) => {
  180. return disableMacros(row[key]);
  181. })
  182. ),
  183. });
  184. // Need to also manually replace # since encodeURI skips them
  185. const encodedDataUrl = `data:text/csv;charset=utf8,${encodeURIComponent(csvContent)}`;
  186. // Create a download link then click it, this is so we can get a filename
  187. const link = document.createElement('a');
  188. const now = new Date();
  189. link.setAttribute('href', encodedDataUrl);
  190. link.setAttribute('download', `${filename} ${getUtcDateString(now)}.csv`);
  191. link.click();
  192. link.remove();
  193. // Make testing easier
  194. return encodedDataUrl;
  195. }
  196. const ALIASED_AGGREGATES_COLUMN = {
  197. last_seen: 'timestamp',
  198. failure_count: 'transaction.status',
  199. };
  200. /**
  201. * Convert an aggregate into the resulting column from a drilldown action.
  202. * The result is null if the drilldown results in the aggregate being removed.
  203. */
  204. function drilldownAggregate(
  205. func: Extract<Column, {kind: 'function'}>
  206. ): Extract<Column, {kind: 'field'}> | null {
  207. const key = func.function[0];
  208. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  209. const aggregation = AGGREGATIONS[key];
  210. let column = func.function[1];
  211. if (ALIASED_AGGREGATES_COLUMN.hasOwnProperty(key)) {
  212. // Some aggregates are just shortcuts to other aggregates with
  213. // predefined arguments so we can directly map them to the result.
  214. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  215. column = ALIASED_AGGREGATES_COLUMN[key];
  216. } else if (aggregation?.parameters?.[0]) {
  217. const parameter = aggregation.parameters[0];
  218. if (parameter.kind !== 'column') {
  219. // The aggregation does not accept a column as a parameter,
  220. // so we clear the column.
  221. column = '';
  222. } else if (!column && parameter.required === false) {
  223. // The parameter was not given for a non-required parameter,
  224. // so we fall back to the default.
  225. column = parameter.defaultValue;
  226. }
  227. } else {
  228. // The aggregation does not exist or does not have any parameters,
  229. // so we clear the column.
  230. column = '';
  231. }
  232. return column ? {kind: 'field', field: column} : null;
  233. }
  234. /**
  235. * Convert an aggregated query into one that does not have aggregates.
  236. * Will also apply additions conditions defined in `additionalConditions`
  237. * and generate conditions based on the `dataRow` parameter and the current fields
  238. * in the `eventView`.
  239. */
  240. export function getExpandedResults(
  241. eventView: EventView,
  242. additionalConditions: Record<string, string>,
  243. dataRow?: TableDataRow | Event
  244. ): EventView {
  245. const fieldSet = new Set();
  246. // Expand any functions in the resulting column, and dedupe the result.
  247. // Mark any column as null to remove it.
  248. const expandedColumns: Array<Column | null> = eventView.fields.map((field: Field) => {
  249. const exploded = explodeFieldString(field.field, field.alias);
  250. const column = exploded.kind === 'function' ? drilldownAggregate(exploded) : exploded;
  251. if (
  252. // if expanding the function failed
  253. column === null ||
  254. // the new column is already present
  255. fieldSet.has(column.field) ||
  256. // Skip aggregate equations, their functions will already be added so we just want to remove it
  257. isAggregateEquation(field.field)
  258. ) {
  259. return null;
  260. }
  261. fieldSet.add(column.field);
  262. return column;
  263. });
  264. // id should be default column when expanded results in no columns; but only if
  265. // the Discover query's columns is non-empty.
  266. // This typically occurs in Discover drilldowns.
  267. if (fieldSet.size === 0 && expandedColumns.length) {
  268. expandedColumns[0] = {kind: 'field', field: 'id'};
  269. }
  270. // update the columns according the expansion above
  271. const nextView = expandedColumns.reduceRight(
  272. (newView, column, index) =>
  273. column === null
  274. ? newView.withDeletedColumn(index, undefined)
  275. : newView.withUpdatedColumn(index, column, undefined),
  276. eventView.clone()
  277. );
  278. nextView.query = generateExpandedConditions(nextView, additionalConditions, dataRow);
  279. return nextView;
  280. }
  281. /**
  282. * Create additional conditions based on the fields in an EventView
  283. * and a datarow/event
  284. */
  285. function generateAdditionalConditions(
  286. eventView: EventView,
  287. dataRow?: TableDataRow | Event
  288. ): Record<string, string | string[]> {
  289. const specialKeys = Object.values(URL_PARAM);
  290. const conditions: Record<string, string | string[]> = {};
  291. if (!dataRow) {
  292. return conditions;
  293. }
  294. eventView.fields.forEach((field: Field) => {
  295. const column = explodeFieldString(field.field, field.alias);
  296. // Skip aggregate fields
  297. if (column.kind === 'function') {
  298. return;
  299. }
  300. const dataKey = getAggregateAlias(field.field);
  301. // Append the current field as a condition if it exists in the dataRow
  302. // Or is a simple key in the event. More complex deeply nested fields are
  303. // more challenging to get at as their location in the structure does not
  304. // match their name.
  305. if (dataRow.hasOwnProperty(dataKey)) {
  306. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  307. let value = dataRow[dataKey];
  308. if (Array.isArray(value)) {
  309. if (value.length > 1) {
  310. conditions[column.field] = value;
  311. return;
  312. }
  313. // An array with only one value is equivalent to the value itself.
  314. value = value[0];
  315. }
  316. // if the value will be quoted, then do not trim it as the whitespaces
  317. // may be important to the query and should not be trimmed
  318. const shouldQuote =
  319. value === null || value === undefined
  320. ? false
  321. : /[\s\(\)\\"]/g.test(String(value).trim());
  322. const nextValue =
  323. value === null || value === undefined
  324. ? ''
  325. : shouldQuote
  326. ? String(value)
  327. : String(value).trim();
  328. if (isMeasurement(column.field) && !nextValue) {
  329. // Do not add measurement conditions if nextValue is falsey.
  330. // It's expected that nextValue is a numeric value.
  331. return;
  332. }
  333. switch (column.field) {
  334. case 'timestamp':
  335. // normalize the "timestamp" field to ensure the payload works
  336. conditions[column.field] = getUtcDateString(nextValue);
  337. break;
  338. default:
  339. conditions[column.field] = nextValue;
  340. }
  341. }
  342. // If we have an event, check tags as well.
  343. if (dataRow.tags && Array.isArray(dataRow.tags)) {
  344. const tagIndex = dataRow.tags.findIndex(item => item.key === dataKey);
  345. if (tagIndex > -1) {
  346. const key = specialKeys.includes(column.field)
  347. ? `tags[${column.field}]`
  348. : column.field;
  349. const tagValue = dataRow.tags[tagIndex]!.value;
  350. conditions[key] = tagValue;
  351. }
  352. }
  353. });
  354. return conditions;
  355. }
  356. /**
  357. * Discover queries can query either Errors, Transactions or a combination
  358. * of the two datasets. This is a util to determine if the query will excusively
  359. * hit the Transactions dataset.
  360. */
  361. export function usesTransactionsDataset(eventView: EventView, yAxisValue: string[]) {
  362. let usesTransactions: boolean = false;
  363. const parsedQuery = new MutableSearch(eventView.query);
  364. for (const yAxis of yAxisValue) {
  365. const aggregateArg = getAggregateArg(yAxis) ?? '';
  366. if (isMeasurement(aggregateArg) || aggregateArg === 'transaction.duration') {
  367. usesTransactions = true;
  368. break;
  369. }
  370. const eventTypeFilter = parsedQuery.getFilterValues('event.type');
  371. if (
  372. eventTypeFilter.length > 0 &&
  373. eventTypeFilter.every(filter => filter === 'transaction')
  374. ) {
  375. usesTransactions = true;
  376. break;
  377. }
  378. }
  379. return usesTransactions;
  380. }
  381. function generateExpandedConditions(
  382. eventView: EventView,
  383. additionalConditions: Record<string, string>,
  384. dataRow?: TableDataRow | Event
  385. ): string {
  386. const parsedQuery = new MutableSearch(eventView.query);
  387. // Remove any aggregates from the search conditions.
  388. // otherwise, it'll lead to an invalid query result.
  389. for (const key in parsedQuery.filters) {
  390. const column = explodeFieldString(key);
  391. if (column.kind === 'function') {
  392. parsedQuery.removeFilter(key);
  393. }
  394. }
  395. const conditions: Record<string, string | string[]> = Object.assign(
  396. {},
  397. additionalConditions,
  398. generateAdditionalConditions(eventView, dataRow)
  399. );
  400. // Add additional conditions provided and generated.
  401. for (const key in conditions) {
  402. const value = conditions[key]!;
  403. if (Array.isArray(value)) {
  404. parsedQuery.setFilterValues(key, value);
  405. continue;
  406. }
  407. if (key === 'project.id') {
  408. eventView.project = [...eventView.project, parseInt(value, 10)];
  409. continue;
  410. }
  411. if (key === 'environment') {
  412. if (!eventView.environment.includes(value)) {
  413. eventView.environment = [...eventView.environment, value];
  414. }
  415. continue;
  416. }
  417. const column = explodeFieldString(key);
  418. // Skip aggregates as they will be invalid.
  419. if (column.kind === 'function') {
  420. continue;
  421. }
  422. parsedQuery.setFilterValues(key, [value]);
  423. }
  424. return parsedQuery.formatString();
  425. }
  426. type FieldGeneratorOpts = {
  427. organization: OrganizationSummary;
  428. aggregations?: Record<string, Aggregation>;
  429. customMeasurements?: Array<{functions: string[]; key: string}> | null;
  430. fieldKeys?: string[];
  431. measurementKeys?: string[] | null;
  432. spanOperationBreakdownKeys?: string[];
  433. tagKeys?: string[] | null;
  434. };
  435. export function generateFieldOptions({
  436. organization,
  437. tagKeys,
  438. measurementKeys,
  439. spanOperationBreakdownKeys,
  440. customMeasurements,
  441. aggregations = AGGREGATIONS,
  442. fieldKeys = DISCOVER_FIELDS,
  443. }: FieldGeneratorOpts) {
  444. let functions = Object.keys(aggregations);
  445. // Strip tracing features if the org doesn't have access.
  446. if (!organization.features.includes('performance-view')) {
  447. fieldKeys = fieldKeys.filter(item => !TRACING_FIELDS.includes(item));
  448. functions = functions.filter(item => !TRACING_FIELDS.includes(item));
  449. }
  450. // Strip profiling features if the org doesn't have access.
  451. if (!organization.features.includes('profiling')) {
  452. fieldKeys = fieldKeys.filter(item => !PROFILING_FIELDS.includes(item));
  453. }
  454. const fieldOptions: Record<string, SelectValue<FieldValue>> = {};
  455. // Index items by prefixed keys as custom tags can overlap both fields and
  456. // function names. Having a mapping makes finding the value objects easier
  457. // later as well.
  458. functions.forEach(func => {
  459. const ellipsis = aggregations[func]!.parameters.length ? '\u2026' : '';
  460. const parameters = aggregations[func]!.parameters.map(param => {
  461. const overrides = aggregations[func]!.getFieldOverrides;
  462. if (typeof overrides === 'undefined') {
  463. return param;
  464. }
  465. return {
  466. ...param,
  467. ...overrides({parameter: param}),
  468. };
  469. });
  470. fieldOptions[`function:${func}`] = {
  471. label: `${func}(${ellipsis})`,
  472. value: {
  473. kind: FieldValueKind.FUNCTION,
  474. meta: {
  475. name: func,
  476. parameters,
  477. },
  478. },
  479. };
  480. });
  481. fieldKeys.forEach(field => {
  482. fieldOptions[`field:${field}`] = {
  483. label: field,
  484. value: {
  485. kind: FieldValueKind.FIELD,
  486. meta: {
  487. name: field,
  488. dataType: (getFieldDefinition(field)?.valueType ??
  489. FieldValueType.STRING) as ColumnType,
  490. },
  491. },
  492. };
  493. });
  494. if (measurementKeys !== undefined && measurementKeys !== null) {
  495. measurementKeys.sort();
  496. measurementKeys.forEach(measurement => {
  497. fieldOptions[`measurement:${measurement}`] = {
  498. label: measurement,
  499. value: {
  500. kind: FieldValueKind.MEASUREMENT,
  501. meta: {name: measurement, dataType: measurementType(measurement)},
  502. },
  503. };
  504. });
  505. }
  506. if (customMeasurements !== undefined && customMeasurements !== null) {
  507. customMeasurements.sort(({key: currentKey}, {key: nextKey}) =>
  508. currentKey > nextKey ? 1 : currentKey === nextKey ? 0 : -1
  509. );
  510. customMeasurements.forEach(({key, functions: supportedFunctions}) => {
  511. fieldOptions[`measurement:${key}`] = {
  512. label: key,
  513. value: {
  514. kind: FieldValueKind.CUSTOM_MEASUREMENT,
  515. meta: {
  516. name: key,
  517. dataType: measurementType(key),
  518. functions: supportedFunctions,
  519. },
  520. },
  521. };
  522. });
  523. }
  524. if (Array.isArray(spanOperationBreakdownKeys)) {
  525. spanOperationBreakdownKeys.sort();
  526. spanOperationBreakdownKeys.forEach(breakdownField => {
  527. if (!fieldKeys.includes(breakdownField)) {
  528. // These span op breakdowns are sometimes included in the fieldKeys
  529. // so check before we add them, or else we surface duplicates
  530. fieldOptions[`span_op_breakdown:${breakdownField}`] = {
  531. label: breakdownField,
  532. value: {
  533. kind: FieldValueKind.BREAKDOWN,
  534. meta: {name: breakdownField, dataType: 'duration'},
  535. },
  536. };
  537. }
  538. });
  539. }
  540. if (tagKeys !== undefined && tagKeys !== null) {
  541. tagKeys.sort();
  542. tagKeys.forEach(tag => {
  543. const tagValue =
  544. fieldKeys.includes(tag) || aggregations.hasOwnProperty(tag)
  545. ? `tags[${tag}]`
  546. : tag;
  547. fieldOptions[`tag:${tag}`] = {
  548. label: tag,
  549. value: {
  550. kind: FieldValueKind.TAG,
  551. meta: {name: tagValue, dataType: 'string'},
  552. },
  553. };
  554. });
  555. }
  556. return fieldOptions;
  557. }
  558. const RENDER_PREBUILT_KEY = 'discover-render-prebuilt';
  559. export function shouldRenderPrebuilt(): boolean {
  560. const shouldRender = localStorage.getItem(RENDER_PREBUILT_KEY);
  561. return shouldRender === 'true' || shouldRender === null;
  562. }
  563. export function setRenderPrebuilt(value: boolean) {
  564. localStorage.setItem(RENDER_PREBUILT_KEY, value ? 'true' : 'false');
  565. }
  566. export function eventViewToWidgetQuery({
  567. eventView,
  568. yAxis,
  569. displayType,
  570. }: {
  571. displayType: DisplayType;
  572. eventView: EventView;
  573. yAxis?: string | string[];
  574. }) {
  575. const fields = eventView.fields.map(({field}) => field);
  576. const {columns, aggregates} = getColumnsAndAggregates(fields);
  577. const sort = eventView.sorts[0];
  578. const queryYAxis = typeof yAxis === 'string' ? [yAxis] : yAxis ?? ['count()'];
  579. let orderby = '';
  580. // The orderby should only be set to sort.field if it is a Top N query
  581. // since the query uses all of the fields, or if the ordering is used in the y-axis
  582. if (sort) {
  583. let orderbyFunction = '';
  584. const aggregateFields = [...queryYAxis, ...aggregates];
  585. for (const field of aggregateFields) {
  586. if (sort.field === getAggregateAlias(field)) {
  587. orderbyFunction = field;
  588. break;
  589. }
  590. }
  591. const bareOrderby = orderbyFunction === '' ? sort.field : orderbyFunction;
  592. if (displayType === DisplayType.TOP_N || bareOrderby) {
  593. orderby = `${sort.kind === 'desc' ? '-' : ''}${bareOrderby}`;
  594. }
  595. }
  596. let newAggregates = aggregates;
  597. if (displayType !== DisplayType.TABLE) {
  598. newAggregates = queryYAxis;
  599. }
  600. const widgetQuery: WidgetQuery = {
  601. name: '',
  602. aggregates: newAggregates,
  603. columns: [...(displayType === DisplayType.TOP_N ? columns : [])],
  604. fields: [...(displayType === DisplayType.TOP_N ? fields : []), ...queryYAxis],
  605. conditions: eventView.query,
  606. orderby,
  607. };
  608. return widgetQuery;
  609. }
  610. export function handleAddQueryToDashboard({
  611. eventView,
  612. location,
  613. query,
  614. organization,
  615. router,
  616. yAxis,
  617. widgetType,
  618. source,
  619. }: {
  620. eventView: EventView;
  621. location: Location;
  622. organization: Organization;
  623. router: InjectedRouter;
  624. source: DashboardWidgetSource;
  625. widgetType: WidgetType | undefined;
  626. query?: NewQuery;
  627. yAxis?: string | string[];
  628. }) {
  629. const displayType = displayModeToDisplayType(eventView.display as DisplayModes);
  630. const defaultWidgetQuery = eventViewToWidgetQuery({
  631. eventView,
  632. displayType,
  633. yAxis,
  634. });
  635. const {query: widgetAsQueryParams} = constructAddQueryToDashboardLink({
  636. eventView,
  637. query,
  638. organization,
  639. yAxis,
  640. location,
  641. widgetType,
  642. source,
  643. });
  644. openAddToDashboardModal({
  645. organization,
  646. selection: {
  647. projects: eventView.project.slice(),
  648. environments: eventView.environment.slice(),
  649. datetime: {
  650. start: eventView.start!,
  651. end: eventView.end!,
  652. period: eventView.statsPeriod!,
  653. // Previously undetected because the type used to rely on an implicit any value.
  654. // @ts-expect-error TS(2322): Type 'string | boolean | undefined' is not assigna... Remove this comment to see the full error message
  655. utc: eventView.utc,
  656. },
  657. },
  658. widget: {
  659. // We need the event view name for when we're adding from a saved query page
  660. title: (query?.name ??
  661. (eventView.name === 'All Errors' ? t('Custom Widget') : eventView.name))!,
  662. displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType,
  663. queries: [
  664. {
  665. ...defaultWidgetQuery,
  666. aggregates: [...(typeof yAxis === 'string' ? [yAxis] : yAxis ?? ['count()'])],
  667. ...(organization.features.includes('dashboards-widget-builder-redesign')
  668. ? {
  669. // The widget query params filters out aggregate fields
  670. // so we can use the fields as columns. This is so yAxes
  671. // can be grouped by the fields.
  672. fields: widgetAsQueryParams?.field ?? [],
  673. columns: widgetAsQueryParams?.field ?? [],
  674. }
  675. : {}),
  676. },
  677. ],
  678. interval: eventView.interval!,
  679. limit: widgetAsQueryParams?.limit,
  680. widgetType,
  681. },
  682. router,
  683. // Previously undetected because the type relied on implicit any.
  684. // @ts-expect-error TS(2322): Type '{ dataset?: WidgetType | undefined; descript... Remove this comment to see the full error message
  685. widgetAsQueryParams,
  686. location,
  687. });
  688. return;
  689. }
  690. export function getTargetForTransactionSummaryLink(
  691. dataRow: EventData,
  692. organization: Organization,
  693. projects?: Project[],
  694. nextView?: EventView,
  695. location?: Location
  696. ) {
  697. let projectID: string | string[] | undefined;
  698. const filterProjects = location?.query.project;
  699. if (typeof filterProjects === 'string' && filterProjects !== '-1') {
  700. // Project selector in discover has just one selected project
  701. projectID = filterProjects;
  702. } else {
  703. const projectMatch = projects?.find(
  704. project =>
  705. project.slug && [dataRow['project.name'], dataRow.project].includes(project.slug)
  706. );
  707. projectID = projectMatch ? [projectMatch.id] : undefined;
  708. }
  709. const target = transactionSummaryRouteWithQuery({
  710. organization,
  711. transaction: String(dataRow.transaction),
  712. projectID,
  713. query: nextView?.getPageFiltersQuery() || {},
  714. });
  715. // Pass on discover filter params when there are multiple
  716. // projects associated with the transaction
  717. if (!projectID && filterProjects) {
  718. target.query.project = filterProjects;
  719. }
  720. return target;
  721. }
  722. export function constructAddQueryToDashboardLink({
  723. eventView,
  724. query,
  725. organization,
  726. yAxis,
  727. location,
  728. widgetType,
  729. source,
  730. }: {
  731. eventView: EventView;
  732. organization: Organization;
  733. source: DashboardWidgetSource;
  734. location?: Location;
  735. query?: NewQuery;
  736. widgetType?: WidgetType;
  737. yAxis?: string | string[];
  738. }) {
  739. const displayType = displayModeToDisplayType(eventView.display as DisplayModes);
  740. const defaultTableFields = eventView.fields.map(({field}) => field);
  741. const defaultWidgetQuery = eventViewToWidgetQuery({
  742. eventView,
  743. displayType,
  744. yAxis,
  745. });
  746. const defaultTitle =
  747. query?.name ?? (eventView.name !== 'All Events' ? eventView.name : undefined);
  748. const limit =
  749. displayType === DisplayType.TOP_N || eventView.display === DisplayModes.DAILYTOP5
  750. ? Number(eventView.topEvents) || TOP_N
  751. : undefined;
  752. if (organization.features.includes('dashboards-widget-builder-redesign')) {
  753. const widget: Widget = {
  754. title: defaultTitle!,
  755. displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType,
  756. widgetType,
  757. limit,
  758. interval: eventView.interval ?? '',
  759. queries: [
  760. {
  761. ...defaultWidgetQuery,
  762. aggregates: [...(typeof yAxis === 'string' ? [yAxis] : yAxis ?? ['count()'])],
  763. fields: eventView.getFields(),
  764. columns:
  765. widgetType === WidgetType.SPANS ||
  766. displayType === DisplayType.TOP_N ||
  767. eventView.display === DisplayModes.DAILYTOP5
  768. ? eventView
  769. .getFields()
  770. .filter(
  771. column => defined(column) && !isAggregateFieldOrEquation(column)
  772. )
  773. : [],
  774. },
  775. ],
  776. };
  777. return {
  778. pathname: `/organizations/${organization.slug}/dashboards/new/widget-builder/widget/new/`,
  779. query: {
  780. ...location?.query,
  781. start: eventView.start,
  782. end: eventView.end,
  783. statsPeriod: eventView.statsPeriod,
  784. ...convertWidgetToBuilderStateParams(widget),
  785. source,
  786. },
  787. };
  788. }
  789. return {
  790. pathname: `/organizations/${organization.slug}/dashboards/new/widget/new/`,
  791. query: {
  792. ...location?.query,
  793. start: eventView.start,
  794. end: eventView.end,
  795. statsPeriod: eventView.statsPeriod,
  796. defaultWidgetQuery: urlEncode(defaultWidgetQuery),
  797. defaultTableColumns: defaultTableFields,
  798. defaultTitle,
  799. displayType: displayType === DisplayType.TOP_N ? DisplayType.AREA : displayType,
  800. dataset: widgetType,
  801. field: eventView.getFields(),
  802. limit,
  803. source,
  804. },
  805. };
  806. }
  807. export const SAVED_QUERY_DATASET_TO_WIDGET_TYPE = {
  808. [SavedQueryDatasets.ERRORS]: WidgetType.ERRORS,
  809. [SavedQueryDatasets.TRANSACTIONS]: WidgetType.TRANSACTIONS,
  810. };