data.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886
  1. import {Location} from 'history';
  2. import {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  3. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  4. import {t} from 'sentry/locale';
  5. import {NewQuery, Organization, Project, SelectValue} from 'sentry/types';
  6. import EventView from 'sentry/utils/discover/eventView';
  7. import {
  8. MEPState,
  9. METRIC_SEARCH_SETTING_PARAM,
  10. METRIC_SETTING_PARAM,
  11. } from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
  12. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  15. import {getCurrentTrendParameter} from 'sentry/views/performance/trends/utils';
  16. import {getCurrentLandingDisplay, LandingDisplayField} from './landing/utils';
  17. import {
  18. getVitalDetailTableMehStatusFunction,
  19. getVitalDetailTablePoorStatusFunction,
  20. vitalNameFromLocation,
  21. } from './vitalDetail/utils';
  22. export const DEFAULT_STATS_PERIOD = '24h';
  23. export const DEFAULT_PROJECT_THRESHOLD_METRIC = 'duration';
  24. export const DEFAULT_PROJECT_THRESHOLD = 300;
  25. export const COLUMN_TITLES = [
  26. 'transaction',
  27. 'project',
  28. 'tpm',
  29. 'p50',
  30. 'p95',
  31. 'failure rate',
  32. 'apdex',
  33. 'users',
  34. 'user misery',
  35. ];
  36. export enum PERFORMANCE_TERM {
  37. TPM = 'tpm',
  38. THROUGHPUT = 'throughput',
  39. FAILURE_RATE = 'failureRate',
  40. P50 = 'p50',
  41. P75 = 'p75',
  42. P95 = 'p95',
  43. P99 = 'p99',
  44. LCP = 'lcp',
  45. FCP = 'fcp',
  46. FID = 'fid',
  47. CLS = 'cls',
  48. STATUS_BREAKDOWN = 'statusBreakdown',
  49. DURATION_DISTRIBUTION = 'durationDistribution',
  50. USER_MISERY = 'userMisery',
  51. APDEX = 'apdex',
  52. APP_START_COLD = 'appStartCold',
  53. APP_START_WARM = 'appStartWarm',
  54. SLOW_FRAMES = 'slowFrames',
  55. FROZEN_FRAMES = 'frozenFrames',
  56. STALL_PERCENTAGE = 'stallPercentage',
  57. MOST_ISSUES = 'mostIssues',
  58. MOST_ERRORS = 'mostErrors',
  59. SLOW_HTTP_SPANS = 'slowHTTPSpans',
  60. }
  61. export type TooltipOption = SelectValue<string> & {
  62. tooltip: string;
  63. };
  64. export function getAxisOptions(organization: Organization): TooltipOption[] {
  65. return [
  66. {
  67. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APDEX),
  68. value: 'apdex()',
  69. label: t('Apdex'),
  70. },
  71. {
  72. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  73. value: 'tpm()',
  74. label: t('Transactions Per Minute'),
  75. },
  76. {
  77. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  78. value: 'failure_rate()',
  79. label: t('Failure Rate'),
  80. },
  81. {
  82. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P50),
  83. value: 'p50()',
  84. label: t('p50 Duration'),
  85. },
  86. {
  87. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P95),
  88. value: 'p95()',
  89. label: t('p95 Duration'),
  90. },
  91. {
  92. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P99),
  93. value: 'p99()',
  94. label: t('p99 Duration'),
  95. },
  96. ];
  97. }
  98. export type AxisOption = TooltipOption & {
  99. field: string;
  100. label: string;
  101. backupOption?: AxisOption;
  102. isDistribution?: boolean;
  103. isLeftDefault?: boolean;
  104. isRightDefault?: boolean;
  105. };
  106. export function getFrontendAxisOptions(organization: Organization): AxisOption[] {
  107. return [
  108. {
  109. tooltip: getTermHelp(organization, PERFORMANCE_TERM.LCP),
  110. value: `p75(lcp)`,
  111. label: t('LCP p75'),
  112. field: 'p75(measurements.lcp)',
  113. isLeftDefault: true,
  114. backupOption: {
  115. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FCP),
  116. value: `p75(fcp)`,
  117. label: t('FCP p75'),
  118. field: 'p75(measurements.fcp)',
  119. },
  120. },
  121. {
  122. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  123. value: 'lcp_distribution',
  124. label: t('LCP Distribution'),
  125. field: 'measurements.lcp',
  126. isDistribution: true,
  127. isRightDefault: true,
  128. backupOption: {
  129. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  130. value: 'fcp_distribution',
  131. label: t('FCP Distribution'),
  132. field: 'measurements.fcp',
  133. isDistribution: true,
  134. },
  135. },
  136. {
  137. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  138. value: 'tpm()',
  139. label: t('Transactions Per Minute'),
  140. field: 'tpm()',
  141. },
  142. ];
  143. }
  144. export function getFrontendOtherAxisOptions(organization: Organization): AxisOption[] {
  145. return [
  146. {
  147. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P50),
  148. value: `p50()`,
  149. label: t('Duration p50'),
  150. field: 'p50(transaction.duration)',
  151. },
  152. {
  153. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P75),
  154. value: `p75()`,
  155. label: t('Duration p75'),
  156. field: 'p75(transaction.duration)',
  157. isLeftDefault: true,
  158. },
  159. {
  160. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P95),
  161. value: `p95()`,
  162. label: t('Duration p95'),
  163. field: 'p95(transaction.duration)',
  164. },
  165. {
  166. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  167. value: 'duration_distribution',
  168. label: t('Duration Distribution'),
  169. field: 'transaction.duration',
  170. isDistribution: true,
  171. isRightDefault: true,
  172. },
  173. ];
  174. }
  175. export function getBackendAxisOptions(organization: Organization): AxisOption[] {
  176. return [
  177. {
  178. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P50),
  179. value: `p50()`,
  180. label: t('Duration p50'),
  181. field: 'p50(transaction.duration)',
  182. },
  183. {
  184. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P75),
  185. value: `p75()`,
  186. label: t('Duration p75'),
  187. field: 'p75(transaction.duration)',
  188. isLeftDefault: true,
  189. },
  190. {
  191. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P95),
  192. value: `p95()`,
  193. label: t('Duration p95'),
  194. field: 'p95(transaction.duration)',
  195. },
  196. {
  197. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P99),
  198. value: `p99()`,
  199. label: t('Duration p99'),
  200. field: 'p99(transaction.duration)',
  201. },
  202. {
  203. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  204. value: 'tpm()',
  205. label: t('Transactions Per Minute'),
  206. field: 'tpm()',
  207. },
  208. {
  209. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  210. value: 'failure_rate()',
  211. label: t('Failure Rate'),
  212. field: 'failure_rate()',
  213. },
  214. {
  215. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  216. value: 'duration_distribution',
  217. label: t('Duration Distribution'),
  218. field: 'transaction.duration',
  219. isDistribution: true,
  220. isRightDefault: true,
  221. },
  222. {
  223. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APDEX),
  224. value: 'apdex()',
  225. label: t('Apdex'),
  226. field: 'apdex()',
  227. },
  228. ];
  229. }
  230. export function getMobileAxisOptions(organization: Organization): AxisOption[] {
  231. return [
  232. {
  233. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
  234. value: `p50(measurements.app_start_cold)`,
  235. label: t('Cold Start Duration p50'),
  236. field: 'p50(measurements.app_start_cold)',
  237. },
  238. {
  239. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
  240. value: `p75(measurements.app_start_cold)`,
  241. label: t('Cold Start Duration p75'),
  242. field: 'p75(measurements.app_start_cold)',
  243. isLeftDefault: true,
  244. },
  245. {
  246. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
  247. value: `p95(measurements.app_start_cold)`,
  248. label: t('Cold Start Duration p95'),
  249. field: 'p95(measurements.app_start_cold)',
  250. },
  251. {
  252. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_COLD),
  253. value: `p99(measurements.app_start_cold)`,
  254. label: t('Cold Start Duration p99'),
  255. field: 'p99(measurements.app_start_cold)',
  256. },
  257. {
  258. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  259. value: 'app_start_cold_distribution',
  260. label: t('Cold Start Distribution'),
  261. field: 'measurements.app_start_cold',
  262. isDistribution: true,
  263. isRightDefault: true,
  264. },
  265. {
  266. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
  267. value: `p50(measurements.app_start_warm)`,
  268. label: t('Warm Start Duration p50'),
  269. field: 'p50(measurements.app_start_warm)',
  270. },
  271. {
  272. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
  273. value: `p75(measurements.app_start_warm)`,
  274. label: t('Warm Start Duration p75'),
  275. field: 'p75(measurements.app_start_warm)',
  276. },
  277. {
  278. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
  279. value: `p95(measurements.app_start_warm)`,
  280. label: t('Warm Start Duration p95'),
  281. field: 'p95(measurements.app_start_warm)',
  282. },
  283. {
  284. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APP_START_WARM),
  285. value: `p99(measurements.app_start_warm)`,
  286. label: t('Warm Start Duration p99'),
  287. field: 'p99(measurements.app_start_warm)',
  288. },
  289. {
  290. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  291. value: 'app_start_warm_distribution',
  292. label: t('Warm Start Distribution'),
  293. field: 'measurements.app_start_warm',
  294. isDistribution: true,
  295. },
  296. {
  297. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  298. value: 'tpm()',
  299. label: t('Transactions Per Minute'),
  300. field: 'tpm()',
  301. },
  302. {
  303. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  304. value: 'failure_rate()',
  305. label: t('Failure Rate'),
  306. field: 'failure_rate()',
  307. },
  308. ];
  309. }
  310. type TermFormatter = (organization: Organization) => string;
  311. export const PERFORMANCE_TERMS: Record<PERFORMANCE_TERM, TermFormatter> = {
  312. tpm: () => t('TPM is the number of recorded transaction events per minute.'),
  313. throughput: () =>
  314. t('Throughput is the number of recorded transaction events per minute.'),
  315. failureRate: () =>
  316. t(
  317. 'Failure rate is the percentage of recorded transactions that had a known and unsuccessful status.'
  318. ),
  319. p50: () => t('p50 indicates the duration that 50% of transactions are faster than.'),
  320. p75: () => t('p75 indicates the duration that 75% of transactions are faster than.'),
  321. p95: () => t('p95 indicates the duration that 95% of transactions are faster than.'),
  322. p99: () => t('p99 indicates the duration that 99% of transactions are faster than.'),
  323. lcp: () =>
  324. t('Largest contentful paint (LCP) is a web vital meant to represent user load times'),
  325. fcp: () =>
  326. t('First contentful paint (FCP) is a web vital meant to represent user load times'),
  327. fid: () =>
  328. t(
  329. 'First input delay (FID) is a web vital representing load for the first user interaction on a page.'
  330. ),
  331. cls: () =>
  332. t(
  333. 'Cumulative layout shift (CLS) is a web vital measuring unexpected visual shifting a user experiences.'
  334. ),
  335. statusBreakdown: () =>
  336. t(
  337. 'The breakdown of transaction statuses. This may indicate what type of failure it is.'
  338. ),
  339. durationDistribution: () =>
  340. t(
  341. 'Distribution buckets counts of transactions at specifics times for your current date range'
  342. ),
  343. userMisery: () =>
  344. t(
  345. "User Misery is a score that represents the number of unique users who have experienced load times 4x the project's configured threshold. Adjust project threshold in project performance settings."
  346. ),
  347. apdex: () =>
  348. t(
  349. 'Apdex is the ratio of both satisfactory and tolerable response times to all response times. To adjust the tolerable threshold, go to project performance settings.'
  350. ),
  351. appStartCold: () =>
  352. t('Cold start is a measure of the application start up time from scratch.'),
  353. appStartWarm: () =>
  354. t('Warm start is a measure of the application start up time while still in memory.'),
  355. slowFrames: () => t('The count of the number of slow frames in the transaction.'),
  356. frozenFrames: () => t('The count of the number of frozen frames in the transaction.'),
  357. mostErrors: () => t('Transactions with the most associated errors.'),
  358. mostIssues: () => t('The most instances of an issue for a related transaction.'),
  359. slowHTTPSpans: () => t('The transactions with the slowest spans of a certain type.'),
  360. stallPercentage: () =>
  361. t(
  362. 'The percentage of the transaction duration in which the application is in a stalled state.'
  363. ),
  364. };
  365. export function getTermHelp(
  366. organization: Organization,
  367. term: keyof typeof PERFORMANCE_TERMS
  368. ): string {
  369. if (!PERFORMANCE_TERMS.hasOwnProperty(term)) {
  370. return '';
  371. }
  372. return PERFORMANCE_TERMS[term](organization);
  373. }
  374. function shouldAddDefaultConditions(location: Location) {
  375. const {query} = location;
  376. const searchQuery = decodeScalar(query.query, '');
  377. const isDefaultQuery = decodeScalar(query.isDefaultQuery);
  378. return !searchQuery && isDefaultQuery !== 'false';
  379. }
  380. function isUsingLimitedSearch(location: Location, withStaticFilters: boolean) {
  381. const {query} = location;
  382. const mepSearchState = decodeScalar(query[METRIC_SEARCH_SETTING_PARAM], '');
  383. const mepSettingState = decodeScalar(query[METRIC_SETTING_PARAM], ''); // TODO: Can be removed since it's for dev ui only.
  384. return (
  385. withStaticFilters &&
  386. (mepSearchState === MEPState.metricsOnly || mepSettingState === MEPState.metricsOnly)
  387. );
  388. }
  389. function generateGenericPerformanceEventView(
  390. location: Location,
  391. withStaticFilters: boolean
  392. ): EventView {
  393. const {query} = location;
  394. const fields = [
  395. 'team_key_transaction',
  396. 'transaction',
  397. 'project',
  398. 'tpm()',
  399. 'p50()',
  400. 'p95()',
  401. 'failure_rate()',
  402. 'apdex()',
  403. 'count_unique(user)',
  404. 'count_miserable(user)',
  405. 'user_misery()',
  406. ];
  407. const hasStartAndEnd = query.start && query.end;
  408. const savedQuery: NewQuery = {
  409. id: undefined,
  410. name: t('Performance'),
  411. query: 'event.type:transaction',
  412. projects: [],
  413. fields,
  414. version: 2,
  415. };
  416. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  417. widths[savedQuery.fields.length - 1] = '110';
  418. savedQuery.widths = widths;
  419. if (!query.statsPeriod && !hasStartAndEnd) {
  420. savedQuery.range = DEFAULT_STATS_PERIOD;
  421. }
  422. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  423. const searchQuery = decodeScalar(query.query, '');
  424. const conditions = new MutableSearch(searchQuery);
  425. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  426. // This is not an override condition since we want the duration to appear in the search bar as a default.
  427. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  428. conditions.setFilterValues('transaction.duration', ['<15m']);
  429. }
  430. // If there is a bare text search, we want to treat it as a search
  431. // on the transaction name.
  432. if (conditions.freeText.length > 0) {
  433. const parsedFreeText = isLimitedSearch
  434. ? decodeScalar(conditions.freeText, '')
  435. : conditions.freeText.join(' ');
  436. if (isLimitedSearch) {
  437. // the query here is a user entered condition, no need to escape it
  438. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  439. } else {
  440. // the query here is a user entered condition, no need to escape it
  441. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  442. }
  443. conditions.freeText = [];
  444. }
  445. savedQuery.query = conditions.formatString();
  446. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  447. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  448. if (query.trendParameter) {
  449. // projects and projectIds are not necessary here since trendParameter will always
  450. // be present in location and will not be determined based on the project type
  451. const trendParameter = getCurrentTrendParameter(location, [], []);
  452. if (Boolean(WEB_VITAL_DETAILS[trendParameter.column])) {
  453. eventView.additionalConditions.addFilterValues('has', [trendParameter.column]);
  454. }
  455. }
  456. return eventView;
  457. }
  458. function generateBackendPerformanceEventView(
  459. location: Location,
  460. withStaticFilters: boolean
  461. ): EventView {
  462. const {query} = location;
  463. const fields = [
  464. 'team_key_transaction',
  465. 'transaction',
  466. 'project',
  467. 'transaction.op',
  468. 'http.method',
  469. 'tpm()',
  470. 'p50()',
  471. 'p95()',
  472. 'failure_rate()',
  473. 'apdex()',
  474. 'count_unique(user)',
  475. 'count_miserable(user)',
  476. 'user_misery()',
  477. ];
  478. const hasStartAndEnd = query.start && query.end;
  479. const savedQuery: NewQuery = {
  480. id: undefined,
  481. name: t('Performance'),
  482. query: 'event.type:transaction',
  483. projects: [],
  484. fields,
  485. version: 2,
  486. };
  487. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  488. widths[savedQuery.fields.length - 1] = '110';
  489. savedQuery.widths = widths;
  490. if (!query.statsPeriod && !hasStartAndEnd) {
  491. savedQuery.range = DEFAULT_STATS_PERIOD;
  492. }
  493. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  494. const searchQuery = decodeScalar(query.query, '');
  495. const conditions = new MutableSearch(searchQuery);
  496. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  497. // This is not an override condition since we want the duration to appear in the search bar as a default.
  498. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  499. conditions.setFilterValues('transaction.duration', ['<15m']);
  500. }
  501. // If there is a bare text search, we want to treat it as a search
  502. // on the transaction name.
  503. if (conditions.freeText.length > 0) {
  504. const parsedFreeText = isLimitedSearch
  505. ? decodeScalar(conditions.freeText, '')
  506. : conditions.freeText.join(' ');
  507. if (isLimitedSearch) {
  508. // the query here is a user entered condition, no need to escape it
  509. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  510. } else {
  511. // the query here is a user entered condition, no need to escape it
  512. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  513. }
  514. conditions.freeText = [];
  515. }
  516. savedQuery.query = conditions.formatString();
  517. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  518. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  519. return eventView;
  520. }
  521. function generateMobilePerformanceEventView(
  522. location: Location,
  523. projects: Project[],
  524. genericEventView: EventView,
  525. withStaticFilters: boolean
  526. ): EventView {
  527. const {query} = location;
  528. const fields = [
  529. 'team_key_transaction',
  530. 'transaction',
  531. 'project',
  532. 'transaction.op',
  533. 'tpm()',
  534. 'p75(measurements.frames_slow_rate)',
  535. 'p75(measurements.frames_frozen_rate)',
  536. ];
  537. // At this point, all projects are mobile projects.
  538. // If in addition to that, all projects are react-native projects,
  539. // then show the stall percentage as well.
  540. const projectIds = genericEventView.project;
  541. if (projectIds.length > 0 && projectIds[0] !== ALL_ACCESS_PROJECTS) {
  542. const selectedProjects = projects.filter(p =>
  543. projectIds.includes(parseInt(p.id, 10))
  544. );
  545. if (
  546. selectedProjects.length > 0 &&
  547. selectedProjects.every(project => project.platform === 'react-native')
  548. ) {
  549. fields.push('p75(measurements.stall_percentage)');
  550. }
  551. }
  552. const hasStartAndEnd = query.start && query.end;
  553. const savedQuery: NewQuery = {
  554. id: undefined,
  555. name: t('Performance'),
  556. query: 'event.type:transaction',
  557. projects: [],
  558. fields: [...fields, 'count_unique(user)', 'count_miserable(user)', 'user_misery()'],
  559. version: 2,
  560. };
  561. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  562. widths[savedQuery.fields.length - 1] = '110';
  563. savedQuery.widths = widths;
  564. if (!query.statsPeriod && !hasStartAndEnd) {
  565. savedQuery.range = DEFAULT_STATS_PERIOD;
  566. }
  567. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  568. const searchQuery = decodeScalar(query.query, '');
  569. const conditions = new MutableSearch(searchQuery);
  570. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  571. // This is not an override condition since we want the duration to appear in the search bar as a default.
  572. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  573. conditions.setFilterValues('transaction.duration', ['<15m']);
  574. }
  575. // If there is a bare text search, we want to treat it as a search
  576. // on the transaction name.
  577. if (conditions.freeText.length > 0) {
  578. const parsedFreeText = isLimitedSearch
  579. ? // pick first element to search transactions by name
  580. decodeScalar(conditions.freeText, '')
  581. : conditions.freeText.join(' ');
  582. if (isLimitedSearch) {
  583. // the query here is a user entered condition, no need to escape it
  584. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  585. } else {
  586. // the query here is a user entered condition, no need to escape it
  587. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  588. }
  589. conditions.freeText = [];
  590. }
  591. savedQuery.query = conditions.formatString();
  592. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  593. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  594. return eventView;
  595. }
  596. function generateFrontendPageloadPerformanceEventView(
  597. location: Location,
  598. withStaticFilters: boolean
  599. ): EventView {
  600. const {query} = location;
  601. const fields = [
  602. 'team_key_transaction',
  603. 'transaction',
  604. 'project',
  605. 'tpm()',
  606. 'p75(measurements.fcp)',
  607. 'p75(measurements.lcp)',
  608. 'p75(measurements.fid)',
  609. 'p75(measurements.cls)',
  610. 'count_unique(user)',
  611. 'count_miserable(user)',
  612. 'user_misery()',
  613. ];
  614. const hasStartAndEnd = query.start && query.end;
  615. const savedQuery: NewQuery = {
  616. id: undefined,
  617. name: t('Performance'),
  618. query: 'event.type:transaction',
  619. projects: [],
  620. fields,
  621. version: 2,
  622. };
  623. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  624. widths[savedQuery.fields.length - 1] = '110';
  625. savedQuery.widths = widths;
  626. if (!query.statsPeriod && !hasStartAndEnd) {
  627. savedQuery.range = DEFAULT_STATS_PERIOD;
  628. }
  629. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  630. const searchQuery = decodeScalar(query.query, '');
  631. const conditions = new MutableSearch(searchQuery);
  632. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  633. // This is not an override condition since we want the duration to appear in the search bar as a default.
  634. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  635. conditions.setFilterValues('transaction.duration', ['<15m']);
  636. }
  637. // If there is a bare text search, we want to treat it as a search
  638. // on the transaction name.
  639. if (conditions.freeText.length > 0) {
  640. const parsedFreeText = isLimitedSearch
  641. ? // pick first element to search transactions by name
  642. decodeScalar(conditions.freeText, '')
  643. : conditions.freeText.join(' ');
  644. if (isLimitedSearch) {
  645. // the query here is a user entered condition, no need to escape it
  646. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  647. } else {
  648. // the query here is a user entered condition, no need to escape it
  649. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  650. }
  651. conditions.freeText = [];
  652. }
  653. savedQuery.query = conditions.formatString();
  654. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  655. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  656. eventView.additionalConditions.addFilterValues('transaction.op', ['pageload']);
  657. return eventView;
  658. }
  659. function generateFrontendOtherPerformanceEventView(
  660. location: Location,
  661. withStaticFilters: boolean
  662. ): EventView {
  663. const {query} = location;
  664. const fields = [
  665. 'team_key_transaction',
  666. 'transaction',
  667. 'project',
  668. 'transaction.op',
  669. 'tpm()',
  670. 'p50(transaction.duration)',
  671. 'p75(transaction.duration)',
  672. 'p95(transaction.duration)',
  673. 'count_unique(user)',
  674. 'count_miserable(user)',
  675. 'user_misery()',
  676. ];
  677. const hasStartAndEnd = query.start && query.end;
  678. const savedQuery: NewQuery = {
  679. id: undefined,
  680. name: t('Performance'),
  681. query: 'event.type:transaction',
  682. projects: [],
  683. fields,
  684. version: 2,
  685. };
  686. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  687. widths[savedQuery.fields.length - 1] = '110';
  688. savedQuery.widths = widths;
  689. if (!query.statsPeriod && !hasStartAndEnd) {
  690. savedQuery.range = DEFAULT_STATS_PERIOD;
  691. }
  692. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  693. const searchQuery = decodeScalar(query.query, '');
  694. const conditions = new MutableSearch(searchQuery);
  695. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  696. // This is not an override condition since we want the duration to appear in the search bar as a default.
  697. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  698. conditions.setFilterValues('transaction.duration', ['<15m']);
  699. }
  700. // If there is a bare text search, we want to treat it as a search
  701. // on the transaction name.
  702. if (conditions.freeText.length > 0 && !isLimitedSearch) {
  703. const parsedFreeText = isLimitedSearch
  704. ? // pick first element to search transactions by name
  705. decodeScalar(conditions.freeText, '')
  706. : conditions.freeText.join(' ');
  707. if (isLimitedSearch) {
  708. // the query here is a user entered condition, no need to escape it
  709. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  710. } else {
  711. // the query here is a user entered condition, no need to escape it
  712. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  713. }
  714. conditions.freeText = [];
  715. }
  716. savedQuery.query = conditions.formatString();
  717. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  718. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  719. return eventView;
  720. }
  721. export function generatePerformanceEventView(
  722. location: Location,
  723. projects: Project[],
  724. {isTrends = false, withStaticFilters = false} = {}
  725. ) {
  726. const eventView = generateGenericPerformanceEventView(location, withStaticFilters);
  727. if (isTrends) {
  728. return eventView;
  729. }
  730. const display = getCurrentLandingDisplay(location, projects, eventView);
  731. switch (display?.field) {
  732. case LandingDisplayField.FRONTEND_PAGELOAD:
  733. return generateFrontendPageloadPerformanceEventView(location, withStaticFilters);
  734. case LandingDisplayField.FRONTEND_OTHER:
  735. return generateFrontendOtherPerformanceEventView(location, withStaticFilters);
  736. case LandingDisplayField.BACKEND:
  737. return generateBackendPerformanceEventView(location, withStaticFilters);
  738. case LandingDisplayField.MOBILE:
  739. return generateMobilePerformanceEventView(
  740. location,
  741. projects,
  742. eventView,
  743. withStaticFilters
  744. );
  745. default:
  746. return eventView;
  747. }
  748. }
  749. export function generatePerformanceVitalDetailView(location: Location): EventView {
  750. const {query} = location;
  751. const vitalName = vitalNameFromLocation(location);
  752. const hasStartAndEnd = query.start && query.end;
  753. const savedQuery: NewQuery = {
  754. id: undefined,
  755. name: t('Vitals Performance Details'),
  756. query: 'event.type:transaction',
  757. projects: [],
  758. fields: [
  759. 'team_key_transaction',
  760. 'transaction',
  761. 'project',
  762. 'count_unique(user)',
  763. 'count()',
  764. `p50(${vitalName})`,
  765. `p75(${vitalName})`,
  766. `p95(${vitalName})`,
  767. getVitalDetailTablePoorStatusFunction(vitalName),
  768. getVitalDetailTableMehStatusFunction(vitalName),
  769. ],
  770. version: 2,
  771. yAxis: [`p75(${vitalName})`],
  772. };
  773. if (!query.statsPeriod && !hasStartAndEnd) {
  774. savedQuery.range = DEFAULT_STATS_PERIOD;
  775. }
  776. savedQuery.orderby = decodeScalar(query.sort, '-count');
  777. const searchQuery = decodeScalar(query.query, '');
  778. const conditions = new MutableSearch(searchQuery);
  779. // If there is a bare text search, we want to treat it as a search
  780. // on the transaction name.
  781. if (conditions.freeText.length > 0) {
  782. // the query here is a user entered condition, no need to escape it
  783. conditions.setFilterValues(
  784. 'transaction',
  785. [`*${conditions.freeText.join(' ')}*`],
  786. false
  787. );
  788. conditions.freeText = [];
  789. }
  790. conditions.setFilterValues('event.type', ['transaction']);
  791. savedQuery.query = conditions.formatString();
  792. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  793. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  794. eventView.additionalConditions.addFilterValues('has', [vitalName]);
  795. return eventView;
  796. }