data.tsx 27 KB

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