index.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733
  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 EnvironmentPageFilter from 'sentry/components/environmentPageFilter';
  17. import {Body, Main, Side} from 'sentry/components/layouts/thirds';
  18. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  19. import {ChangeData} from 'sentry/components/organizations/timeRangeSelector';
  20. import PageTimeRangeSelector from 'sentry/components/pageTimeRangeSelector';
  21. import {DEFAULT_RELATIVE_PERIODS} from 'sentry/constants';
  22. import {t} from 'sentry/locale';
  23. import space from 'sentry/styles/space';
  24. import {
  25. NewQuery,
  26. Organization,
  27. PageFilters,
  28. ReleaseProject,
  29. SessionFieldWithOperation,
  30. } from 'sentry/types';
  31. import {getUtcDateString} from 'sentry/utils/dates';
  32. import {TableDataRow} from 'sentry/utils/discover/discoverQuery';
  33. import EventView from 'sentry/utils/discover/eventView';
  34. import {MobileVital, SpanOpBreakdown, WebVital} from 'sentry/utils/fields';
  35. import {formatVersion} from 'sentry/utils/formatters';
  36. import {decodeScalar} from 'sentry/utils/queryString';
  37. import routeTitleGen from 'sentry/utils/routeTitle';
  38. import withApi from 'sentry/utils/withApi';
  39. import withOrganization from 'sentry/utils/withOrganization';
  40. import withPageFilters from 'sentry/utils/withPageFilters';
  41. import AsyncView from 'sentry/views/asyncView';
  42. import {DisplayModes} from 'sentry/views/performance/transactionSummary/transactionOverview/charts';
  43. import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
  44. import {TrendChangeType, TrendView} from 'sentry/views/performance/trends/types';
  45. import {
  46. platformToPerformanceType,
  47. PROJECT_PERFORMANCE_TYPE,
  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 AsyncView<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 === PROJECT_PERFORMANCE_TYPE.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.SpansHttp})`,
  195. `p75(${SpanOpBreakdown.SpansBrowser})`,
  196. `p75(${SpanOpBreakdown.SpansResource})`,
  197. ],
  198. }) as EventView)
  199. : performanceType === PROJECT_PERFORMANCE_TYPE.BACKEND
  200. ? (EventView.fromSavedQuery({
  201. ...baseQuery,
  202. fields: [...baseQuery.fields, 'apdex()', 'p75(spans.http)', 'p75(spans.db)'],
  203. }) as EventView)
  204. : performanceType === PROJECT_PERFORMANCE_TYPE.MOBILE
  205. ? (EventView.fromSavedQuery({
  206. ...baseQuery,
  207. fields: [
  208. ...baseQuery.fields,
  209. `p75(${MobileVital.AppStartCold})`,
  210. `p75(${MobileVital.AppStartWarm})`,
  211. `p75(${MobileVital.FramesSlow})`,
  212. `p75(${MobileVital.FramesFrozen})`,
  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. return (
  396. <SessionsRequest {...sessionsRequestProps}>
  397. {({
  398. loading: allReleasesLoading,
  399. reloading: allReleasesReloading,
  400. errored: allReleasesErrored,
  401. response: allReleases,
  402. }) => (
  403. <SessionsRequest
  404. {...sessionsRequestProps}
  405. query={searchReleaseVersion(version)}
  406. >
  407. {({
  408. loading: thisReleaseLoading,
  409. reloading: thisReleaseReloading,
  410. errored: thisReleaseErrored,
  411. response: thisRelease,
  412. }) => {
  413. const loading = allReleasesLoading || thisReleaseLoading;
  414. const reloading = allReleasesReloading || thisReleaseReloading;
  415. const errored = allReleasesErrored || thisReleaseErrored;
  416. return (
  417. <Body>
  418. <Main>
  419. {isReleaseArchived(release) && (
  420. <ReleaseArchivedNotice
  421. onRestore={() => this.handleRestore(project, refetchData)}
  422. />
  423. )}
  424. <ReleaseDetailsPageFilters>
  425. <EnvironmentPageFilter />
  426. <PageTimeRangeSelector
  427. organization={organization}
  428. relative={period ?? ''}
  429. start={start ?? null}
  430. end={end ?? null}
  431. utc={utc ?? null}
  432. onUpdate={this.handleDateChange}
  433. relativeOptions={
  434. releaseBounds.type !== 'ancient'
  435. ? {
  436. [RELEASE_PERIOD_KEY]: (
  437. <Fragment>
  438. {releaseBounds.type === 'clamped'
  439. ? t('Clamped Release Period')
  440. : t('Entire Release Period')}{' '}
  441. (
  442. <DateTime
  443. date={releaseBounds.releaseStart}
  444. /> -{' '}
  445. <DateTime date={releaseBounds.releaseEnd} />)
  446. </Fragment>
  447. ),
  448. ...DEFAULT_RELATIVE_PERIODS,
  449. }
  450. : DEFAULT_RELATIVE_PERIODS
  451. }
  452. defaultPeriod={
  453. releaseBounds.type !== 'ancient'
  454. ? RELEASE_PERIOD_KEY
  455. : '90d'
  456. }
  457. defaultAbsolute={{
  458. start: moment(releaseBounds.releaseStart)
  459. .subtract(1, 'hour')
  460. .toDate(),
  461. end: releaseBounds.releaseEnd
  462. ? moment(releaseBounds.releaseEnd)
  463. .add(1, 'hour')
  464. .toDate()
  465. : undefined,
  466. }}
  467. />
  468. </ReleaseDetailsPageFilters>
  469. {(hasDiscover || hasPerformance || hasHealthData) && (
  470. <ReleaseComparisonChart
  471. release={release}
  472. releaseSessions={thisRelease}
  473. allSessions={allReleases}
  474. platform={project.platform}
  475. location={location}
  476. loading={loading}
  477. reloading={reloading}
  478. errored={errored}
  479. project={project}
  480. organization={organization}
  481. api={api}
  482. hasHealthData={hasHealthData}
  483. />
  484. )}
  485. <ReleaseIssues
  486. organization={organization}
  487. selection={selection}
  488. version={version}
  489. location={location}
  490. releaseBounds={releaseBounds}
  491. queryFilterDescription={t('In this release')}
  492. withChart
  493. />
  494. <Feature features={['performance-view']}>
  495. {hasReleaseComparisonPerformance ? (
  496. <PerformanceCardTable
  497. organization={organization}
  498. project={project}
  499. location={location}
  500. allReleasesEventView={allReleasesPerformanceView}
  501. releaseEventView={releasePerformanceView}
  502. performanceType={performanceType}
  503. />
  504. ) : (
  505. <TransactionsList
  506. location={location}
  507. organization={organization}
  508. eventView={releaseEventView}
  509. trendView={releaseTrendView}
  510. selected={selectedSort}
  511. options={sortOptions}
  512. handleDropdownChange={
  513. this.handleTransactionsListSortChange
  514. }
  515. titles={titles}
  516. generateLink={generateLink}
  517. />
  518. )}
  519. </Feature>
  520. </Main>
  521. <Side>
  522. <ReleaseStats
  523. organization={organization}
  524. release={release}
  525. project={project}
  526. />
  527. {hasHealthData && (
  528. <ReleaseAdoption
  529. releaseSessions={thisRelease}
  530. allSessions={allReleases}
  531. loading={loading}
  532. reloading={reloading}
  533. errored={errored}
  534. release={release}
  535. project={project}
  536. environment={environments}
  537. />
  538. )}
  539. <ProjectReleaseDetails
  540. release={release}
  541. releaseMeta={releaseMeta}
  542. orgSlug={organization.slug}
  543. projectSlug={project.slug}
  544. />
  545. {commitCount > 0 && (
  546. <CommitAuthorBreakdown
  547. version={version}
  548. orgId={organization.slug}
  549. projectSlug={project.slug}
  550. />
  551. )}
  552. {releaseMeta.projects.length > 1 && (
  553. <OtherProjects
  554. projects={releaseMeta.projects.filter(
  555. p => p.slug !== project.slug
  556. )}
  557. location={location}
  558. version={version}
  559. organization={organization}
  560. />
  561. )}
  562. {hasHealthData && (
  563. <TotalCrashFreeUsers
  564. organization={organization}
  565. version={version}
  566. projectSlug={project.slug}
  567. location={location}
  568. />
  569. )}
  570. {deploys.length > 0 && (
  571. <Deploys
  572. version={version}
  573. orgSlug={organization.slug}
  574. deploys={deploys}
  575. projectId={project.id}
  576. />
  577. )}
  578. </Side>
  579. </Body>
  580. );
  581. }}
  582. </SessionsRequest>
  583. )}
  584. </SessionsRequest>
  585. );
  586. }}
  587. </ReleaseContext.Consumer>
  588. );
  589. }
  590. }
  591. function generateTransactionLink(
  592. version: string,
  593. projectId: number,
  594. selection: PageFilters,
  595. value: string
  596. ) {
  597. return (
  598. organization: Organization,
  599. tableRow: TableDataRow,
  600. _query: Query
  601. ): LocationDescriptor => {
  602. const {transaction} = tableRow;
  603. const trendTransaction = ['regression', 'improved'].includes(value);
  604. const {environments, datetime} = selection;
  605. const {start, end, period} = datetime;
  606. return transactionSummaryRouteWithQuery({
  607. orgSlug: organization.slug,
  608. transaction: transaction! as string,
  609. query: {
  610. query: trendTransaction ? '' : `release:${version}`,
  611. environment: environments,
  612. start: start ? getUtcDateString(start) : undefined,
  613. end: end ? getUtcDateString(end) : undefined,
  614. statsPeriod: period,
  615. },
  616. projectID: projectId.toString(),
  617. display: trendTransaction ? DisplayModes.TREND : DisplayModes.DURATION,
  618. });
  619. };
  620. }
  621. function getDropdownOptions(): DropdownOption[] {
  622. return [
  623. {
  624. sort: {kind: 'desc', field: 'failure_count'},
  625. value: TransactionsListOption.FAILURE_COUNT,
  626. label: t('Failing Transactions'),
  627. },
  628. {
  629. sort: {kind: 'desc', field: 'epm'},
  630. value: TransactionsListOption.TPM,
  631. label: t('Frequent Transactions'),
  632. },
  633. {
  634. sort: {kind: 'desc', field: 'p50'},
  635. value: TransactionsListOption.SLOW,
  636. label: t('Slow Transactions'),
  637. },
  638. {
  639. sort: {kind: 'desc', field: 'p75_measurements_lcp'},
  640. value: TransactionsListOption.SLOW_LCP,
  641. label: t('Slow LCP'),
  642. },
  643. {
  644. sort: {kind: 'desc', field: 'trend_percentage()'},
  645. query: [['confidence()', '>6']],
  646. trendType: TrendChangeType.REGRESSION,
  647. value: TransactionsListOption.REGRESSION,
  648. label: t('Trending Regressions'),
  649. },
  650. {
  651. sort: {kind: 'asc', field: 'trend_percentage()'},
  652. query: [['confidence()', '>6']],
  653. trendType: TrendChangeType.IMPROVED,
  654. value: TransactionsListOption.IMPROVEMENT,
  655. label: t('Trending Improvements'),
  656. },
  657. ];
  658. }
  659. function getTransactionsListSort(location: Location): {
  660. selectedSort: DropdownOption;
  661. sortOptions: DropdownOption[];
  662. } {
  663. const sortOptions = getDropdownOptions();
  664. const urlParam = decodeScalar(
  665. location.query.showTransactions,
  666. TransactionsListOption.FAILURE_COUNT
  667. );
  668. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  669. return {selectedSort, sortOptions};
  670. }
  671. const ReleaseDetailsPageFilters = styled('div')`
  672. display: grid;
  673. grid-template-columns: minmax(0, max-content) 1fr;
  674. gap: ${space(2)};
  675. margin-bottom: ${space(2)};
  676. @media (max-width: ${p => p.theme.breakpoints.small}) {
  677. grid-template-columns: auto;
  678. }
  679. `;
  680. export default withApi(withPageFilters(withOrganization(ReleaseOverview)));