index.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694
  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 'app/actionCreators/release';
  7. import {Client} from 'app/api';
  8. import Feature from 'app/components/acl/feature';
  9. import {DateTimeObject} from 'app/components/charts/utils';
  10. import DateTime from 'app/components/dateTime';
  11. import TransactionsList, {DropdownOption} from 'app/components/discover/transactionsList';
  12. import {Body, Main, Side} from 'app/components/layouts/thirds';
  13. import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
  14. import {ChangeData} from 'app/components/organizations/timeRangeSelector';
  15. import PageTimeRangeSelector from 'app/components/organizations/timeRangeSelector/pageTimeRangeSelector';
  16. import {DEFAULT_RELATIVE_PERIODS} from 'app/constants';
  17. import {t} from 'app/locale';
  18. import space from 'app/styles/space';
  19. import {GlobalSelection, NewQuery, Organization, ReleaseProject} from 'app/types';
  20. import {trackAnalyticsEvent} from 'app/utils/analytics';
  21. import {getUtcDateString} from 'app/utils/dates';
  22. import {TableDataRow} from 'app/utils/discover/discoverQuery';
  23. import EventView from 'app/utils/discover/eventView';
  24. import {WebVital} from 'app/utils/discover/fields';
  25. import {formatVersion} from 'app/utils/formatters';
  26. import {decodeScalar} from 'app/utils/queryString';
  27. import routeTitleGen from 'app/utils/routeTitle';
  28. import withApi from 'app/utils/withApi';
  29. import withGlobalSelection from 'app/utils/withGlobalSelection';
  30. import withOrganization from 'app/utils/withOrganization';
  31. import AsyncView from 'app/views/asyncView';
  32. import {DisplayModes} from 'app/views/performance/transactionSummary/charts';
  33. import {transactionSummaryRouteWithQuery} from 'app/views/performance/transactionSummary/utils';
  34. import {TrendChangeType, TrendView} from 'app/views/performance/trends/types';
  35. import {getReleaseParams, isReleaseArchived, ReleaseBounds} from '../../utils';
  36. import {ReleaseContext} from '..';
  37. import ReleaseChart from './chart/';
  38. import {EventType, YAxis} from './chart/releaseChartControls';
  39. import CommitAuthorBreakdown from './commitAuthorBreakdown';
  40. import Deploys from './deploys';
  41. import Issues from './issues';
  42. import OtherProjects from './otherProjects';
  43. import ProjectReleaseDetails from './projectReleaseDetails';
  44. import ReleaseAdoption from './releaseAdoption';
  45. import ReleaseArchivedNotice from './releaseArchivedNotice';
  46. import ReleaseComparisonChart from './releaseComparisonChart';
  47. import ReleaseDetailsRequest from './releaseDetailsRequest';
  48. import ReleaseStats from './releaseStats';
  49. import TotalCrashFreeUsers from './totalCrashFreeUsers';
  50. const RELEASE_PERIOD_KEY = 'release';
  51. export enum TransactionsListOption {
  52. FAILURE_COUNT = 'failure_count',
  53. TPM = 'tpm',
  54. SLOW = 'slow',
  55. SLOW_LCP = 'slow_lcp',
  56. REGRESSION = 'regression',
  57. IMPROVEMENT = 'improved',
  58. }
  59. type RouteParams = {
  60. orgId: string;
  61. release: string;
  62. };
  63. type Props = RouteComponentProps<RouteParams, {}> & {
  64. organization: Organization;
  65. selection: GlobalSelection;
  66. api: Client;
  67. };
  68. class ReleaseOverview extends AsyncView<Props> {
  69. getTitle() {
  70. const {params, organization} = this.props;
  71. return routeTitleGen(
  72. t('Release %s', formatVersion(params.release)),
  73. organization.slug,
  74. false
  75. );
  76. }
  77. handleYAxisChange = (yAxis: YAxis, project: ReleaseProject) => {
  78. const {location, router, organization} = this.props;
  79. const {eventType, vitalType, ...query} = location.query;
  80. trackAnalyticsEvent({
  81. eventKey: `release_detail.change_chart`,
  82. eventName: `Release Detail: Change Chart`,
  83. organization_id: parseInt(organization.id, 10),
  84. display: yAxis,
  85. eventType,
  86. vitalType,
  87. platform: project.platform,
  88. });
  89. router.push({
  90. ...location,
  91. query: {...query, yAxis},
  92. });
  93. };
  94. handleEventTypeChange = (eventType: EventType, project: ReleaseProject) => {
  95. const {location, router, organization} = this.props;
  96. trackAnalyticsEvent({
  97. eventKey: `release_detail.change_chart`,
  98. eventName: `Release Detail: Change Chart`,
  99. organization_id: parseInt(organization.id, 10),
  100. display: YAxis.EVENTS,
  101. eventType,
  102. platform: project.platform,
  103. });
  104. router.push({
  105. ...location,
  106. query: {...location.query, eventType},
  107. });
  108. };
  109. handleVitalTypeChange = (vitalType: WebVital, project: ReleaseProject) => {
  110. const {location, router, organization} = this.props;
  111. trackAnalyticsEvent({
  112. eventKey: `release_detail.change_chart`,
  113. eventName: `Release Detail: Change Chart`,
  114. organization_id: parseInt(organization.id, 10),
  115. display: YAxis.COUNT_VITAL,
  116. vitalType,
  117. platform: project.platform,
  118. });
  119. router.push({
  120. ...location,
  121. query: {...location.query, vitalType},
  122. });
  123. };
  124. handleRestore = async (project: ReleaseProject, successCallback: () => void) => {
  125. const {params, organization} = this.props;
  126. try {
  127. await restoreRelease(new Client(), {
  128. orgSlug: organization.slug,
  129. projectSlug: project.slug,
  130. releaseVersion: params.release,
  131. });
  132. successCallback();
  133. } catch {
  134. // do nothing, action creator is already displaying error message
  135. }
  136. };
  137. getYAxis(hasHealthData: boolean, hasPerformance: boolean): YAxis {
  138. const {yAxis} = this.props.location.query;
  139. if (typeof yAxis === 'string') {
  140. if (Object.values(YAxis).includes(yAxis as YAxis)) {
  141. return yAxis as YAxis;
  142. }
  143. }
  144. if (hasHealthData) {
  145. return YAxis.SESSIONS;
  146. }
  147. if (hasPerformance) {
  148. return YAxis.FAILED_TRANSACTIONS;
  149. }
  150. return YAxis.EVENTS;
  151. }
  152. getEventType(yAxis: YAxis): EventType {
  153. if (yAxis === YAxis.EVENTS) {
  154. const {eventType} = this.props.location.query;
  155. if (typeof eventType === 'string') {
  156. if (Object.values(EventType).includes(eventType as EventType)) {
  157. return eventType as EventType;
  158. }
  159. }
  160. }
  161. return EventType.ALL;
  162. }
  163. getVitalType(yAxis: YAxis): WebVital {
  164. if (yAxis === YAxis.COUNT_VITAL) {
  165. const {vitalType} = this.props.location.query;
  166. if (typeof vitalType === 'string') {
  167. if (Object.values(WebVital).includes(vitalType as WebVital)) {
  168. return vitalType as WebVital;
  169. }
  170. }
  171. }
  172. return WebVital.LCP;
  173. }
  174. getReleaseEventView(
  175. version: string,
  176. projectId: number,
  177. selectedSort: DropdownOption,
  178. releaseBounds: ReleaseBounds,
  179. defaultStatsPeriod: string
  180. ): EventView {
  181. const {selection, location, organization} = this.props;
  182. const {environments} = selection;
  183. const {start, end, statsPeriod} = getReleaseParams({
  184. location,
  185. releaseBounds,
  186. defaultStatsPeriod,
  187. allowEmptyPeriod: organization.features.includes('release-comparison'),
  188. });
  189. const baseQuery: NewQuery = {
  190. id: undefined,
  191. version: 2,
  192. name: `Release ${formatVersion(version)}`,
  193. query: `event.type:transaction release:${version}`,
  194. fields: ['transaction', 'failure_count()', 'epm()', 'p50()'],
  195. orderby: '-failure_count',
  196. range: statsPeriod || undefined,
  197. environment: environments,
  198. projects: [projectId],
  199. start: start ? getUtcDateString(start) : undefined,
  200. end: end ? getUtcDateString(end) : undefined,
  201. };
  202. switch (selectedSort.value) {
  203. case TransactionsListOption.SLOW_LCP:
  204. return EventView.fromSavedQuery({
  205. ...baseQuery,
  206. query: `event.type:transaction release:${version} epm():>0.01 has:measurements.lcp`,
  207. fields: ['transaction', 'failure_count()', 'epm()', 'p75(measurements.lcp)'],
  208. orderby: 'p75_measurements_lcp',
  209. });
  210. case TransactionsListOption.SLOW:
  211. return EventView.fromSavedQuery({
  212. ...baseQuery,
  213. query: `event.type:transaction release:${version} epm():>0.01`,
  214. });
  215. case TransactionsListOption.FAILURE_COUNT:
  216. return EventView.fromSavedQuery({
  217. ...baseQuery,
  218. query: `event.type:transaction release:${version} failure_count():>0`,
  219. });
  220. default:
  221. return EventView.fromSavedQuery(baseQuery);
  222. }
  223. }
  224. getReleaseTrendView(
  225. version: string,
  226. projectId: number,
  227. versionDate: string,
  228. releaseBounds: ReleaseBounds,
  229. defaultStatsPeriod: string
  230. ): EventView {
  231. const {selection, location, organization} = this.props;
  232. const {environments} = selection;
  233. const {start, end, statsPeriod} = getReleaseParams({
  234. location,
  235. releaseBounds,
  236. defaultStatsPeriod,
  237. allowEmptyPeriod: organization.features.includes('release-comparison'),
  238. });
  239. const trendView = EventView.fromSavedQuery({
  240. id: undefined,
  241. version: 2,
  242. name: `Release ${formatVersion(version)}`,
  243. fields: ['transaction'],
  244. query: 'tpm():>0.01 trend_percentage():>0%',
  245. range: statsPeriod || undefined,
  246. environment: environments,
  247. projects: [projectId],
  248. start: start ? getUtcDateString(start) : undefined,
  249. end: end ? getUtcDateString(end) : undefined,
  250. }) as TrendView;
  251. trendView.middle = versionDate;
  252. return trendView;
  253. }
  254. get pageDateTime(): DateTimeObject {
  255. const query = this.props.location.query;
  256. const {start, end, statsPeriod} = getParams(query, {
  257. allowEmptyPeriod: true,
  258. allowAbsoluteDatetime: true,
  259. allowAbsolutePageDatetime: true,
  260. });
  261. if (statsPeriod) {
  262. return {period: statsPeriod};
  263. }
  264. if (start && end) {
  265. return {
  266. start: moment.utc(start).format(),
  267. end: moment.utc(end).format(),
  268. };
  269. }
  270. return {};
  271. }
  272. handleTransactionsListSortChange = (value: string) => {
  273. const {location} = this.props;
  274. const target = {
  275. pathname: location.pathname,
  276. query: {...location.query, showTransactions: value, transactionCursor: undefined},
  277. };
  278. browserHistory.push(target);
  279. };
  280. handleDateChange = (datetime: ChangeData) => {
  281. const {router, location} = this.props;
  282. const {start, end, relative, utc} = datetime;
  283. if (start && end) {
  284. const parser = utc ? moment.utc : moment;
  285. router.push({
  286. ...location,
  287. query: {
  288. ...location.query,
  289. pageStatsPeriod: undefined,
  290. pageStart: parser(start).format(),
  291. pageEnd: parser(end).format(),
  292. pageUtc: utc ?? undefined,
  293. },
  294. });
  295. return;
  296. }
  297. router.push({
  298. ...location,
  299. query: {
  300. ...location.query,
  301. pageStatsPeriod: relative === RELEASE_PERIOD_KEY ? undefined : relative,
  302. pageStart: undefined,
  303. pageEnd: undefined,
  304. pageUtc: undefined,
  305. },
  306. });
  307. };
  308. render() {
  309. const {organization, selection, location, api, router} = this.props;
  310. const {start, end, period, utc} = this.pageDateTime;
  311. return (
  312. <ReleaseContext.Consumer>
  313. {({
  314. release,
  315. project,
  316. deploys,
  317. releaseMeta,
  318. refetchData,
  319. defaultStatsPeriod,
  320. isHealthLoading,
  321. getHealthData,
  322. hasHealthData,
  323. releaseBounds,
  324. }) => {
  325. const {commitCount, version} = release;
  326. const hasDiscover = organization.features.includes('discover-basic');
  327. const hasPerformance = organization.features.includes('performance-view');
  328. const yAxis = this.getYAxis(hasHealthData, hasPerformance);
  329. const eventType = this.getEventType(yAxis);
  330. const vitalType = this.getVitalType(yAxis);
  331. const {selectedSort, sortOptions} = getTransactionsListSort(location);
  332. const releaseEventView = this.getReleaseEventView(
  333. version,
  334. project.id,
  335. selectedSort,
  336. releaseBounds,
  337. defaultStatsPeriod
  338. );
  339. const titles =
  340. selectedSort.value !== TransactionsListOption.SLOW_LCP
  341. ? [t('transaction'), t('failure_count()'), t('tpm()'), t('p50()')]
  342. : [t('transaction'), t('failure_count()'), t('tpm()'), t('p75(lcp)')];
  343. const releaseTrendView = this.getReleaseTrendView(
  344. version,
  345. project.id,
  346. releaseMeta.released,
  347. releaseBounds,
  348. defaultStatsPeriod
  349. );
  350. const generateLink = {
  351. transaction: generateTransactionLink(
  352. version,
  353. project.id,
  354. selection,
  355. location.query.showTransactions
  356. ),
  357. };
  358. return (
  359. <ReleaseDetailsRequest
  360. organization={organization}
  361. location={location}
  362. disable={!organization.features.includes('release-comparison')}
  363. version={version}
  364. releaseBounds={releaseBounds}
  365. >
  366. {({thisRelease, allReleases, loading, reloading, errored}) => (
  367. <Body>
  368. <Main>
  369. {isReleaseArchived(release) && (
  370. <ReleaseArchivedNotice
  371. onRestore={() => this.handleRestore(project, refetchData)}
  372. />
  373. )}
  374. <Feature features={['release-comparison']}>
  375. {({hasFeature}) =>
  376. hasFeature ? (
  377. <Fragment>
  378. <StyledPageTimeRangeSelector
  379. organization={organization}
  380. relative={period ?? ''}
  381. start={start ?? null}
  382. end={end ?? null}
  383. utc={utc ?? null}
  384. onUpdate={this.handleDateChange}
  385. relativeOptions={{
  386. [RELEASE_PERIOD_KEY]: (
  387. <Fragment>
  388. {t('Entire Release Period')} (
  389. <DateTime
  390. date={releaseBounds.releaseStart}
  391. timeAndDate
  392. />{' '}
  393. -{' '}
  394. <DateTime
  395. date={releaseBounds.releaseEnd}
  396. timeAndDate
  397. />
  398. )
  399. </Fragment>
  400. ),
  401. ...DEFAULT_RELATIVE_PERIODS,
  402. }}
  403. defaultPeriod={RELEASE_PERIOD_KEY}
  404. />
  405. {(hasDiscover || hasPerformance || hasHealthData) && (
  406. <ReleaseComparisonChart
  407. release={release}
  408. releaseSessions={thisRelease}
  409. allSessions={allReleases}
  410. platform={project.platform}
  411. location={location}
  412. loading={loading}
  413. reloading={reloading}
  414. errored={errored}
  415. project={project}
  416. organization={organization}
  417. api={api}
  418. hasHealthData={hasHealthData}
  419. />
  420. )}
  421. </Fragment>
  422. ) : (
  423. (hasDiscover || hasPerformance || hasHealthData) && (
  424. <ReleaseChart
  425. releaseMeta={releaseMeta}
  426. selection={selection}
  427. yAxis={yAxis}
  428. onYAxisChange={display =>
  429. this.handleYAxisChange(display, project)
  430. }
  431. eventType={eventType}
  432. onEventTypeChange={type =>
  433. this.handleEventTypeChange(type, project)
  434. }
  435. vitalType={vitalType}
  436. onVitalTypeChange={type =>
  437. this.handleVitalTypeChange(type, project)
  438. }
  439. router={router}
  440. organization={organization}
  441. hasHealthData={hasHealthData}
  442. location={location}
  443. api={api}
  444. version={version}
  445. hasDiscover={hasDiscover}
  446. hasPerformance={hasPerformance}
  447. platform={project.platform}
  448. defaultStatsPeriod={defaultStatsPeriod}
  449. projectSlug={project.slug}
  450. />
  451. )
  452. )
  453. }
  454. </Feature>
  455. <Issues
  456. organization={organization}
  457. selection={selection}
  458. version={version}
  459. location={location}
  460. defaultStatsPeriod={defaultStatsPeriod}
  461. releaseBounds={releaseBounds}
  462. queryFilterDescription={t('In this release')}
  463. withChart
  464. />
  465. <Feature features={['performance-view']}>
  466. <TransactionsList
  467. location={location}
  468. organization={organization}
  469. eventView={releaseEventView}
  470. trendView={releaseTrendView}
  471. selected={selectedSort}
  472. options={sortOptions}
  473. handleDropdownChange={this.handleTransactionsListSortChange}
  474. titles={titles}
  475. generateLink={generateLink}
  476. />
  477. </Feature>
  478. </Main>
  479. <Side>
  480. <ReleaseStats
  481. organization={organization}
  482. release={release}
  483. project={project}
  484. location={location}
  485. selection={selection}
  486. hasHealthData={hasHealthData}
  487. getHealthData={getHealthData}
  488. isHealthLoading={isHealthLoading}
  489. />
  490. <Feature features={['release-comparison']}>
  491. {hasHealthData && (
  492. <ReleaseAdoption
  493. releaseSessions={thisRelease}
  494. allSessions={allReleases}
  495. loading={loading}
  496. reloading={reloading}
  497. errored={errored}
  498. release={release}
  499. project={project}
  500. />
  501. )}
  502. </Feature>
  503. <ProjectReleaseDetails
  504. release={release}
  505. releaseMeta={releaseMeta}
  506. orgSlug={organization.slug}
  507. projectSlug={project.slug}
  508. />
  509. {commitCount > 0 && (
  510. <CommitAuthorBreakdown
  511. version={version}
  512. orgId={organization.slug}
  513. projectSlug={project.slug}
  514. />
  515. )}
  516. {releaseMeta.projects.length > 1 && (
  517. <OtherProjects
  518. projects={releaseMeta.projects.filter(
  519. p => p.slug !== project.slug
  520. )}
  521. location={location}
  522. version={version}
  523. organization={organization}
  524. />
  525. )}
  526. {hasHealthData && (
  527. <TotalCrashFreeUsers
  528. organization={organization}
  529. version={version}
  530. projectSlug={project.slug}
  531. location={location}
  532. />
  533. )}
  534. {deploys.length > 0 && (
  535. <Deploys
  536. version={version}
  537. orgSlug={organization.slug}
  538. deploys={deploys}
  539. projectId={project.id}
  540. />
  541. )}
  542. </Side>
  543. </Body>
  544. )}
  545. </ReleaseDetailsRequest>
  546. );
  547. }}
  548. </ReleaseContext.Consumer>
  549. );
  550. }
  551. }
  552. function generateTransactionLink(
  553. version: string,
  554. projectId: number,
  555. selection: GlobalSelection,
  556. value: string
  557. ) {
  558. return (
  559. organization: Organization,
  560. tableRow: TableDataRow,
  561. _query: Query
  562. ): LocationDescriptor => {
  563. const {transaction} = tableRow;
  564. const trendTransaction = ['regression', 'improved'].includes(value);
  565. const {environments, datetime} = selection;
  566. const {start, end, period} = datetime;
  567. return transactionSummaryRouteWithQuery({
  568. orgSlug: organization.slug,
  569. transaction: transaction! as string,
  570. query: {
  571. query: trendTransaction ? '' : `release:${version}`,
  572. environment: environments,
  573. start: start ? getUtcDateString(start) : undefined,
  574. end: end ? getUtcDateString(end) : undefined,
  575. statsPeriod: period,
  576. },
  577. projectID: projectId.toString(),
  578. display: trendTransaction ? DisplayModes.TREND : DisplayModes.DURATION,
  579. });
  580. };
  581. }
  582. function getDropdownOptions(): DropdownOption[] {
  583. return [
  584. {
  585. sort: {kind: 'desc', field: 'failure_count'},
  586. value: TransactionsListOption.FAILURE_COUNT,
  587. label: t('Failing Transactions'),
  588. },
  589. {
  590. sort: {kind: 'desc', field: 'epm'},
  591. value: TransactionsListOption.TPM,
  592. label: t('Frequent Transactions'),
  593. },
  594. {
  595. sort: {kind: 'desc', field: 'p50'},
  596. value: TransactionsListOption.SLOW,
  597. label: t('Slow Transactions'),
  598. },
  599. {
  600. sort: {kind: 'desc', field: 'p75_measurements_lcp'},
  601. value: TransactionsListOption.SLOW_LCP,
  602. label: t('Slow LCP'),
  603. },
  604. {
  605. sort: {kind: 'desc', field: 'trend_percentage()'},
  606. query: [['t_test()', '<-6']],
  607. trendType: TrendChangeType.REGRESSION,
  608. value: TransactionsListOption.REGRESSION,
  609. label: t('Trending Regressions'),
  610. },
  611. {
  612. sort: {kind: 'asc', field: 'trend_percentage()'},
  613. query: [['t_test()', '>6']],
  614. trendType: TrendChangeType.IMPROVED,
  615. value: TransactionsListOption.IMPROVEMENT,
  616. label: t('Trending Improvements'),
  617. },
  618. ];
  619. }
  620. function getTransactionsListSort(location: Location): {
  621. selectedSort: DropdownOption;
  622. sortOptions: DropdownOption[];
  623. } {
  624. const sortOptions = getDropdownOptions();
  625. const urlParam = decodeScalar(
  626. location.query.showTransactions,
  627. TransactionsListOption.FAILURE_COUNT
  628. );
  629. const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
  630. return {selectedSort, sortOptions};
  631. }
  632. const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
  633. margin-bottom: ${space(1.5)};
  634. `;
  635. export default withApi(withGlobalSelection(withOrganization(ReleaseOverview)));