data.tsx 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889
  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. if (isLimitedSearch) {
  446. conditions.tokens = [];
  447. }
  448. savedQuery.query = conditions.formatString();
  449. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  450. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  451. if (query.trendParameter) {
  452. // projects and projectIds are not necessary here since trendParameter will always
  453. // be present in location and will not be determined based on the project type
  454. const trendParameter = getCurrentTrendParameter(location, [], []);
  455. if (WEB_VITAL_DETAILS[trendParameter.column]) {
  456. eventView.additionalConditions.addFilterValues('has', [trendParameter.column]);
  457. }
  458. }
  459. return eventView;
  460. }
  461. function generateBackendPerformanceEventView(
  462. location: Location,
  463. withStaticFilters: boolean
  464. ): EventView {
  465. const {query} = location;
  466. const fields = [
  467. 'team_key_transaction',
  468. 'transaction',
  469. 'project',
  470. 'transaction.op',
  471. 'http.method',
  472. 'tpm()',
  473. 'p50()',
  474. 'p95()',
  475. 'failure_rate()',
  476. 'apdex()',
  477. 'count_unique(user)',
  478. 'count_miserable(user)',
  479. 'user_misery()',
  480. ];
  481. const hasStartAndEnd = query.start && query.end;
  482. const savedQuery: NewQuery = {
  483. id: undefined,
  484. name: t('Performance'),
  485. query: 'event.type:transaction',
  486. projects: [],
  487. fields,
  488. version: 2,
  489. };
  490. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  491. widths[savedQuery.fields.length - 1] = '110';
  492. savedQuery.widths = widths;
  493. if (!query.statsPeriod && !hasStartAndEnd) {
  494. savedQuery.range = DEFAULT_STATS_PERIOD;
  495. }
  496. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  497. const searchQuery = decodeScalar(query.query, '');
  498. const conditions = new MutableSearch(searchQuery);
  499. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  500. // This is not an override condition since we want the duration to appear in the search bar as a default.
  501. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  502. conditions.setFilterValues('transaction.duration', ['<15m']);
  503. }
  504. // If there is a bare text search, we want to treat it as a search
  505. // on the transaction name.
  506. if (conditions.freeText.length > 0) {
  507. const parsedFreeText = isLimitedSearch
  508. ? decodeScalar(conditions.freeText, '')
  509. : conditions.freeText.join(' ');
  510. if (isLimitedSearch) {
  511. // the query here is a user entered condition, no need to escape it
  512. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  513. } else {
  514. // the query here is a user entered condition, no need to escape it
  515. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  516. }
  517. conditions.freeText = [];
  518. }
  519. savedQuery.query = conditions.formatString();
  520. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  521. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  522. return eventView;
  523. }
  524. function generateMobilePerformanceEventView(
  525. location: Location,
  526. projects: Project[],
  527. genericEventView: EventView,
  528. withStaticFilters: boolean
  529. ): EventView {
  530. const {query} = location;
  531. const fields = [
  532. 'team_key_transaction',
  533. 'transaction',
  534. 'project',
  535. 'transaction.op',
  536. 'tpm()',
  537. 'p75(measurements.frames_slow_rate)',
  538. 'p75(measurements.frames_frozen_rate)',
  539. ];
  540. // At this point, all projects are mobile projects.
  541. // If in addition to that, all projects are react-native projects,
  542. // then show the stall percentage as well.
  543. const projectIds = genericEventView.project;
  544. if (projectIds.length > 0 && projectIds[0] !== ALL_ACCESS_PROJECTS) {
  545. const selectedProjects = projects.filter(p =>
  546. projectIds.includes(parseInt(p.id, 10))
  547. );
  548. if (
  549. selectedProjects.length > 0 &&
  550. selectedProjects.every(project => project.platform === 'react-native')
  551. ) {
  552. fields.push('p75(measurements.stall_percentage)');
  553. }
  554. }
  555. const hasStartAndEnd = query.start && query.end;
  556. const savedQuery: NewQuery = {
  557. id: undefined,
  558. name: t('Performance'),
  559. query: 'event.type:transaction',
  560. projects: [],
  561. fields: [...fields, 'count_unique(user)', 'count_miserable(user)', 'user_misery()'],
  562. version: 2,
  563. };
  564. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  565. widths[savedQuery.fields.length - 1] = '110';
  566. savedQuery.widths = widths;
  567. if (!query.statsPeriod && !hasStartAndEnd) {
  568. savedQuery.range = DEFAULT_STATS_PERIOD;
  569. }
  570. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  571. const searchQuery = decodeScalar(query.query, '');
  572. const conditions = new MutableSearch(searchQuery);
  573. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  574. // This is not an override condition since we want the duration to appear in the search bar as a default.
  575. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  576. conditions.setFilterValues('transaction.duration', ['<15m']);
  577. }
  578. // If there is a bare text search, we want to treat it as a search
  579. // on the transaction name.
  580. if (conditions.freeText.length > 0) {
  581. const parsedFreeText = isLimitedSearch
  582. ? // pick first element to search transactions by name
  583. decodeScalar(conditions.freeText, '')
  584. : conditions.freeText.join(' ');
  585. if (isLimitedSearch) {
  586. // the query here is a user entered condition, no need to escape it
  587. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  588. } else {
  589. // the query here is a user entered condition, no need to escape it
  590. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  591. }
  592. conditions.freeText = [];
  593. }
  594. savedQuery.query = conditions.formatString();
  595. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  596. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  597. return eventView;
  598. }
  599. function generateFrontendPageloadPerformanceEventView(
  600. location: Location,
  601. withStaticFilters: boolean
  602. ): EventView {
  603. const {query} = location;
  604. const fields = [
  605. 'team_key_transaction',
  606. 'transaction',
  607. 'project',
  608. 'tpm()',
  609. 'p75(measurements.fcp)',
  610. 'p75(measurements.lcp)',
  611. 'p75(measurements.fid)',
  612. 'p75(measurements.cls)',
  613. 'count_unique(user)',
  614. 'count_miserable(user)',
  615. 'user_misery()',
  616. ];
  617. const hasStartAndEnd = query.start && query.end;
  618. const savedQuery: NewQuery = {
  619. id: undefined,
  620. name: t('Performance'),
  621. query: 'event.type:transaction',
  622. projects: [],
  623. fields,
  624. version: 2,
  625. };
  626. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  627. widths[savedQuery.fields.length - 1] = '110';
  628. savedQuery.widths = widths;
  629. if (!query.statsPeriod && !hasStartAndEnd) {
  630. savedQuery.range = DEFAULT_STATS_PERIOD;
  631. }
  632. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  633. const searchQuery = decodeScalar(query.query, '');
  634. const conditions = new MutableSearch(searchQuery);
  635. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  636. // This is not an override condition since we want the duration to appear in the search bar as a default.
  637. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  638. conditions.setFilterValues('transaction.duration', ['<15m']);
  639. }
  640. // If there is a bare text search, we want to treat it as a search
  641. // on the transaction name.
  642. if (conditions.freeText.length > 0) {
  643. const parsedFreeText = isLimitedSearch
  644. ? // pick first element to search transactions by name
  645. decodeScalar(conditions.freeText, '')
  646. : conditions.freeText.join(' ');
  647. if (isLimitedSearch) {
  648. // the query here is a user entered condition, no need to escape it
  649. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  650. } else {
  651. // the query here is a user entered condition, no need to escape it
  652. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  653. }
  654. conditions.freeText = [];
  655. }
  656. savedQuery.query = conditions.formatString();
  657. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  658. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  659. eventView.additionalConditions.addFilterValues('transaction.op', ['pageload']);
  660. return eventView;
  661. }
  662. function generateFrontendOtherPerformanceEventView(
  663. location: Location,
  664. withStaticFilters: boolean
  665. ): EventView {
  666. const {query} = location;
  667. const fields = [
  668. 'team_key_transaction',
  669. 'transaction',
  670. 'project',
  671. 'transaction.op',
  672. 'tpm()',
  673. 'p50(transaction.duration)',
  674. 'p75(transaction.duration)',
  675. 'p95(transaction.duration)',
  676. 'count_unique(user)',
  677. 'count_miserable(user)',
  678. 'user_misery()',
  679. ];
  680. const hasStartAndEnd = query.start && query.end;
  681. const savedQuery: NewQuery = {
  682. id: undefined,
  683. name: t('Performance'),
  684. query: 'event.type:transaction',
  685. projects: [],
  686. fields,
  687. version: 2,
  688. };
  689. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  690. widths[savedQuery.fields.length - 1] = '110';
  691. savedQuery.widths = widths;
  692. if (!query.statsPeriod && !hasStartAndEnd) {
  693. savedQuery.range = DEFAULT_STATS_PERIOD;
  694. }
  695. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  696. const searchQuery = decodeScalar(query.query, '');
  697. const conditions = new MutableSearch(searchQuery);
  698. const isLimitedSearch = isUsingLimitedSearch(location, withStaticFilters);
  699. // This is not an override condition since we want the duration to appear in the search bar as a default.
  700. if (shouldAddDefaultConditions(location) && !withStaticFilters) {
  701. conditions.setFilterValues('transaction.duration', ['<15m']);
  702. }
  703. // If there is a bare text search, we want to treat it as a search
  704. // on the transaction name.
  705. if (conditions.freeText.length > 0 && !isLimitedSearch) {
  706. const parsedFreeText = isLimitedSearch
  707. ? // pick first element to search transactions by name
  708. decodeScalar(conditions.freeText, '')
  709. : conditions.freeText.join(' ');
  710. if (isLimitedSearch) {
  711. // the query here is a user entered condition, no need to escape it
  712. conditions.setFilterValues('transaction', [`${parsedFreeText}`], false);
  713. } else {
  714. // the query here is a user entered condition, no need to escape it
  715. conditions.setFilterValues('transaction', [`*${parsedFreeText}*`], false);
  716. }
  717. conditions.freeText = [];
  718. }
  719. savedQuery.query = conditions.formatString();
  720. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  721. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  722. return eventView;
  723. }
  724. export function generatePerformanceEventView(
  725. location: Location,
  726. projects: Project[],
  727. {isTrends = false, withStaticFilters = false} = {}
  728. ) {
  729. const eventView = generateGenericPerformanceEventView(location, withStaticFilters);
  730. if (isTrends) {
  731. return eventView;
  732. }
  733. const display = getCurrentLandingDisplay(location, projects, eventView);
  734. switch (display?.field) {
  735. case LandingDisplayField.FRONTEND_PAGELOAD:
  736. return generateFrontendPageloadPerformanceEventView(location, withStaticFilters);
  737. case LandingDisplayField.FRONTEND_OTHER:
  738. return generateFrontendOtherPerformanceEventView(location, withStaticFilters);
  739. case LandingDisplayField.BACKEND:
  740. return generateBackendPerformanceEventView(location, withStaticFilters);
  741. case LandingDisplayField.MOBILE:
  742. return generateMobilePerformanceEventView(
  743. location,
  744. projects,
  745. eventView,
  746. withStaticFilters
  747. );
  748. default:
  749. return eventView;
  750. }
  751. }
  752. export function generatePerformanceVitalDetailView(location: Location): EventView {
  753. const {query} = location;
  754. const vitalName = vitalNameFromLocation(location);
  755. const hasStartAndEnd = query.start && query.end;
  756. const savedQuery: NewQuery = {
  757. id: undefined,
  758. name: t('Vitals Performance Details'),
  759. query: 'event.type:transaction',
  760. projects: [],
  761. fields: [
  762. 'team_key_transaction',
  763. 'transaction',
  764. 'project',
  765. 'count_unique(user)',
  766. 'count()',
  767. `p50(${vitalName})`,
  768. `p75(${vitalName})`,
  769. `p95(${vitalName})`,
  770. getVitalDetailTablePoorStatusFunction(vitalName),
  771. getVitalDetailTableMehStatusFunction(vitalName),
  772. ],
  773. version: 2,
  774. yAxis: [`p75(${vitalName})`],
  775. };
  776. if (!query.statsPeriod && !hasStartAndEnd) {
  777. savedQuery.range = DEFAULT_STATS_PERIOD;
  778. }
  779. savedQuery.orderby = decodeScalar(query.sort, '-count');
  780. const searchQuery = decodeScalar(query.query, '');
  781. const conditions = new MutableSearch(searchQuery);
  782. // If there is a bare text search, we want to treat it as a search
  783. // on the transaction name.
  784. if (conditions.freeText.length > 0) {
  785. // the query here is a user entered condition, no need to escape it
  786. conditions.setFilterValues(
  787. 'transaction',
  788. [`*${conditions.freeText.join(' ')}*`],
  789. false
  790. );
  791. conditions.freeText = [];
  792. }
  793. conditions.setFilterValues('event.type', ['transaction']);
  794. savedQuery.query = conditions.formatString();
  795. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  796. eventView.additionalConditions.addFilterValues('event.type', ['transaction']);
  797. eventView.additionalConditions.addFilterValues('has', [vitalName]);
  798. return eventView;
  799. }