Skip to content

Commit

Permalink
feat(workflow): Team insights crash free table (#28600)
Browse files Browse the repository at this point in the history
  • Loading branch information
scttcper authored Sep 21, 2021
1 parent 680bd1d commit f2dd7f4
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 1 deletion.
1 change: 1 addition & 0 deletions static/app/types/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,7 @@ export type Project = {
isInternal: boolean;
hasUserReports?: boolean;
hasAccess: boolean;
hasSessions: boolean;
firstEvent: 'string' | null;
firstTransactionEvent: boolean;
subjectTemplate: string;
Expand Down
4 changes: 3 additions & 1 deletion static/app/views/projectDetail/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {Location} from 'history';
import {canIncludePreviousPeriod} from 'app/components/charts/utils';
import {GlobalSelection} from 'app/types';

export function shouldFetchPreviousPeriod(datetime: GlobalSelection['datetime']) {
export function shouldFetchPreviousPeriod(
datetime: Partial<GlobalSelection['datetime']>
) {
const {start, end, period} = datetime;

return !start && !end && canIncludePreviousPeriod(true, period);
Expand Down
11 changes: 11 additions & 0 deletions static/app/views/teamInsights/overview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import withTeamsForUser from 'app/utils/withTeamsForUser';
import HeaderTabs from './headerTabs';
import TeamKeyTransactions from './keyTransactions';
import TeamDropdown from './teamDropdown';
import TeamStability from './teamStability';

type Props = {
api: Client;
Expand Down Expand Up @@ -181,6 +182,16 @@ function TeamInsightsOverview({
/>
</ControlsWrapper>

<SectionTitle>{t('Stability')}</SectionTitle>
<TeamStability
projects={projects}
organization={organization}
period={period}
start={start}
end={end}
utc={utc}
/>

<SectionTitle>{t('Performance')}</SectionTitle>
<TeamKeyTransactions
organization={organization}
Expand Down
219 changes: 219 additions & 0 deletions static/app/views/teamInsights/teamStability.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';
import round from 'lodash/round';

import AsyncComponent from 'app/components/asyncComponent';
import {DateTimeObject} from 'app/components/charts/utils';
import IdBadge from 'app/components/idBadge';
import {getParams} from 'app/components/organizations/globalSelectionHeader/getParams';
import PanelTable from 'app/components/panels/panelTable';
import Placeholder from 'app/components/placeholder';
import {IconArrow} from 'app/icons';
import {t, tct} from 'app/locale';
import space from 'app/styles/space';
import {Organization, Project, SessionApiResponse, SessionField} from 'app/types';
import {getCrashFreeRate} from 'app/utils/sessions';
import {Color} from 'app/utils/theme';
import {displayCrashFreePercent} from 'app/views/releases/utils';

type Props = AsyncComponent['props'] & {
organization: Organization;
projects: Project[];
} & DateTimeObject;

type State = AsyncComponent['state'] & {
/** weekly selected date range */
periodSessions: SessionApiResponse | null;
/** Locked to last 7 days */
weekSessions: SessionApiResponse | null;
};

class TeamStability extends AsyncComponent<Props, State> {
shouldRenderBadRequests = true;

getDefaultState(): State {
return {
...super.getDefaultState(),
weekSessions: null,
periodSessions: null,
};
}

getEndpoints() {
const {organization, start, end, period, utc, projects} = this.props;

const projectsWithSessions = projects.filter(project => project.hasSessions);

if (projectsWithSessions.length === 0) {
return [];
}

const datetime = {start, end, period, utc};
const commonQuery = {
environment: [],
project: projectsWithSessions.map(p => p.id),
field: 'sum(session)',
groupBy: ['session.status', 'project'],
interval: '1d',
};

const endpoints: ReturnType<AsyncComponent['getEndpoints']> = [
[
'periodSessions',
`/organizations/${organization.slug}/sessions/`,
{
query: {
...commonQuery,
...getParams(datetime),
},
},
],
[
'weekSessions',
`/organizations/${organization.slug}/sessions/`,
{
query: {
...commonQuery,
statsPeriod: '7d',
},
},
],
];

return endpoints;
}

componentDidUpdate(prevProps: Props) {
const {start, end, period, utc} = this.props;

if (
prevProps.start !== start ||
prevProps.end !== end ||
prevProps.period !== period ||
prevProps.utc !== utc
) {
this.remountComponent();
}
}

getScore(projectId: number, dataset: 'week' | 'period'): number | null {
const {periodSessions, weekSessions} = this.state;
const sessions = dataset === 'week' ? weekSessions : periodSessions;
const projectGroups = sessions?.groups.filter(
group => group.by.project === projectId
);

return getCrashFreeRate(projectGroups, SessionField.SESSIONS);
}

getTrend(projectId: number): number | null {
const periodScore = this.getScore(projectId, 'period');
const weekScore = this.getScore(projectId, 'week');

if (periodScore === null || weekScore === null) {
return null;
}

return weekScore - periodScore;
}

renderLoading() {
return this.renderBody();
}

renderScore(projectId: string, dataset: 'week' | 'period') {
const {loading} = this.state;

if (loading) {
return (
<div>
<Placeholder width="80px" height="25px" />
</div>
);
}

const score = this.getScore(Number(projectId), dataset);

if (score === null) {
return '\u2014';
}

return displayCrashFreePercent(score);
}

renderTrend(projectId: string) {
const {loading} = this.state;

if (loading) {
return (
<div>
<Placeholder width="80px" height="25px" />
</div>
);
}

const trend = this.getTrend(Number(projectId));

if (trend === null) {
return '\u2014';
}

return (
<SubText color={trend >= 0 ? 'green300' : 'red300'}>
{`${round(trend, 3)}\u0025`}
<PaddedIconArrow direction={trend >= 0 ? 'up' : 'down'} size="xs" />
</SubText>
);
}

renderBody() {
const {projects, period} = this.props;

return (
<PanelTable
headers={[
t('Project'),
tct('Last [period]', {period}),
t('This Week'),
t('Difference'),
]}
>
{projects.map(project => (
<Fragment key={project.id}>
<ProjectBadgeContainer>
<ProjectBadge avatarSize={18} project={project} />
</ProjectBadgeContainer>

<ScoreWrapper>{this.renderScore(project.id, 'period')}</ScoreWrapper>
<ScoreWrapper>{this.renderScore(project.id, 'week')}</ScoreWrapper>
<ScoreWrapper>{this.renderTrend(project.id)}</ScoreWrapper>
</Fragment>
))}
</PanelTable>
);
}
}

export default TeamStability;

const ScoreWrapper = styled('div')`
display: flex;
align-items: center;
`;

const PaddedIconArrow = styled(IconArrow)`
margin: 0 ${space(0.5)};
`;

const SubText = styled('div')<{color: Color}>`
font-size: ${p => p.theme.fontSizeMedium};
color: ${p => p.theme[p.color]};
`;

const ProjectBadgeContainer = styled('div')`
width: 100%;
`;

const ProjectBadge = styled(IdBadge)`
flex-shrink: 0;
`;
46 changes: 46 additions & 0 deletions tests/js/spec/views/teamInsights/teamStability.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {mountWithTheme, waitFor} from 'sentry-test/reactTestingLibrary';

import TeamStability from 'app/views/teamInsights/teamStability';

describe('TeamStability', () => {
it('should comparse selected past crash rate with current week', async () => {
const sessionsApi = MockApiClient.addMockResponse({
url: `/organizations/org-slug/sessions/`,
body: TestStubs.SessionStatusCountByProjectInPeriod(),
});
const project = TestStubs.Project({hasSessions: true, id: 123});
const wrapper = mountWithTheme(
<TeamStability
projects={[project]}
organization={TestStubs.Organization()}
period="7d"
/>
);

await waitFor(() => {
expect(wrapper.queryByTestId('loading-placeholder')).toBeNull();
});

expect(wrapper.getByText('project-slug')).toBeInTheDocument();
expect(wrapper.getAllByText('90%')).toHaveLength(2);
expect(wrapper.getByText('0%')).toBeInTheDocument(2);
expect(sessionsApi).toHaveBeenCalledTimes(2);
});

it('should render no sessions', async () => {
const noSessionProject = TestStubs.Project({hasSessions: false, id: 123});
const wrapper = mountWithTheme(
<TeamStability
projects={[noSessionProject]}
organization={TestStubs.Organization()}
period="7d"
/>
);

await waitFor(() => {
expect(wrapper.queryByTestId('loading-placeholder')).toBeNull();
});

expect(wrapper.getAllByText('\u2014')).toHaveLength(3);
});
});

0 comments on commit f2dd7f4

Please sign in to comment.