index.tsx 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747
  1. import {Fragment} from 'react';
  2. import type {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location, LocationDescriptor} from 'history';
  5. import moment from 'moment';
  6. import {restoreRelease} from 'sentry/actionCreators/release';
  7. import {Client} from 'sentry/api';
  8. import Feature from 'sentry/components/acl/feature';
  9. import SessionsRequest from 'sentry/components/charts/sessionsRequest';
  10. import type {DateTimeObject} from 'sentry/components/charts/utils';
  11. import {DateTime} from 'sentry/components/dateTime';
  12. import PerformanceCardTable from 'sentry/components/discover/performanceCardTable';
  13. import type {DropdownOption} from 'sentry/components/discover/transactionsList';
  14. import TransactionsList from 'sentry/components/discover/transactionsList';
  15. import * as Layout from 'sentry/components/layouts/thirds';
  16. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  17. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  18. import type {ChangeData} from 'sentry/components/timeRangeSelector';
  19. import {TimeRangeSelector} from 'sentry/components/timeRangeSelector';
  20. import {t} from 'sentry/locale';
  21. import {space} from 'sentry/styles/space';
  22. import type {NewQuery, Organization, PageFilters, ReleaseProject} from 'sentry/types';
  23. import {SessionFieldWithOperation} from 'sentry/types';
  24. import {browserHistory} from 'sentry/utils/browserHistory';
  25. import {getUtcDateString} from 'sentry/utils/dates';
  26. import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
  27. import EventView from 'sentry/utils/discover/eventView';
  28. import {MobileVital, SpanOpBreakdown, WebVital} from 'sentry/utils/fields';
  29. import {decodeScalar} from 'sentry/utils/queryString';
  30. import routeTitleGen from 'sentry/utils/routeTitle';
  31. import {formatVersion} from 'sentry/utils/versions/formatVersion';
  32. import withApi from 'sentry/utils/withApi';
  33. import withOrganization from 'sentry/utils/withOrganization';
  34. import withPageFilters from 'sentry/utils/withPageFilters';
  35. import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
  36. import {
  37. DisplayModes,
  38. transactionSummaryRouteWithQuery,
  39. } from 'sentry/views/performance/transactionSummary/utils';
  40. import type {TrendView} from 'sentry/views/performance/trends/types';
  41. import {TrendChangeType} from 'sentry/views/performance/trends/types';
  42. import {
  43. platformToPerformanceType,
  44. ProjectPerformanceType,
  45. } from 'sentry/views/performance/utils';
  46. import type {ReleaseBounds} from '../../utils';
  47. import {getReleaseParams, isReleaseArchived, searchReleaseVersion} from '../../utils';
  48. import {ReleaseContext} from '..';
  49. import CommitAuthorBreakdown from './sidebar/commitAuthorBreakdown';
  50. import Deploys from './sidebar/deploys';
  51. import OtherProjects from './sidebar/otherProjects';
  52. import ProjectReleaseDetails from './sidebar/projectReleaseDetails';
  53. import ReleaseAdoption from './sidebar/releaseAdoption';
  54. import ReleaseStats from './sidebar/releaseStats';
  55. import TotalCrashFreeUsers from './sidebar/totalCrashFreeUsers';
  56. import ReleaseArchivedNotice from './releaseArchivedNotice';
  57. import ReleaseComparisonChart from './releaseComparisonChart';
  58. import ReleaseIssues from './releaseIssues';
  59. const RELEASE_PERIOD_KEY = 'release';
  60. export enum TransactionsListOption {
  61. FAILURE_COUNT = 'failure_count',
  62. TPM = 'tpm',
  63. SLOW = 'slow',
  64. SLOW_LCP = 'slow_lcp',
  65. REGRESSION = 'regression',
  66. IMPROVEMENT = 'improved',
  67. }
  68. type RouteParams = {
  69. orgId: string;
  70. release: string;
  71. };
  72. type Props = RouteComponentProps<RouteParams, {}> & {
  73. api: Client;
  74. organization: Organization;
  75. selection: PageFilters;
  76. };
  77. class ReleaseOverview extends DeprecatedAsyncView<Props> {
  78. getTitle() {
  79. const {params, organization} = this.props;
  80. return routeTitleGen(
  81. t('Release %s', formatVersion(params.release)),
  82. organization.slug,
  83. false
  84. );
  85. }
  86. handleRestore = async (project: ReleaseProject, successCallback: () => void) => {
  87. const {params, organization} = this.props;
  88. try {
  89. await restoreRelease(new Client(), {
  90. orgSlug: organization.slug,
  91. projectSlug: project.slug,
  92. releaseVersion: params.release,
  93. });
  94. successCallback();
  95. } catch {
  96. // do nothing, action creator is already displaying error message
  97. }
  98. };
  99. getReleaseEventView(
  100. version: string,
  101. projectId: number,
  102. selectedSort: DropdownOption,
  103. releaseBounds: ReleaseBounds
  104. ): EventView {
  105. const {selection, location} = this.props;
  106. const {environments} = selection;
  107. const {start, end, statsPeriod} = getReleaseParams({
  108. location,
  109. releaseBounds,
  110. });
  111. const baseQuery: NewQuery = {
  112. id: undefined,
  113. version: 2,
  114. name: `Release ${formatVersion(version)}`,
  115. query: `event.type:transaction ${searchReleaseVersion(version)}`,
  116. fields: ['transaction', 'failure_count()', 'epm()', 'p50()'],
  117. orderby: '-failure_count',
  118. range: statsPeriod || undefined,
  119. environment: environments,
  120. projects: [projectId],
  121. start: start ? getUtcDateString(start) : undefined,
  122. end: end ? getUtcDateString(end) : undefined,
  123. };
  124. switch (selectedSort.value) {
  125. case TransactionsListOption.SLOW_LCP:
  126. return EventView.fromSavedQuery({
  127. ...baseQuery,
  128. query: `event.type:transaction release:${version} epm():>0.01 has:measurements.lcp`,
  129. fields: ['transaction', 'failure_count()', 'epm()', 'p75(measurements.lcp)'],
  130. orderby: 'p75_measurements_lcp',
  131. });
  132. case TransactionsListOption.SLOW:
  133. return EventView.fromSavedQuery({
  134. ...baseQuery,
  135. query: `event.type:transaction release:${version} epm():>0.01`,
  136. });
  137. case TransactionsListOption.FAILURE_COUNT:
  138. return EventView.fromSavedQuery({
  139. ...baseQuery,
  140. query: `event.type:transaction release:${version} failure_count():>0`,
  141. });
  142. default:
  143. return EventView.fromSavedQuery(baseQuery);
  144. }
  145. }
  146. getReleaseTrendView(
  147. version: string,
  148. projectId: number,
  149. versionDate: string,
  150. releaseBounds: ReleaseBounds
  151. ): EventView {
  152. const {selection, location} = this.props;
  153. const {environments} = selection;
  154. const {start, end, statsPeriod} = getReleaseParams({
  155. location,
  156. releaseBounds,
  157. });
  158. const trendView = EventView.fromSavedQuery({
  159. id: undefined,
  160. version: 2,
  161. name: `Release ${formatVersion(version)}`,
  162. fields: ['transaction'],
  163. query: 'tpm():>0.01 trend_percentage():>0%',
  164. range: statsPeriod || undefined,
  165. environment: environments,
  166. projects: [projectId],
  167. start: start ? getUtcDateString(start) : undefined,
  168. end: end ? getUtcDateString(end) : undefined,
  169. }) as TrendView;
  170. trendView.middle = versionDate;
  171. return trendView;
  172. }
  173. getReleasePerformanceEventView(
  174. performanceType: string,
  175. baseQuery: NewQuery
  176. ): EventView {
  177. const eventView =
  178. performanceType === ProjectPerformanceType.FRONTEND
  179. ? (EventView.fromSavedQuery({
  180. ...baseQuery,
  181. fields: [
  182. ...baseQuery.fields,
  183. `p75(${WebVital.FCP})`,
  184. `p75(${WebVital.FID})`,
  185. `p75(${WebVital.LCP})`,
  186. `p75(${WebVital.CLS})`,
  187. `p75(${SpanOpBreakdown.SPANS_HTTP})`,
  188. `p75(${SpanOpBreakdown.SPANS_BROWSER})`,
  189. `p75(${SpanOpBreakdown.SPANS_RESOURCE})`,
  190. ],
  191. }) as EventView)
  192. : performanceType === ProjectPerformanceType.BACKEND
  193. ? (EventView.fromSavedQuery({
  194. ...baseQuery,
  195. fields: [
  196. ...baseQuery.fields,
  197. 'apdex()',
  198. 'p75(spans.http)',
  199. 'p75(spans.db)',
  200. ],
  201. }) as EventView)
  202. : performanceType === ProjectPerformanceType.MOBILE
  203. ? (EventView.fromSavedQuery({
  204. ...baseQuery,
  205. fields: [
  206. ...baseQuery.fields,
  207. `p75(${MobileVital.APP_START_COLD})`,
  208. `p75(${MobileVital.APP_START_WARM})`,
  209. `p75(${MobileVital.FRAMES_SLOW})`,
  210. `p75(${MobileVital.FRAMES_FROZEN})`,
  211. ],
  212. }) as EventView)
  213. : (EventView.fromSavedQuery({
  214. ...baseQuery,
  215. }) as EventView);
  216. return eventView;
  217. }
  218. getAllReleasesPerformanceView(
  219. projectId: number,
  220. performanceType: string,
  221. releaseBounds: ReleaseBounds
  222. ) {
  223. const {selection, location} = this.props;
  224. const {environments} = selection;
  225. const {start, end, statsPeriod} = getReleaseParams({
  226. location,
  227. releaseBounds,
  228. });
  229. const baseQuery: NewQuery = {
  230. id: undefined,
  231. version: 2,
  232. name: 'All Releases',
  233. query: 'event.type:transaction',
  234. fields: ['user_misery()'],
  235. range: statsPeriod || undefined,
  236. environment: environments,
  237. projects: [projectId],
  238. start: start ? getUtcDateString(start) : undefined,
  239. end: end ? getUtcDateString(end) : undefined,
  240. };
  241. return this.getReleasePerformanceEventView(performanceType, baseQuery);
  242. }
  243. getReleasePerformanceView(
  244. version: string,
  245. projectId: number,
  246. performanceType: string,
  247. releaseBounds: ReleaseBounds
  248. ) {
  249. const {selection, location} = this.props;
  250. const {environments} = selection;
  251. const {start, end, statsPeriod} = getReleaseParams({
  252. location,
  253. releaseBounds,
  254. });
  255. const baseQuery: NewQuery = {
  256. id: undefined,
  257. version: 2,
  258. name: `Release:${version}`,
  259. query: `event.type:transaction release:${version}`,
  260. fields: ['user_misery()'],
  261. range: statsPeriod || undefined,
  262. environment: environments,
  263. projects: [projectId],
  264. start: start ? getUtcDateString(start) : undefined,
  265. end: end ? getUtcDateString(end) : undefined,
  266. };
  267. return this.getReleasePerformanceEventView(performanceType, baseQuery);
  268. }
  269. get pageDateTime(): DateTimeObject {
  270. const query = this.props.location.query;
  271. const {start, end, statsPeriod} = normalizeDateTimeParams(query, {
  272. allowEmptyPeriod: true,
  273. allowAbsoluteDatetime: true,
  274. allowAbsolutePageDatetime: true,
  275. });
  276. if (statsPeriod) {
  277. return {period: statsPeriod};
  278. }
  279. if (start && end) {
  280. return {
  281. start: moment.utc(start).format(),
  282. end: moment.utc(end).format(),
  283. };
  284. }
  285. return {};
  286. }
  287. handleTransactionsListSortChange = (value: string) => {
  288. const {location} = this.props;
  289. const target = {
  290. pathname: location.pathname,
  291. query: {...location.query, showTransactions: value, transactionCursor: undefined},
  292. };
  293. browserHistory.push(target);
  294. };
  295. handleDateChange = (datetime: ChangeData) => {
  296. const {router, location} = this.props;
  297. const {start, end, relative, utc} = datetime;
  298. if (start && end) {
  299. const parser = utc ? moment.utc : moment;
  300. router.push({
  301. ...location,
  302. query: {
  303. ...location.query,
  304. pageStatsPeriod: undefined,
  305. pageStart: parser(start).format(),
  306. pageEnd: parser(end).format(),
  307. pageUtc: utc ?? undefined,
  308. },
  309. });
  310. return;
  311. }
  312. router.push({
  313. ...location,
  314. query: {
  315. ...location.query,
  316. pageStatsPeriod: relative === RELEASE_PERIOD_KEY ? undefined : relative,
  317. pageStart: undefined,
  318. pageEnd: undefined,
  319. pageUtc: undefined,
  320. },
  321. });
  322. };
  323. render() {
  324. const {organization, selection, location, api} = this.props;
  325. const {start, end, period, utc} = this.pageDateTime;
  326. return (
  327. <ReleaseContext.Consumer>
  328. {({
  329. release,
  330. project,
  331. deploys,
  332. releaseMeta,
  333. refetchData,
  334. hasHealthData,
  335. releaseBounds,
  336. }) => {
  337. const {commitCount, version} = release;
  338. const hasDiscover = organization.features.includes('discover-basic');
  339. const hasPerformance = organization.features.includes('performance-view');
  340. const hasReleaseComparisonPerformance = organization.features.includes(
  341. 'release-comparison-performance'
  342. );
  343. const {environments} = selection;
  344. const performanceType = platformToPerformanceType([project], [project.id]);
  345. const {selectedSort, sortOptions} = getTransactionsListSort(location);
  346. const releaseEventView = this.getReleaseEventView(
  347. version,
  348. project.id,
  349. selectedSort,
  350. releaseBounds
  351. );
  352. const titles =
  353. selectedSort.value !== TransactionsListOption.SLOW_LCP
  354. ? [t('transaction'), t('failure_count()'), t('tpm()'), t('p50()')]
  355. : [t('transaction'), t('failure_count()'), t('tpm()'), t('p75(lcp)')];
  356. const releaseTrendView = this.getReleaseTrendView(
  357. version,
  358. project.id,
  359. releaseMeta.released,
  360. releaseBounds
  361. );
  362. const allReleasesPerformanceView = this.getAllReleasesPerformanceView(
  363. project.id,
  364. performanceType,
  365. releaseBounds
  366. );
  367. const releasePerformanceView = this.getReleasePerformanceView(
  368. version,
  369. project.id,
  370. performanceType,
  371. releaseBounds
  372. );
  373. const generateLink = {
  374. transaction: generateTransactionLink(
  375. version,
  376. project.id,
  377. selection,
  378. location.query.showTransactions
  379. ),
  380. };
  381. const sessionsRequestProps: Omit<SessionsRequest['props'], 'children'> = {
  382. api,
  383. organization,
  384. field: [
  385. SessionFieldWithOperation.USERS,
  386. SessionFieldWithOperation.SESSIONS,
  387. SessionFieldWithOperation.DURATION,
  388. ],
  389. groupBy: ['session.status'],
  390. ...getReleaseParams({location, releaseBounds}),
  391. shouldFilterSessionsInTimeWindow: true,
  392. };
  393. const defaultDateTimeSelected = !period && !start && !end;
  394. const releaseBoundsLabel =
  395. releaseBounds.type === 'clamped'
  396. ? t('Clamped Release Period')
  397. : t('Entire Release Period');
  398. return (
  399. <SessionsRequest {...sessionsRequestProps}>
  400. {({
  401. loading: allReleasesLoading,
  402. reloading: allReleasesReloading,
  403. errored: allReleasesErrored,
  404. response: allReleases,
  405. }) => (
  406. <SessionsRequest
  407. {...sessionsRequestProps}
  408. query={searchReleaseVersion(version)}
  409. >
  410. {({
  411. loading: thisReleaseLoading,
  412. reloading: thisReleaseReloading,
  413. errored: thisReleaseErrored,
  414. response: thisRelease,
  415. }) => {
  416. const loading = allReleasesLoading || thisReleaseLoading;
  417. const reloading = allReleasesReloading || thisReleaseReloading;
  418. const errored = allReleasesErrored || thisReleaseErrored;
  419. return (
  420. <Layout.Body>
  421. <Layout.Main>
  422. {isReleaseArchived(release) && (
  423. <ReleaseArchivedNotice
  424. onRestore={() => this.handleRestore(project, refetchData)}
  425. />
  426. )}
  427. <ReleaseDetailsPageFilters>
  428. <EnvironmentPageFilter />
  429. <TimeRangeSelector
  430. relative={
  431. period ??
  432. (defaultDateTimeSelected ? RELEASE_PERIOD_KEY : null)
  433. }
  434. start={start ?? null}
  435. end={end ?? null}
  436. utc={utc ?? null}
  437. onChange={this.handleDateChange}
  438. menuTitle={t('Filter Time Range')}
  439. triggerLabel={
  440. defaultDateTimeSelected ? releaseBoundsLabel : null
  441. }
  442. relativeOptions={({defaultOptions, arbitraryOptions}) =>
  443. releaseBounds.type !== 'ancient'
  444. ? {
  445. [RELEASE_PERIOD_KEY]: (
  446. <Fragment>
  447. {releaseBoundsLabel}
  448. <br />
  449. <ReleaseBoundsDescription
  450. primary={defaultDateTimeSelected}
  451. >
  452. <DateTime date={releaseBounds.releaseStart} />
  453. –<DateTime date={releaseBounds.releaseEnd} />
  454. </ReleaseBoundsDescription>
  455. </Fragment>
  456. ),
  457. ...defaultOptions,
  458. ...arbitraryOptions,
  459. }
  460. : {...defaultOptions, ...arbitraryOptions}
  461. }
  462. defaultPeriod={
  463. releaseBounds.type !== 'ancient'
  464. ? RELEASE_PERIOD_KEY
  465. : '90d'
  466. }
  467. defaultAbsolute={{
  468. start: moment(releaseBounds.releaseStart)
  469. .subtract(1, 'hour')
  470. .toDate(),
  471. end: releaseBounds.releaseEnd
  472. ? moment(releaseBounds.releaseEnd)
  473. .add(1, 'hour')
  474. .toDate()
  475. : undefined,
  476. }}
  477. />
  478. </ReleaseDetailsPageFilters>
  479. {(hasDiscover || hasPerformance || hasHealthData) && (
  480. <ReleaseComparisonChart
  481. release={release}
  482. releaseSessions={thisRelease}
  483. allSessions={allReleases}
  484. platform={project.platform}
  485. location={location}
  486. loading={loading}
  487. reloading={reloading}
  488. errored={errored}
  489. project={project}
  490. organization={organization}
  491. api={api}
  492. hasHealthData={hasHealthData}
  493. />
  494. )}
  495. <ReleaseIssues
  496. organization={organization}
  497. version={version}
  498. location={location}
  499. releaseBounds={releaseBounds}
  500. queryFilterDescription={t('In this release')}
  501. withChart
  502. />
  503. <Feature features="performance-view">
  504. {hasReleaseComparisonPerformance ? (
  505. <PerformanceCardTable
  506. organization={organization}
  507. project={project}
  508. location={location}
  509. allReleasesEventView={allReleasesPerformanceView}
  510. releaseEventView={releasePerformanceView}
  511. performanceType={performanceType}
  512. />
  513. ) : (
  514. <TransactionsList
  515. location={location}
  516. organization={organization}
  517. eventView={releaseEventView}
  518. trendView={releaseTrendView}
  519. selected={selectedSort}
  520. options={sortOptions}
  521. handleDropdownChange={
  522. this.handleTransactionsListSortChange
  523. }
  524. titles={titles}
  525. generateLink={generateLink}
  526. supportsInvestigationRule={false}
  527. />
  528. )}
  529. </Feature>
  530. </Layout.Main>
  531. <Layout.Side>
  532. <ReleaseStats
  533. organization={organization}
  534. release={release}
  535. project={project}
  536. />
  537. {hasHealthData && (
  538. <ReleaseAdoption
  539. releaseSessions={thisRelease}
  540. allSessions={allReleases}
  541. loading={loading}
  542. reloading={reloading}
  543. errored={errored}
  544. release={release}
  545. project={project}
  546. environment={environments}
  547. />
  548. )}
  549. <ProjectReleaseDetails
  550. release={release}
  551. releaseMeta={releaseMeta}
  552. projectSlug={project.slug}
  553. />
  554. {commitCount > 0 && (
  555. <CommitAuthorBreakdown
  556. version={version}
  557. orgId={organization.slug}
  558. projectSlug={project.slug}
  559. />
  560. )}
  561. {releaseMeta.projects.length > 1 && (
  562. <OtherProjects
  563. projects={releaseMeta.projects.filter(
  564. p => p.slug !== project.slug
  565. )}
  566. location={location}
  567. version={version}
  568. organization={organization}
  569. />
  570. )}
  571. {hasHealthData && (
  572. <TotalCrashFreeUsers
  573. organization={organization}
  574. version={version}
  575. projectSlug={project.slug}
  576. location={location}
  577. />
  578. )}
  579. {deploys.length > 0 && (
  580. <Deploys
  581. version={version}
  582. orgSlug={organization.slug}
  583. deploys={deploys}
  584. projectId={project.id}
  585. />
  586. )}
  587. </Layout.Side>
  588. </Layout.Body>
  589. );
  590. }}
  591. </SessionsRequest>
  592. )}
  593. </SessionsRequest>
  594. );
  595. }}
  596. </ReleaseContext.Consumer>
  597. );
  598. }
  599. }
  600. function generateTransactionLink(
  601. version: string,
  602. projectId: number,
  603. selection: PageFilters,
  604. value: string
  605. ) {
  606. return (
  607. organization: Organization,
  608. tableRow: TableDataRow,
  609. _location: Location
  610. ): LocationDescriptor => {
  611. const {transaction} = tableRow;
  612. const trendTransaction = ['regression', 'improved'].includes(value);
  613. const {environments, datetime} = selection;
  614. const {start, end, period} = datetime;
  615. return transactionSummaryRouteWithQuery({
  616. orgSlug: organization.slug,
  617. transaction: transaction! as string,
  618. query: {
  619. query: trendTransaction ? '' : `release:${version}`,
  620. environment: environments,
  621. start: start ? getUtcDateString(start) : undefined,
  622. end: end ? getUtcDateString(end) : undefined,
  623. statsPeriod: period,
  624. },
  625. projectID: projectId.toString(),
  626. display: trendTransaction ? DisplayModes.TREND : DisplayModes.DURATION,
  627. });
  628. };
  629. }
  630. function getDropdownOptions(): DropdownOption[] {
  631. return [
  632. {
  633. sort: {kind: 'desc', field: 'failure_count'},
  634. value: TransactionsListOption.FAILURE_COUNT,
  635. label: t('Failing Transactions'),
  636. },
  637. {
  638. sort: {kind: 'desc', field: 'epm'},
  639. value: TransactionsListOption.TPM,
  640. label: t('Frequent Transactions'),
  641. },
  642. {
  643. sort: {kind: 'desc', field: 'p50'},
  644. value: TransactionsListOption.SLOW,
  645. label: t('Slow Transactions'),
  646. },
  647. {
  648. sort: {kind: 'desc', field: 'p75_measurements_lcp'},
  649. value: TransactionsListOption.SLOW_LCP,
  650. label: t('Slow LCP'),
  651. },
  652. {
  653. sort: {kind: 'desc', field: 'trend_percentage()'},
  654. query: [['confidence()', '>6']],
  655. trendType: TrendChangeType.REGRESSION,
  656. value: TransactionsListOption.REGRESSION,
  657. label: t('Trending Regressions'),
  658. },
  659. {
  660. sort: {kind: 'asc', field: 'trend_percentage()'},
  661. query: [['confidence()', '>6']],
  662. trendType: TrendChangeType.IMPROVED,
  663. value: TransactionsListOption.IMPROVEMENT,
  664. label: t('Trending Improvements'),
  665. },
  666. ];
  667. }
  668. function getTransactionsListSort(location: Location): {
  669. selectedSort: DropdownOption;
  670. sortOptions: DropdownOption[];
  671. } {
  672. const sortOptions = getDropdownOptions();
  673. const urlParam = decodeScalar(
  674. location.query.showTransactions,
  675. TransactionsListOption.FAILURE_COUNT
  676. );
  677. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  678. return {selectedSort, sortOptions};
  679. }
  680. const ReleaseDetailsPageFilters = styled('div')`
  681. display: grid;
  682. grid-template-columns: minmax(0, max-content) 1fr;
  683. gap: ${space(2)};
  684. margin-bottom: ${space(2)};
  685. @media (max-width: ${p => p.theme.breakpoints.small}) {
  686. grid-template-columns: auto;
  687. }
  688. `;
  689. const ReleaseBoundsDescription = styled('span')<{primary: boolean}>`
  690. font-size: ${p => p.theme.fontSizeSmall};
  691. color: ${p => (p.primary ? p.theme.activeText : p.theme.subText)};
  692. `;
  693. ReleaseOverview.contextType = ReleaseContext;
  694. export default withApi(withPageFilters(withOrganization(ReleaseOverview)));