Skip to content
19 changes: 18 additions & 1 deletion static/app/components/scoreCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type ScoreCardProps = {
title: React.ReactNode;
className?: string;
help?: React.ReactNode;
isEstimate?: boolean;
isTooltipHoverable?: boolean;
renderOpenButton?: () => React.ReactNode;
score?: React.ReactNode;
Expand All @@ -27,7 +28,11 @@ function ScoreCard({
className,
renderOpenButton,
isTooltipHoverable,
isEstimate,
}: ScoreCardProps) {
const value = score ?? '\u2014';
const displayScore = isEstimate ? `~${value}` : value;

return (
<ScorePanel className={className}>
<HeaderWrapper>
Expand All @@ -46,7 +51,8 @@ function ScoreCard({
</HeaderWrapper>

<ScoreWrapper>
<Score>{score ?? '\u2014'}</Score>
<Score>{displayScore}</Score>
{isEstimate && <Asterisk>*</Asterisk>}
{defined(trend) && (
<Trend trendStatus={trendStatus}>
<TextOverflow>{trend}</TextOverflow>
Expand Down Expand Up @@ -122,4 +128,15 @@ export const Trend = styled('div')<TrendProps>`
overflow: hidden;
`;

const Asterisk = styled('div')`
color: grey;
font-size: ${p => p.theme.fontSizeRelativeSmall};
display: inline-block;
width: 10pt;
height: 10pt;
position: relative;
top: -10px;
margin-left: ${space(0.25)};
`;

export default ScoreCard;
31 changes: 31 additions & 0 deletions static/app/views/organizationStats/index.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ describe('OrganizationStats', function () {
expect(screen.getAllByText('Invalid')[0]).toBeInTheDocument();
expect(screen.getAllByText('15')[0]).toBeInTheDocument();

expect(
screen.queryByText('*This is an estimation, and may not be 100% accurate.')
).not.toBeInTheDocument();

// Correct API Calls
const mockExpectations = {
UsageStatsOrg: {
Expand Down Expand Up @@ -519,6 +523,33 @@ describe('OrganizationStats', function () {
).toBeInTheDocument();
});

it('shows estimation text when profile duration category is selected', async () => {
const newOrg = initializeOrg({
organization: {
features: [
'global-views',
'team-insights',
'continuous-profiling-stats',
'continuous-profiling',
],
},
});

render(
<OrganizationStats
{...defaultProps}
location={{...defaultProps.location, query: {dataCategory: 'profileDuration'}}}
organization={newOrg.organization}
/>,
{
router: newOrg.router,
}
);
expect(
await screen.findByText('*This is an estimation, and may not be 100% accurate.')
).toBeInTheDocument();
});

it('denies access without project membership', async function () {
const newOrg = initializeOrg({
organization: {
Expand Down
47 changes: 44 additions & 3 deletions static/app/views/organizationStats/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import withPageFilters from 'sentry/utils/withPageFilters';
import HeaderTabs from 'sentry/views/organizationStats/header';
import {getPerformanceBaseUrl} from 'sentry/views/performance/utils';
import {makeProjectsPathname} from 'sentry/views/projects/pathname';
import {getDocsLinkForEventType} from 'sentry/views/settings/account/notifications/utils';

import type {ChartDataTransform} from './usageChart';
import {CHART_OPTIONS_DATACATEGORY} from './usageChart';
Expand Down Expand Up @@ -319,6 +320,29 @@ export class OrganizationStats extends Component<OrganizationStatsProps> {
);
}

renderEstimationDisclaimer() {
if (
this.dataCategory === DATA_CATEGORY_INFO.profileDuration.plural ||
this.dataCategory === DATA_CATEGORY_INFO.profileDurationUI.plural
) {
return (
<EstimationText data-test-id="estimation-text">
{tct(
'*This is an estimation, and may not be 100% accurate. [estimateLink: How we calculate estimated usage]',
{
estimateLink: (
<ExternalLink
href={getDocsLinkForEventType(DataCategoryExact.PROFILE_DURATION)} // TODO(continuous profiling): update link when docs are ready
/>
),
}
)}
</EstimationText>
);
}
return null;
}

render() {
const {organization} = this.props;
const hasTeamInsights = organization.features.includes('team-insights');
Expand Down Expand Up @@ -349,7 +373,10 @@ export class OrganizationStats extends Component<OrganizationStatsProps> {
<Body>
<Layout.Main fullWidth>
<HookHeader organization={organization} />
{this.renderProjectPageControl()}
<ControlsWrapper>
{this.renderProjectPageControl()}
{this.renderEstimationDisclaimer()}
</ControlsWrapper>
<div>
<ErrorBoundary mini>{this.renderUsageStatsOrg()}</ErrorBoundary>
</div>
Expand Down Expand Up @@ -425,12 +452,26 @@ const HeadingSubtitle = styled('p')`
margin-bottom: 0;
`;

const ControlsWrapper = styled('div')`
display: flex;
align-items: center;
gap: ${space(0.5)};
margin-bottom: ${space(2)};
justify-content: space-between;
`;

const PageControl = styled('div')`
display: grid;
width: 100%;
margin-bottom: ${space(2)};

margin-bottom: 0;
grid-template-columns: minmax(0, max-content);
@media (max-width: ${p => p.theme.breakpoints.small}) {
grid-template-columns: minmax(0, 1fr);
}
`;

const EstimationText = styled('div')`
color: ${p => p.theme.subText};
font-size: ${p => p.theme.fontSizeSmall};
line-height: ${p => p.theme.text.lineHeightBody};
`;
67 changes: 67 additions & 0 deletions static/app/views/organizationStats/mapSeriesToChart.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,71 @@ describe('mapSeriesToChart func', function () {
// sums up rate limited, abuse, and cardinality limited
expect(mappedSeries.cardStats.rateLimited).toBe('11');
});

it('should correctly sum up the profile chunks', function () {
const mappedSeries = mapSeriesToChart({
orgStats: {
start: '2021-01-01T00:00:00Z',
end: '2021-01-07T00:00:00Z',
intervals: ['2021-01-01T00:00:00Z', '2021-01-02T00:00:00Z'],
groups: [
{
by: {
outcome: 'invalid',
reason: 'bad',
category: 'profile_chunk',
},
totals: {
'sum(quantity)': 10,
},
series: {
'sum(quantity)': [1, 2],
},
},
{
by: {
outcome: 'accepted',
reason: 'good',
category: 'profile_chunk',
},
totals: {
'sum(quantity)': 10,
},
series: {
'sum(quantity)': [3, 4],
},
},
{
by: {
outcome: 'accepted',
reason: 'good',
category: 'profile_duration',
},
totals: {
'sum(quantity)': 10,
},
series: {
'sum(quantity)': [1, 2],
},
},
],
},
chartDateInterval: '1h',
chartDateUtc: true,
dataCategory: 'profile_duration',
endpointQuery: {},
});

// multiplies dropped profile chunks by 9000
expect(mappedSeries.chartStats.invalid).toEqual([
{value: ['Jan 1 12:00 AM - 1:00 AM (+00:00)', 9000]},
{value: ['Jan 2 12:00 AM - 1:00 AM (+00:00)', 18000]},
]);

// does not add accepted profile chunks to accepted profile duration
expect(mappedSeries.chartStats.accepted).toEqual([
{value: ['Jan 1 12:00 AM - 1:00 AM (+00:00)', 1]},
{value: ['Jan 2 12:00 AM - 1:00 AM (+00:00)', 2]},
]);
});
});
22 changes: 20 additions & 2 deletions static/app/views/organizationStats/mapSeriesToChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,20 @@ import type {ChartStats} from './usageChart';
import {SeriesTypes} from './usageChart';
import {formatUsageWithUnits, getFormatUsageOptions} from './utils';

// used for estimated dropped continuous profile hours and ui profile hours from profile chunks and profile chunks ui
export function droppedProfileChunkMultiplier(
category: number | string | undefined,
outcome: number | string | undefined
) {
if (category === 'profile_chunk' || category === 'profile_chunk_ui') {
if (outcome === Outcome.ACCEPTED) {
return 0;
}
return 9000;
}
return 1;
}

export function mapSeriesToChart({
orgStats,
dataCategory,
Expand Down Expand Up @@ -101,10 +115,13 @@ export function mapSeriesToChart({
countAcceptedStored += group.totals['sum(quantity)']!;
}
} else {
const value =
group.totals['sum(quantity)']! *
droppedProfileChunkMultiplier(category, outcome);
if (outcome !== Outcome.CLIENT_DISCARD) {
count.total += group.totals['sum(quantity)']!;
count.total += value;
}
(count as any)[outcome!] += group.totals['sum(quantity)']!;
(count as any)[outcome!] += value;
}

if (category === 'span_indexed' && outcome !== Outcome.ACCEPTED) {
Expand All @@ -113,6 +130,7 @@ export function mapSeriesToChart({
}

group.series['sum(quantity)']!.forEach((stat, i) => {
stat = stat * droppedProfileChunkMultiplier(category, outcome);
const dataObject = {name: orgStats.intervals[i]!, value: stat};

const strigfiedReason = String(group.by.reason ?? '');
Expand Down
13 changes: 12 additions & 1 deletion static/app/views/organizationStats/usageStatsOrg.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import UsageChart, {
SeriesTypes,
} from './usageChart';
import UsageStatsPerMin from './usageStatsPerMin';
import {isDisplayUtc} from './utils';
import {isContinuousProfiling, isDisplayUtc} from './utils';

export interface UsageStatsOrganizationProps extends WithRouterProps {
dataCategory: DataCategoryInfo['plural'];
Expand Down Expand Up @@ -150,6 +150,12 @@ class UsageStatsOrganization<
groupBy.push('category');
category.push('span_indexed');
}
if (['profile_duration', 'profile_duration_ui'].includes(dataCategoryApiName)) {
groupBy.push('category');
category.push(
dataCategoryApiName === 'profile_duration' ? 'profile_chunk' : 'profile_chunk_ui'
);
}

return {
...queryDatetime,
Expand Down Expand Up @@ -370,6 +376,7 @@ class UsageStatsOrganization<
const {total, accepted, accepted_stored, invalid, rateLimited, filtered} =
this.chartData.cardStats;
const dataCategoryNameLower = dataCategoryName.toLowerCase();
const shouldShowEstimate = isContinuousProfiling(dataCategory);

const navigateToInboundFilterSettings = (event: ReactMouseEvent) => {
event.preventDefault();
Expand Down Expand Up @@ -429,6 +436,7 @@ class UsageStatsOrganization<
}
),
score: filtered,
isEstimate: shouldShowEstimate,
},
rateLimited: {
title: tct('Rate Limited [dataCategory]', {dataCategory: dataCategoryName}),
Expand All @@ -445,6 +453,7 @@ class UsageStatsOrganization<
}
),
score: rateLimited,
isEstimate: shouldShowEstimate,
},
invalid: {
title: tct('Invalid [dataCategory]', {dataCategory: dataCategoryName}),
Expand All @@ -461,6 +470,7 @@ class UsageStatsOrganization<
}
),
score: invalid,
isEstimate: shouldShowEstimate,
},
};
return cardMetadata;
Expand All @@ -478,6 +488,7 @@ class UsageStatsOrganization<
score={loading ? undefined : card.score}
help={card.help}
trend={card.trend}
isEstimate={card.isEstimate}
isTooltipHoverable
/>
));
Expand Down
Loading
Loading