index.tsx 27 KB

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