utils.tsx 25 KB

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