index.tsx 23 KB

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