data.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587
  1. import {Location} from 'history';
  2. import {COL_WIDTH_UNDEFINED} from 'app/components/gridEditable';
  3. import {t} from 'app/locale';
  4. import {LightWeightOrganization, NewQuery, SelectValue} from 'app/types';
  5. import EventView from 'app/utils/discover/eventView';
  6. import {decodeScalar} from 'app/utils/queryString';
  7. import {stringifyQueryObject, tokenizeSearch} from 'app/utils/tokenizeSearch';
  8. import {getCurrentLandingDisplay, LandingDisplayField} from './landing/utils';
  9. import {
  10. getVitalDetailTableMehStatusFunction,
  11. getVitalDetailTablePoorStatusFunction,
  12. vitalNameFromLocation,
  13. } from './vitalDetail/utils';
  14. export const DEFAULT_STATS_PERIOD = '24h';
  15. export const COLUMN_TITLES = [
  16. 'transaction',
  17. 'project',
  18. 'tpm',
  19. 'p50',
  20. 'p95',
  21. 'failure rate',
  22. 'apdex',
  23. 'users',
  24. 'user misery',
  25. ];
  26. export enum PERFORMANCE_TERM {
  27. APDEX = 'apdex',
  28. TPM = 'tpm',
  29. THROUGHPUT = 'throughput',
  30. FAILURE_RATE = 'failureRate',
  31. P50 = 'p50',
  32. P75 = 'p75',
  33. P95 = 'p95',
  34. P99 = 'p99',
  35. LCP = 'lcp',
  36. FCP = 'fcp',
  37. USER_MISERY = 'userMisery',
  38. STATUS_BREAKDOWN = 'statusBreakdown',
  39. DURATION_DISTRIBUTION = 'durationDistribution',
  40. }
  41. export type TooltipOption = SelectValue<string> & {
  42. tooltip: string;
  43. };
  44. export function getAxisOptions(organization: LightWeightOrganization): TooltipOption[] {
  45. return [
  46. {
  47. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APDEX),
  48. value: `apdex(${organization.apdexThreshold})`,
  49. label: t('Apdex'),
  50. },
  51. {
  52. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  53. value: 'tpm()',
  54. label: t('Transactions Per Minute'),
  55. },
  56. {
  57. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  58. value: 'failure_rate()',
  59. label: t('Failure Rate'),
  60. },
  61. {
  62. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P50),
  63. value: 'p50()',
  64. label: t('p50 Duration'),
  65. },
  66. {
  67. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P95),
  68. value: 'p95()',
  69. label: t('p95 Duration'),
  70. },
  71. {
  72. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P99),
  73. value: 'p99()',
  74. label: t('p99 Duration'),
  75. },
  76. ];
  77. }
  78. export type AxisOption = TooltipOption & {
  79. field: string;
  80. backupOption?: AxisOption;
  81. label: string;
  82. isDistribution?: boolean;
  83. isLeftDefault?: boolean;
  84. isRightDefault?: boolean;
  85. };
  86. export function getFrontendAxisOptions(
  87. organization: LightWeightOrganization
  88. ): AxisOption[] {
  89. return [
  90. {
  91. tooltip: getTermHelp(organization, PERFORMANCE_TERM.LCP),
  92. value: `p75(lcp)`,
  93. label: t('LCP p75'),
  94. field: 'p75(measurements.lcp)',
  95. isLeftDefault: true,
  96. backupOption: {
  97. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FCP),
  98. value: `p75(fcp)`,
  99. label: t('FCP p75'),
  100. field: 'p75(measurements.fcp)',
  101. },
  102. },
  103. {
  104. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  105. value: 'lcp_distribution',
  106. label: t('LCP Distribution'),
  107. field: 'measurements.lcp',
  108. isDistribution: true,
  109. isRightDefault: true,
  110. backupOption: {
  111. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  112. value: 'fcp_distribution',
  113. label: t('FCP Distribution'),
  114. field: 'measurements.fcp',
  115. isDistribution: true,
  116. },
  117. },
  118. {
  119. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  120. value: 'tpm()',
  121. label: t('Transactions Per Minute'),
  122. field: 'tpm()',
  123. },
  124. ];
  125. }
  126. export function getFrontendOtherAxisOptions(
  127. organization: LightWeightOrganization
  128. ): AxisOption[] {
  129. return [
  130. {
  131. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P50),
  132. value: `p50()`,
  133. label: t('Duration p50'),
  134. field: 'p50(transaction.duration)',
  135. },
  136. {
  137. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P75),
  138. value: `p75()`,
  139. label: t('Duration p75'),
  140. field: 'p75(transaction.duration)',
  141. isLeftDefault: true,
  142. },
  143. {
  144. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P95),
  145. value: `p95()`,
  146. label: t('Duration p95'),
  147. field: 'p95(transaction.duration)',
  148. },
  149. {
  150. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  151. value: 'duration_distribution',
  152. label: t('Duration Distribution'),
  153. field: 'transaction.duration',
  154. isDistribution: true,
  155. isRightDefault: true,
  156. },
  157. ];
  158. }
  159. export function getBackendAxisOptions(
  160. organization: LightWeightOrganization
  161. ): AxisOption[] {
  162. return [
  163. {
  164. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P50),
  165. value: `p50()`,
  166. label: t('Duration p50'),
  167. field: 'p50(transaction.duration)',
  168. },
  169. {
  170. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P75),
  171. value: `p75()`,
  172. label: t('Duration p75'),
  173. field: 'p75(transaction.duration)',
  174. isLeftDefault: true,
  175. },
  176. {
  177. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P95),
  178. value: `p95()`,
  179. label: t('Duration p95'),
  180. field: 'p95(transaction.duration)',
  181. },
  182. {
  183. tooltip: getTermHelp(organization, PERFORMANCE_TERM.P99),
  184. value: `p99()`,
  185. label: t('Duration p99'),
  186. field: 'p99(transaction.duration)',
  187. },
  188. {
  189. tooltip: getTermHelp(organization, PERFORMANCE_TERM.APDEX),
  190. value: `apdex(${organization.apdexThreshold})`,
  191. label: t('Apdex'),
  192. field: `apdex(${organization.apdexThreshold})`,
  193. },
  194. {
  195. tooltip: getTermHelp(organization, PERFORMANCE_TERM.TPM),
  196. value: 'tpm()',
  197. label: t('Transactions Per Minute'),
  198. field: 'tpm()',
  199. },
  200. {
  201. tooltip: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE),
  202. value: 'failure_rate()',
  203. label: t('Failure Rate'),
  204. field: 'failure_rate()',
  205. },
  206. {
  207. tooltip: getTermHelp(organization, PERFORMANCE_TERM.DURATION_DISTRIBUTION),
  208. value: 'duration_distribution',
  209. label: t('Duration Distribution'),
  210. field: 'transaction.duration',
  211. isDistribution: true,
  212. isRightDefault: true,
  213. },
  214. ];
  215. }
  216. type TermFormatter = (organization: LightWeightOrganization) => string;
  217. const PERFORMANCE_TERMS: Record<PERFORMANCE_TERM, TermFormatter> = {
  218. apdex: () =>
  219. t(
  220. 'Apdex is the ratio of both satisfactory and tolerable response times to all response times. To adjust the tolerable threshold, go to performance settings.'
  221. ),
  222. tpm: () => t('TPM is the number of recorded transaction events per minute.'),
  223. throughput: () =>
  224. t('Throughput is the number of recorded transaction events per minute.'),
  225. failureRate: () =>
  226. t(
  227. 'Failure rate is the percentage of recorded transactions that had a known and unsuccessful status.'
  228. ),
  229. p50: () => t('p50 indicates the duration that 50% of transactions are faster than.'),
  230. p75: () => t('p75 indicates the duration that 75% of transactions are faster than.'),
  231. p95: () => t('p95 indicates the duration that 95% of transactions are faster than.'),
  232. p99: () => t('p99 indicates the duration that 99% of transactions are faster than.'),
  233. lcp: () =>
  234. t('Largest contentful paint (LCP) is a web vital meant to represent user load times'),
  235. fcp: () =>
  236. t('First contentful paint (FCP) is a web vital meant to represent user load times'),
  237. userMisery: organization =>
  238. t(
  239. "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.",
  240. organization.apdexThreshold
  241. ),
  242. statusBreakdown: () =>
  243. t(
  244. 'The breakdown of transaction statuses. This may indicate what type of failure it is.'
  245. ),
  246. durationDistribution: () =>
  247. t(
  248. 'Distribution buckets counts of transactions at specifics times for your current date range'
  249. ),
  250. };
  251. export function getTermHelp(
  252. organization: LightWeightOrganization,
  253. term: keyof typeof PERFORMANCE_TERMS
  254. ): string {
  255. if (!PERFORMANCE_TERMS.hasOwnProperty(term)) {
  256. return '';
  257. }
  258. return PERFORMANCE_TERMS[term](organization);
  259. }
  260. function generateGenericPerformanceEventView(
  261. organization: LightWeightOrganization,
  262. location: Location
  263. ): EventView {
  264. const {query} = location;
  265. const hasStartAndEnd = query.start && query.end;
  266. const savedQuery: NewQuery = {
  267. id: undefined,
  268. name: t('Performance'),
  269. query: 'event.type:transaction',
  270. projects: [],
  271. fields: [
  272. 'key_transaction',
  273. 'transaction',
  274. 'project',
  275. 'tpm()',
  276. 'p50()',
  277. 'p95()',
  278. 'failure_rate()',
  279. `apdex(${organization.apdexThreshold})`,
  280. 'count_unique(user)',
  281. `count_miserable(user,${organization.apdexThreshold})`,
  282. `user_misery(${organization.apdexThreshold})`,
  283. ],
  284. version: 2,
  285. };
  286. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  287. widths[savedQuery.fields.length - 1] = '110';
  288. savedQuery.widths = widths;
  289. if (!query.statsPeriod && !hasStartAndEnd) {
  290. savedQuery.range = DEFAULT_STATS_PERIOD;
  291. }
  292. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  293. const searchQuery = decodeScalar(query.query, '');
  294. const conditions = tokenizeSearch(searchQuery);
  295. // This is not an override condition since we want the duration to appear in the search bar as a default.
  296. if (!conditions.hasTag('transaction.duration')) {
  297. conditions.setTagValues('transaction.duration', ['<15m']);
  298. }
  299. // If there is a bare text search, we want to treat it as a search
  300. // on the transaction name.
  301. if (conditions.query.length > 0) {
  302. conditions.setTagValues('transaction', [`*${conditions.query.join(' ')}*`]);
  303. conditions.query = [];
  304. }
  305. savedQuery.query = stringifyQueryObject(conditions);
  306. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  307. eventView.additionalConditions.addTagValues('event.type', ['transaction']);
  308. return eventView;
  309. }
  310. function generateBackendPerformanceEventView(
  311. organization: LightWeightOrganization,
  312. location: Location
  313. ): EventView {
  314. const {query} = location;
  315. const hasStartAndEnd = query.start && query.end;
  316. const savedQuery: NewQuery = {
  317. id: undefined,
  318. name: t('Performance'),
  319. query: 'event.type:transaction',
  320. projects: [],
  321. fields: [
  322. 'key_transaction',
  323. 'transaction',
  324. 'project',
  325. 'transaction.op',
  326. 'http.method',
  327. 'tpm()',
  328. 'p50()',
  329. 'p95()',
  330. 'failure_rate()',
  331. `apdex(${organization.apdexThreshold})`,
  332. 'count_unique(user)',
  333. `count_miserable(user,${organization.apdexThreshold})`,
  334. `user_misery(${organization.apdexThreshold})`,
  335. ],
  336. version: 2,
  337. };
  338. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  339. widths[savedQuery.fields.length - 1] = '110';
  340. savedQuery.widths = widths;
  341. if (!query.statsPeriod && !hasStartAndEnd) {
  342. savedQuery.range = DEFAULT_STATS_PERIOD;
  343. }
  344. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  345. const searchQuery = decodeScalar(query.query, '');
  346. const conditions = tokenizeSearch(searchQuery);
  347. // This is not an override condition since we want the duration to appear in the search bar as a default.
  348. if (!conditions.hasTag('transaction.duration')) {
  349. conditions.setTagValues('transaction.duration', ['<15m']);
  350. }
  351. // If there is a bare text search, we want to treat it as a search
  352. // on the transaction name.
  353. if (conditions.query.length > 0) {
  354. conditions.setTagValues('transaction', [`*${conditions.query.join(' ')}*`]);
  355. conditions.query = [];
  356. }
  357. savedQuery.query = stringifyQueryObject(conditions);
  358. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  359. eventView.additionalConditions.addTagValues('event.type', ['transaction']);
  360. return eventView;
  361. }
  362. function generateFrontendPageloadPerformanceEventView(
  363. organization: LightWeightOrganization,
  364. location: Location
  365. ): EventView {
  366. const {query} = location;
  367. const hasStartAndEnd = query.start && query.end;
  368. const savedQuery: NewQuery = {
  369. id: undefined,
  370. name: t('Performance'),
  371. query: 'event.type:transaction',
  372. projects: [],
  373. fields: [
  374. 'key_transaction',
  375. 'transaction',
  376. 'project',
  377. 'tpm()',
  378. 'p75(measurements.fcp)',
  379. 'p75(measurements.lcp)',
  380. 'p75(measurements.fid)',
  381. 'p75(measurements.cls)',
  382. 'count_unique(user)',
  383. `count_miserable(user,${organization.apdexThreshold})`,
  384. `user_misery(${organization.apdexThreshold})`,
  385. ],
  386. version: 2,
  387. };
  388. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  389. widths[savedQuery.fields.length - 1] = '110';
  390. savedQuery.widths = widths;
  391. if (!query.statsPeriod && !hasStartAndEnd) {
  392. savedQuery.range = DEFAULT_STATS_PERIOD;
  393. }
  394. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  395. const searchQuery = decodeScalar(query.query, '');
  396. const conditions = tokenizeSearch(searchQuery);
  397. // This is not an override condition since we want the duration to appear in the search bar as a default.
  398. if (!conditions.hasTag('transaction.duration')) {
  399. conditions.setTagValues('transaction.duration', ['<15m']);
  400. }
  401. // If there is a bare text search, we want to treat it as a search
  402. // on the transaction name.
  403. if (conditions.query.length > 0) {
  404. conditions.setTagValues('transaction', [`*${conditions.query.join(' ')}*`]);
  405. conditions.query = [];
  406. }
  407. savedQuery.query = stringifyQueryObject(conditions);
  408. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  409. eventView.additionalConditions
  410. .addTagValues('event.type', ['transaction'])
  411. .addTagValues('transaction.op', ['pageload']);
  412. return eventView;
  413. }
  414. function generateFrontendOtherPerformanceEventView(
  415. organization: LightWeightOrganization,
  416. location: Location
  417. ): EventView {
  418. const {query} = location;
  419. const hasStartAndEnd = query.start && query.end;
  420. const savedQuery: NewQuery = {
  421. id: undefined,
  422. name: t('Performance'),
  423. query: 'event.type:transaction',
  424. projects: [],
  425. fields: [
  426. 'key_transaction',
  427. 'transaction',
  428. 'project',
  429. 'transaction.op',
  430. 'tpm()',
  431. 'p50(transaction.duration)',
  432. 'p75(transaction.duration)',
  433. 'p95(transaction.duration)',
  434. 'count_unique(user)',
  435. `count_miserable(user,${organization.apdexThreshold})`,
  436. `user_misery(${organization.apdexThreshold})`,
  437. ],
  438. version: 2,
  439. };
  440. const widths = Array(savedQuery.fields.length).fill(COL_WIDTH_UNDEFINED);
  441. widths[savedQuery.fields.length - 1] = '110';
  442. savedQuery.widths = widths;
  443. if (!query.statsPeriod && !hasStartAndEnd) {
  444. savedQuery.range = DEFAULT_STATS_PERIOD;
  445. }
  446. savedQuery.orderby = decodeScalar(query.sort, '-tpm');
  447. const searchQuery = decodeScalar(query.query, '');
  448. const conditions = tokenizeSearch(searchQuery);
  449. // This is not an override condition since we want the duration to appear in the search bar as a default.
  450. if (!conditions.hasTag('transaction.duration')) {
  451. conditions.setTagValues('transaction.duration', ['<15m']);
  452. }
  453. // If there is a bare text search, we want to treat it as a search
  454. // on the transaction name.
  455. if (conditions.query.length > 0) {
  456. conditions.setTagValues('transaction', [`*${conditions.query.join(' ')}*`]);
  457. conditions.query = [];
  458. }
  459. savedQuery.query = stringifyQueryObject(conditions);
  460. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  461. eventView.additionalConditions
  462. .addTagValues('event.type', ['transaction'])
  463. .addTagValues('!transaction.op', ['pageload']);
  464. return eventView;
  465. }
  466. export function generatePerformanceEventView(
  467. organization,
  468. location,
  469. projects,
  470. isTrends = false
  471. ) {
  472. const eventView = generateGenericPerformanceEventView(organization, location);
  473. if (isTrends) {
  474. return eventView;
  475. }
  476. const display = getCurrentLandingDisplay(location, projects, eventView);
  477. switch (display?.field) {
  478. case LandingDisplayField.FRONTEND_PAGELOAD:
  479. return generateFrontendPageloadPerformanceEventView(organization, location);
  480. case LandingDisplayField.FRONTEND_OTHER:
  481. return generateFrontendOtherPerformanceEventView(organization, location);
  482. case LandingDisplayField.BACKEND:
  483. return generateBackendPerformanceEventView(organization, location);
  484. default:
  485. return eventView;
  486. }
  487. }
  488. export function generatePerformanceVitalDetailView(
  489. _organization: LightWeightOrganization,
  490. location: Location
  491. ): EventView {
  492. const {query} = location;
  493. const vitalName = vitalNameFromLocation(location);
  494. const hasStartAndEnd = query.start && query.end;
  495. const savedQuery: NewQuery = {
  496. id: undefined,
  497. name: t('Vitals Performance Details'),
  498. query: 'event.type:transaction',
  499. projects: [],
  500. fields: [
  501. 'key_transaction',
  502. 'transaction',
  503. 'project',
  504. 'count_unique(user)',
  505. 'count()',
  506. `p50(${vitalName})`,
  507. `p75(${vitalName})`,
  508. `p95(${vitalName})`,
  509. getVitalDetailTablePoorStatusFunction(vitalName),
  510. getVitalDetailTableMehStatusFunction(vitalName),
  511. ],
  512. version: 2,
  513. };
  514. if (!query.statsPeriod && !hasStartAndEnd) {
  515. savedQuery.range = DEFAULT_STATS_PERIOD;
  516. }
  517. savedQuery.orderby = decodeScalar(query.sort, '-count');
  518. const searchQuery = decodeScalar(query.query, '');
  519. const conditions = tokenizeSearch(searchQuery);
  520. // If there is a bare text search, we want to treat it as a search
  521. // on the transaction name.
  522. if (conditions.query.length > 0) {
  523. conditions.setTagValues('transaction', [`*${conditions.query.join(' ')}*`]);
  524. conditions.query = [];
  525. }
  526. savedQuery.query = stringifyQueryObject(conditions);
  527. const eventView = EventView.fromNewQueryWithLocation(savedQuery, location);
  528. eventView.additionalConditions
  529. .addTagValues('event.type', ['transaction'])
  530. .addTagValues('has', [vitalName]);
  531. return eventView;
  532. }