data.tsx 26 KB

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