Files
2024-02-15 17:01:50 +00:00

761 lines
27 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {Fragment} from 'react';
import type {RouteComponentProps} from 'react-router';
import {browserHistory} from 'react-router';
import styled from '@emotion/styled';
import type {Location, LocationDescriptor, Query} from 'history';
import moment from 'moment';
import {restoreRelease} from 'sentry/actionCreators/release';
import {Client} from 'sentry/api';
import Feature from 'sentry/components/acl/feature';
import SessionsRequest from 'sentry/components/charts/sessionsRequest';
import type {DateTimeObject} from 'sentry/components/charts/utils';
import DateTime from 'sentry/components/dateTime';
import PerformanceCardTable from 'sentry/components/discover/performanceCardTable';
import type {DropdownOption} from 'sentry/components/discover/transactionsList';
import TransactionsList from 'sentry/components/discover/transactionsList';
import * as Layout from 'sentry/components/layouts/thirds';
import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
import type {ChangeData} from 'sentry/components/timeRangeSelector';
import {TimeRangeSelector} from 'sentry/components/timeRangeSelector';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {NewQuery, Organization, PageFilters, ReleaseProject} from 'sentry/types';
import {SessionFieldWithOperation} from 'sentry/types';
import {getUtcDateString} from 'sentry/utils/dates';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
import EventView from 'sentry/utils/discover/eventView';
import {MobileVital, SpanOpBreakdown, WebVital} from 'sentry/utils/fields';
import {formatVersion} from 'sentry/utils/formatters';
import {decodeScalar} from 'sentry/utils/queryString';
import routeTitleGen from 'sentry/utils/routeTitle';
import withApi from 'sentry/utils/withApi';
import withOrganization from 'sentry/utils/withOrganization';
import withPageFilters from 'sentry/utils/withPageFilters';
import DeprecatedAsyncView from 'sentry/views/deprecatedAsyncView';
import {
DisplayModes,
transactionSummaryRouteWithQuery,
} from 'sentry/views/performance/transactionSummary/utils';
import type {TrendView} from 'sentry/views/performance/trends/types';
import {TrendChangeType} from 'sentry/views/performance/trends/types';
import {
platformToPerformanceType,
ProjectPerformanceType,
} from 'sentry/views/performance/utils';
import type {ReleaseBounds} from '../../utils';
import {getReleaseParams, isReleaseArchived, searchReleaseVersion} from '../../utils';
import {ReleaseContext} from '..';
import CommitAuthorBreakdown from './sidebar/commitAuthorBreakdown';
import Deploys from './sidebar/deploys';
import OtherProjects from './sidebar/otherProjects';
import ProjectReleaseDetails from './sidebar/projectReleaseDetails';
import ReleaseAdoption from './sidebar/releaseAdoption';
import ReleaseStats from './sidebar/releaseStats';
import ThresholdStatuses from './sidebar/thresholdStatuses';
import TotalCrashFreeUsers from './sidebar/totalCrashFreeUsers';
import ReleaseArchivedNotice from './releaseArchivedNotice';
import ReleaseComparisonChart from './releaseComparisonChart';
import ReleaseIssues from './releaseIssues';
const RELEASE_PERIOD_KEY = 'release';
export enum TransactionsListOption {
FAILURE_COUNT = 'failure_count',
TPM = 'tpm',
SLOW = 'slow',
SLOW_LCP = 'slow_lcp',
REGRESSION = 'regression',
IMPROVEMENT = 'improved',
}
type RouteParams = {
orgId: string;
release: string;
};
type Props = RouteComponentProps<RouteParams, {}> & {
api: Client;
organization: Organization;
selection: PageFilters;
};
class ReleaseOverview extends DeprecatedAsyncView<Props> {
getTitle() {
const {params, organization} = this.props;
return routeTitleGen(
t('Release %s', formatVersion(params.release)),
organization.slug,
false
);
}
handleRestore = async (project: ReleaseProject, successCallback: () => void) => {
const {params, organization} = this.props;
try {
await restoreRelease(new Client(), {
orgSlug: organization.slug,
projectSlug: project.slug,
releaseVersion: params.release,
});
successCallback();
} catch {
// do nothing, action creator is already displaying error message
}
};
getReleaseEventView(
version: string,
projectId: number,
selectedSort: DropdownOption,
releaseBounds: ReleaseBounds
): EventView {
const {selection, location} = this.props;
const {environments} = selection;
const {start, end, statsPeriod} = getReleaseParams({
location,
releaseBounds,
});
const baseQuery: NewQuery = {
id: undefined,
version: 2,
name: `Release ${formatVersion(version)}`,
query: `event.type:transaction ${searchReleaseVersion(version)}`,
fields: ['transaction', 'failure_count()', 'epm()', 'p50()'],
orderby: '-failure_count',
range: statsPeriod || undefined,
environment: environments,
projects: [projectId],
start: start ? getUtcDateString(start) : undefined,
end: end ? getUtcDateString(end) : undefined,
};
switch (selectedSort.value) {
case TransactionsListOption.SLOW_LCP:
return EventView.fromSavedQuery({
...baseQuery,
query: `event.type:transaction release:${version} epm():>0.01 has:measurements.lcp`,
fields: ['transaction', 'failure_count()', 'epm()', 'p75(measurements.lcp)'],
orderby: 'p75_measurements_lcp',
});
case TransactionsListOption.SLOW:
return EventView.fromSavedQuery({
...baseQuery,
query: `event.type:transaction release:${version} epm():>0.01`,
});
case TransactionsListOption.FAILURE_COUNT:
return EventView.fromSavedQuery({
...baseQuery,
query: `event.type:transaction release:${version} failure_count():>0`,
});
default:
return EventView.fromSavedQuery(baseQuery);
}
}
getReleaseTrendView(
version: string,
projectId: number,
versionDate: string,
releaseBounds: ReleaseBounds
): EventView {
const {selection, location} = this.props;
const {environments} = selection;
const {start, end, statsPeriod} = getReleaseParams({
location,
releaseBounds,
});
const trendView = EventView.fromSavedQuery({
id: undefined,
version: 2,
name: `Release ${formatVersion(version)}`,
fields: ['transaction'],
query: 'tpm():>0.01 trend_percentage():>0%',
range: statsPeriod || undefined,
environment: environments,
projects: [projectId],
start: start ? getUtcDateString(start) : undefined,
end: end ? getUtcDateString(end) : undefined,
}) as TrendView;
trendView.middle = versionDate;
return trendView;
}
getReleasePerformanceEventView(
performanceType: string,
baseQuery: NewQuery
): EventView {
const eventView =
performanceType === ProjectPerformanceType.FRONTEND
? (EventView.fromSavedQuery({
...baseQuery,
fields: [
...baseQuery.fields,
`p75(${WebVital.FCP})`,
`p75(${WebVital.FID})`,
`p75(${WebVital.LCP})`,
`p75(${WebVital.CLS})`,
`p75(${SpanOpBreakdown.SPANS_HTTP})`,
`p75(${SpanOpBreakdown.SPANS_BROWSER})`,
`p75(${SpanOpBreakdown.SPANS_RESOURCE})`,
],
}) as EventView)
: performanceType === ProjectPerformanceType.BACKEND
? (EventView.fromSavedQuery({
...baseQuery,
fields: [
...baseQuery.fields,
'apdex()',
'p75(spans.http)',
'p75(spans.db)',
],
}) as EventView)
: performanceType === ProjectPerformanceType.MOBILE
? (EventView.fromSavedQuery({
...baseQuery,
fields: [
...baseQuery.fields,
`p75(${MobileVital.APP_START_COLD})`,
`p75(${MobileVital.APP_START_WARM})`,
`p75(${MobileVital.FRAMES_SLOW})`,
`p75(${MobileVital.FRAMES_FROZEN})`,
],
}) as EventView)
: (EventView.fromSavedQuery({
...baseQuery,
}) as EventView);
return eventView;
}
getAllReleasesPerformanceView(
projectId: number,
performanceType: string,
releaseBounds: ReleaseBounds
) {
const {selection, location} = this.props;
const {environments} = selection;
const {start, end, statsPeriod} = getReleaseParams({
location,
releaseBounds,
});
const baseQuery: NewQuery = {
id: undefined,
version: 2,
name: 'All Releases',
query: 'event.type:transaction',
fields: ['user_misery()'],
range: statsPeriod || undefined,
environment: environments,
projects: [projectId],
start: start ? getUtcDateString(start) : undefined,
end: end ? getUtcDateString(end) : undefined,
};
return this.getReleasePerformanceEventView(performanceType, baseQuery);
}
getReleasePerformanceView(
version: string,
projectId: number,
performanceType: string,
releaseBounds: ReleaseBounds
) {
const {selection, location} = this.props;
const {environments} = selection;
const {start, end, statsPeriod} = getReleaseParams({
location,
releaseBounds,
});
const baseQuery: NewQuery = {
id: undefined,
version: 2,
name: `Release:${version}`,
query: `event.type:transaction release:${version}`,
fields: ['user_misery()'],
range: statsPeriod || undefined,
environment: environments,
projects: [projectId],
start: start ? getUtcDateString(start) : undefined,
end: end ? getUtcDateString(end) : undefined,
};
return this.getReleasePerformanceEventView(performanceType, baseQuery);
}
get pageDateTime(): DateTimeObject {
const query = this.props.location.query;
const {start, end, statsPeriod} = normalizeDateTimeParams(query, {
allowEmptyPeriod: true,
allowAbsoluteDatetime: true,
allowAbsolutePageDatetime: true,
});
if (statsPeriod) {
return {period: statsPeriod};
}
if (start && end) {
return {
start: moment.utc(start).format(),
end: moment.utc(end).format(),
};
}
return {};
}
handleTransactionsListSortChange = (value: string) => {
const {location} = this.props;
const target = {
pathname: location.pathname,
query: {...location.query, showTransactions: value, transactionCursor: undefined},
};
browserHistory.push(target);
};
handleDateChange = (datetime: ChangeData) => {
const {router, location} = this.props;
const {start, end, relative, utc} = datetime;
if (start && end) {
const parser = utc ? moment.utc : moment;
router.push({
...location,
query: {
...location.query,
pageStatsPeriod: undefined,
pageStart: parser(start).format(),
pageEnd: parser(end).format(),
pageUtc: utc ?? undefined,
},
});
return;
}
router.push({
...location,
query: {
...location.query,
pageStatsPeriod: relative === RELEASE_PERIOD_KEY ? undefined : relative,
pageStart: undefined,
pageEnd: undefined,
pageUtc: undefined,
},
});
};
render() {
const {organization, selection, location, api} = this.props;
const {start, end, period, utc} = this.pageDateTime;
const hasV2ReleaseUIEnabled =
organization.features.includes('releases-v2-internal') ||
organization.features.includes('releases-v2') ||
organization.features.includes('releases-v2-st');
return (
<ReleaseContext.Consumer>
{({
release,
project,
deploys,
releaseMeta,
refetchData,
hasHealthData,
releaseBounds,
}) => {
const {commitCount, version} = release;
const hasDiscover = organization.features.includes('discover-basic');
const hasPerformance = organization.features.includes('performance-view');
const hasReleaseComparisonPerformance = organization.features.includes(
'release-comparison-performance'
);
const {environments} = selection;
const performanceType = platformToPerformanceType([project], [project.id]);
const {selectedSort, sortOptions} = getTransactionsListSort(location);
const releaseEventView = this.getReleaseEventView(
version,
project.id,
selectedSort,
releaseBounds
);
const titles =
selectedSort.value !== TransactionsListOption.SLOW_LCP
? [t('transaction'), t('failure_count()'), t('tpm()'), t('p50()')]
: [t('transaction'), t('failure_count()'), t('tpm()'), t('p75(lcp)')];
const releaseTrendView = this.getReleaseTrendView(
version,
project.id,
releaseMeta.released,
releaseBounds
);
const allReleasesPerformanceView = this.getAllReleasesPerformanceView(
project.id,
performanceType,
releaseBounds
);
const releasePerformanceView = this.getReleasePerformanceView(
version,
project.id,
performanceType,
releaseBounds
);
const generateLink = {
transaction: generateTransactionLink(
version,
project.id,
selection,
location.query.showTransactions
),
};
const sessionsRequestProps: Omit<SessionsRequest['props'], 'children'> = {
api,
organization,
field: [
SessionFieldWithOperation.USERS,
SessionFieldWithOperation.SESSIONS,
SessionFieldWithOperation.DURATION,
],
groupBy: ['session.status'],
...getReleaseParams({location, releaseBounds}),
shouldFilterSessionsInTimeWindow: true,
};
const defaultDateTimeSelected = !period && !start && !end;
const releaseBoundsLabel =
releaseBounds.type === 'clamped'
? t('Clamped Release Period')
: t('Entire Release Period');
return (
<SessionsRequest {...sessionsRequestProps}>
{({
loading: allReleasesLoading,
reloading: allReleasesReloading,
errored: allReleasesErrored,
response: allReleases,
}) => (
<SessionsRequest
{...sessionsRequestProps}
query={searchReleaseVersion(version)}
>
{({
loading: thisReleaseLoading,
reloading: thisReleaseReloading,
errored: thisReleaseErrored,
response: thisRelease,
}) => {
const loading = allReleasesLoading || thisReleaseLoading;
const reloading = allReleasesReloading || thisReleaseReloading;
const errored = allReleasesErrored || thisReleaseErrored;
return (
<Layout.Body>
<Layout.Main>
{isReleaseArchived(release) && (
<ReleaseArchivedNotice
onRestore={() => this.handleRestore(project, refetchData)}
/>
)}
<ReleaseDetailsPageFilters>
<EnvironmentPageFilter />
<TimeRangeSelector
relative={
period ??
(defaultDateTimeSelected ? RELEASE_PERIOD_KEY : null)
}
start={start ?? null}
end={end ?? null}
utc={utc ?? null}
onChange={this.handleDateChange}
menuTitle={t('Filter Time Range')}
triggerLabel={
defaultDateTimeSelected ? releaseBoundsLabel : null
}
relativeOptions={({defaultOptions, arbitraryOptions}) =>
releaseBounds.type !== 'ancient'
? {
[RELEASE_PERIOD_KEY]: (
<Fragment>
{releaseBoundsLabel}
<br />
<ReleaseBoundsDescription
primary={defaultDateTimeSelected}
>
<DateTime date={releaseBounds.releaseStart} />
–<DateTime date={releaseBounds.releaseEnd} />
</ReleaseBoundsDescription>
</Fragment>
),
...defaultOptions,
...arbitraryOptions,
}
: {...defaultOptions, ...arbitraryOptions}
}
defaultPeriod={
releaseBounds.type !== 'ancient'
? RELEASE_PERIOD_KEY
: '90d'
}
defaultAbsolute={{
start: moment(releaseBounds.releaseStart)
.subtract(1, 'hour')
.toDate(),
end: releaseBounds.releaseEnd
? moment(releaseBounds.releaseEnd)
.add(1, 'hour')
.toDate()
: undefined,
}}
/>
</ReleaseDetailsPageFilters>
{(hasDiscover || hasPerformance || hasHealthData) && (
<ReleaseComparisonChart
release={release}
releaseSessions={thisRelease}
allSessions={allReleases}
platform={project.platform}
location={location}
loading={loading}
reloading={reloading}
errored={errored}
project={project}
organization={organization}
api={api}
hasHealthData={hasHealthData}
/>
)}
<ReleaseIssues
organization={organization}
version={version}
location={location}
releaseBounds={releaseBounds}
queryFilterDescription={t('In this release')}
withChart
/>
<Feature features="performance-view">
{hasReleaseComparisonPerformance ? (
<PerformanceCardTable
organization={organization}
project={project}
location={location}
allReleasesEventView={allReleasesPerformanceView}
releaseEventView={releasePerformanceView}
performanceType={performanceType}
/>
) : (
<TransactionsList
location={location}
organization={organization}
eventView={releaseEventView}
trendView={releaseTrendView}
selected={selectedSort}
options={sortOptions}
handleDropdownChange={
this.handleTransactionsListSortChange
}
titles={titles}
generateLink={generateLink}
supportsInvestigationRule={false}
/>
)}
</Feature>
</Layout.Main>
<Layout.Side>
<ReleaseStats
organization={organization}
release={release}
project={project}
/>
{hasV2ReleaseUIEnabled && (
<ThresholdStatuses
project={project}
release={release}
organization={organization}
selectedEnvs={selection.environments}
/>
)}
{hasHealthData && (
<ReleaseAdoption
releaseSessions={thisRelease}
allSessions={allReleases}
loading={loading}
reloading={reloading}
errored={errored}
release={release}
project={project}
environment={environments}
/>
)}
<ProjectReleaseDetails
release={release}
releaseMeta={releaseMeta}
projectSlug={project.slug}
/>
{commitCount > 0 && (
<CommitAuthorBreakdown
version={version}
orgId={organization.slug}
projectSlug={project.slug}
/>
)}
{releaseMeta.projects.length > 1 && (
<OtherProjects
projects={releaseMeta.projects.filter(
p => p.slug !== project.slug
)}
location={location}
version={version}
organization={organization}
/>
)}
{hasHealthData && (
<TotalCrashFreeUsers
organization={organization}
version={version}
projectSlug={project.slug}
location={location}
/>
)}
{deploys.length > 0 && (
<Deploys
version={version}
orgSlug={organization.slug}
deploys={deploys}
projectId={project.id}
/>
)}
</Layout.Side>
</Layout.Body>
);
}}
</SessionsRequest>
)}
</SessionsRequest>
);
}}
</ReleaseContext.Consumer>
);
}
}
function generateTransactionLink(
version: string,
projectId: number,
selection: PageFilters,
value: string
) {
return (
organization: Organization,
tableRow: TableDataRow,
_query: Query
): LocationDescriptor => {
const {transaction} = tableRow;
const trendTransaction = ['regression', 'improved'].includes(value);
const {environments, datetime} = selection;
const {start, end, period} = datetime;
return transactionSummaryRouteWithQuery({
orgSlug: organization.slug,
transaction: transaction! as string,
query: {
query: trendTransaction ? '' : `release:${version}`,
environment: environments,
start: start ? getUtcDateString(start) : undefined,
end: end ? getUtcDateString(end) : undefined,
statsPeriod: period,
},
projectID: projectId.toString(),
display: trendTransaction ? DisplayModes.TREND : DisplayModes.DURATION,
});
};
}
function getDropdownOptions(): DropdownOption[] {
return [
{
sort: {kind: 'desc', field: 'failure_count'},
value: TransactionsListOption.FAILURE_COUNT,
label: t('Failing Transactions'),
},
{
sort: {kind: 'desc', field: 'epm'},
value: TransactionsListOption.TPM,
label: t('Frequent Transactions'),
},
{
sort: {kind: 'desc', field: 'p50'},
value: TransactionsListOption.SLOW,
label: t('Slow Transactions'),
},
{
sort: {kind: 'desc', field: 'p75_measurements_lcp'},
value: TransactionsListOption.SLOW_LCP,
label: t('Slow LCP'),
},
{
sort: {kind: 'desc', field: 'trend_percentage()'},
query: [['confidence()', '>6']],
trendType: TrendChangeType.REGRESSION,
value: TransactionsListOption.REGRESSION,
label: t('Trending Regressions'),
},
{
sort: {kind: 'asc', field: 'trend_percentage()'},
query: [['confidence()', '>6']],
trendType: TrendChangeType.IMPROVED,
value: TransactionsListOption.IMPROVEMENT,
label: t('Trending Improvements'),
},
];
}
function getTransactionsListSort(location: Location): {
selectedSort: DropdownOption;
sortOptions: DropdownOption[];
} {
const sortOptions = getDropdownOptions();
const urlParam = decodeScalar(
location.query.showTransactions,
TransactionsListOption.FAILURE_COUNT
);
const selectedSort = sortOptions.find(opt => opt.value === urlParam) || sortOptions[0];
return {selectedSort, sortOptions};
}
const ReleaseDetailsPageFilters = styled('div')`
display: grid;
grid-template-columns: minmax(0, max-content) 1fr;
gap: ${space(2)};
margin-bottom: ${space(2)};
@media (max-width: ${p => p.theme.breakpoints.small}) {
grid-template-columns: auto;
}
`;
const ReleaseBoundsDescription = styled('span')<{primary: boolean}>`
font-size: ${p => p.theme.fontSizeSmall};
color: ${p => (p.primary ? p.theme.activeText : p.theme.subText)};
`;
ReleaseOverview.contextType = ReleaseContext;
export default withApi(withPageFilters(withOrganization(ReleaseOverview)));