index.tsx 26 KB

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