Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
22254ca
progress
trevor-e Dec 19, 2025
fd9b2a0
:hammer_and_wrench: Sync API Urls to TypeScirpt
getsantry[bot] Dec 19, 2025
cfdc82d
fix
trevor-e Dec 19, 2025
6bdc133
fix
trevor-e Dec 19, 2025
46caebe
fix
trevor-e Dec 19, 2025
42f0e43
fix
trevor-e Dec 19, 2025
c4a9ce4
fix
trevor-e Dec 19, 2025
2362605
Merge branch 'master' into telkins/app-size-endpoint
trevor-e Dec 19, 2025
ebdcb81
fix
trevor-e Dec 19, 2025
ad368a5
fix
trevor-e Dec 19, 2025
f65b9ed
fix
trevor-e Dec 19, 2025
f19387f
fix
trevor-e Dec 19, 2025
fca4692
fix
trevor-e Dec 19, 2025
6ac79f7
params
trevor-e Dec 19, 2025
67387dc
type
trevor-e Dec 19, 2025
067565c
frontend
trevor-e Dec 19, 2025
e0e1576
check null
trevor-e Dec 19, 2025
37112be
progress
trevor-e Dec 20, 2025
16177e9
remove prebuilt dashboard
trevor-e Dec 20, 2025
1dc780c
:hammer_and_wrench: apply pre-commit fixes
getsantry[bot] Dec 20, 2025
7115721
Merge branch 'telkins/app-size-endpoint' into telkins/app-size-dashboard
trevor-e Dec 22, 2025
ee44cd3
progress
trevor-e Dec 22, 2025
3b29a6b
fix
trevor-e Dec 23, 2025
9291caf
Merge branch 'master' into telkins/app-size-dashboard
trevor-e Dec 23, 2025
68c3f68
fix
trevor-e Dec 23, 2025
17993e8
create config
trevor-e Dec 23, 2025
cec10c7
progress
trevor-e Dec 23, 2025
78d335b
fix
trevor-e Dec 23, 2025
e739268
progress
trevor-e Dec 23, 2025
88d5bf7
progress
trevor-e Dec 23, 2025
7c9d13d
fixes
trevor-e Dec 23, 2025
e427c62
fix
trevor-e Dec 23, 2025
fdc7b71
fixes
trevor-e Dec 23, 2025
77417c6
fix
trevor-e Dec 23, 2025
fb4bd05
cleanup
trevor-e Dec 23, 2025
d17425f
clean up params
trevor-e Dec 23, 2025
d4d949d
fix chart labels
trevor-e Dec 29, 2025
70d841a
revert some of the query changes, needed for graph units
trevor-e Dec 29, 2025
351ca3c
fix
trevor-e Dec 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions static/app/utils/discover/charts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {LegendComponentOption} from 'echarts';
import type {Series} from 'sentry/types/echarts';
import {defined} from 'sentry/utils';
import {formatBytesBase2} from 'sentry/utils/bytes/formatBytesBase2';
import {formatBytesBase10} from 'sentry/utils/bytes/formatBytesBase10';
import type {AggregationOutputType, RateUnit} from 'sentry/utils/discover/fields';
import {axisDuration} from 'sentry/utils/duration/axisDuration';
import getDuration from 'sentry/utils/duration/getDuration';
Expand Down Expand Up @@ -46,6 +47,8 @@ export function tooltipFormatterUsingAggregateOutputType(
return getDuration(value / 1000, 2, true);
case 'size':
return formatBytesBase2(value);
case 'size_base10':
return formatBytesBase10(value);
default:
return value.toString();
}
Expand Down Expand Up @@ -94,6 +97,8 @@ export function axisLabelFormatterUsingAggregateOutputType(
return axisDuration(value, durationUnit);
case 'size':
return formatBytesBase2(value, 0);
case 'size_base10':
return formatBytesBase10(value, 0);
case 'rate':
return formatRate(value, rateUnit);
default:
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/discover/fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ export type AggregationOutputType = Extract<
| 'percentage'
| 'string'
| 'size'
| 'size_base10'
| 'rate'
| 'score'
>;
Expand Down
1 change: 1 addition & 0 deletions static/app/utils/fields/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ export enum FieldValueType {
STRING = 'string',
NEVER = 'never',
SIZE = 'size',
SIZE_BASE10 = 'size_base10',
RATE = 'rate',
PERCENT_CHANGE = 'percent_change',
SCORE = 'score',
Expand Down
10 changes: 8 additions & 2 deletions static/app/views/dashboards/datasetConfig/base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {ErrorsConfig} from './errors';
import {ErrorsAndTransactionsConfig} from './errorsAndTransactions';
import {IssuesConfig} from './issues';
import {LogsConfig} from './logs';
import {MobileAppSizeConfig} from './mobileAppSize';
import {ReleasesConfig} from './releases';
import {SpansConfig} from './spans';
import {TraceMetricsConfig} from './traceMetrics';
Expand Down Expand Up @@ -285,7 +286,9 @@ export function getDatasetConfig<T extends WidgetType | undefined>(
? typeof SpansConfig
: T extends WidgetType.TRACEMETRICS
? typeof TraceMetricsConfig
: typeof ErrorsAndTransactionsConfig;
: T extends WidgetType.MOBILE_APP_SIZE
? typeof MobileAppSizeConfig
: typeof ErrorsAndTransactionsConfig;

export function getDatasetConfig(
widgetType?: WidgetType
Expand All @@ -297,7 +300,8 @@ export function getDatasetConfig(
| typeof TransactionsConfig
| typeof LogsConfig
| typeof SpansConfig
| typeof TraceMetricsConfig {
| typeof TraceMetricsConfig
| typeof MobileAppSizeConfig {
switch (widgetType) {
case WidgetType.ISSUE:
return IssuesConfig;
Expand All @@ -313,6 +317,8 @@ export function getDatasetConfig(
return SpansConfig;
case WidgetType.TRACEMETRICS:
return TraceMetricsConfig;
case WidgetType.MOBILE_APP_SIZE:
return MobileAppSizeConfig;
case WidgetType.DISCOVER:
default:
return ErrorsAndTransactionsConfig;
Expand Down
292 changes: 292 additions & 0 deletions static/app/views/dashboards/datasetConfig/mobileAppSize.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,292 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
import {WidgetFixture} from 'sentry-fixture/widget';

import {WidgetType} from 'sentry/views/dashboards/types';

import type {AppSizeResponse} from './mobileAppSize';
import {MobileAppSizeConfig} from './mobileAppSize';

describe('MobileAppSizeConfig', () => {
const organization = OrganizationFixture();

describe('transformSeries', () => {
it('transforms API response to series data', () => {
const data: AppSizeResponse[] = [
{
data: [
[1609459200, [{count: 1000000}]],
[1609545600, [{count: 1100000}]],
[1609632000, [{count: 1200000}]],
],
start: 1609459200,
end: 1609632000,
meta: {fields: {}},
},
];

const widgetQuery = {
conditions: 'app_id=com.example.app',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
};

const result = MobileAppSizeConfig.transformSeries!(
data,
widgetQuery,
organization
);

expect(result).toHaveLength(1);
expect(result[0]!.seriesName).toBe('com.example.app : max(max_install_size)');
Comment thread
cursor[bot] marked this conversation as resolved.
Resolved
expect(result[0]!.data).toHaveLength(3);
expect(result[0]!.data[0]).toEqual({
name: 1609459200000,
value: 1000000,
});
});

it('filters out null and undefined values', () => {
const data: AppSizeResponse[] = [
{
data: [
[1609459200, [{count: 1000000}]],
[1609545600, [{count: null}]],
[1609632000, []],
[1609718400, [{count: 1200000}]],
],
start: 1609459200,
end: 1609718400,
meta: {fields: {}},
},
];

const widgetQuery = {
conditions: '',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
};

const result = MobileAppSizeConfig.transformSeries!(
data,
widgetQuery,
organization
);

expect(result[0]!.data).toHaveLength(2);
expect(result[0]!.data[0]!.value).toBe(1000000);
expect(result[0]!.data[1]!.value).toBe(1200000);
});

it('includes artifact type in series name', () => {
const data: AppSizeResponse[] = [
{
data: [[1609459200, [{count: 1000000}]]],
start: 1609459200,
end: 1609459200,
meta: {fields: {}},
},
];

const widgetQuery = {
conditions: 'app_id=com.example.app&artifact_type=2',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
};

const result = MobileAppSizeConfig.transformSeries!(
data,
widgetQuery,
organization
);

expect(result[0]!.seriesName).toContain('com.example.app');
expect(result[0]!.seriesName).toContain('apk');
expect(result[0]!.seriesName).toContain('max(max_install_size)');
});

it('includes branch and build config in series name', () => {
const data: AppSizeResponse[] = [
{
data: [[1609459200, [{count: 1000000}]]],
start: 1609459200,
end: 1609459200,
meta: {fields: {}},
},
];

const widgetQuery = {
conditions:
'app_id=com.example.app&git_head_ref=main&build_configuration_name=Release',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
};

const result = MobileAppSizeConfig.transformSeries!(
data,
widgetQuery,
organization
);

expect(result[0]!.seriesName).toContain('com.example.app');
expect(result[0]!.seriesName).toContain('main');
expect(result[0]!.seriesName).toContain('Release');
expect(result[0]!.seriesName).toContain('max(max_install_size)');
});

it('uses correct aggregate for download size', () => {
const data: AppSizeResponse[] = [
{
data: [[1609459200, [{count: 500000}]]],
start: 1609459200,
end: 1609459200,
meta: {fields: {}},
},
];

const widgetQuery = {
conditions: 'app_id=com.example.app&size_type=download',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
};

const result = MobileAppSizeConfig.transformSeries!(
data,
widgetQuery,
organization
);

expect(result[0]!.seriesName).toBe('com.example.app : max(max_download_size)');
});
});

describe('getSeriesRequest', () => {
it('uses install size field by default', async () => {
const api = new MockApiClient();
const widget = WidgetFixture({
widgetType: WidgetType.MOBILE_APP_SIZE,
queries: [
{
conditions: 'app_id=com.example.app',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
},
],
});

const mockRequest = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/preprod/app-size-stats/`,
body: {
data: [[1609459200, [{count: 1000000}]]],
start: 1609459200,
end: 1609459200,
meta: {fields: {}},
},
});

await MobileAppSizeConfig.getSeriesRequest!(api, widget, 0, organization, {
datetime: {start: null, end: null, period: '14d', utc: false},
environments: [],
projects: [1],
});

expect(mockRequest).toHaveBeenCalledWith(
`/organizations/${organization.slug}/preprod/app-size-stats/`,
expect.objectContaining({
query: expect.objectContaining({
field: 'max(max_install_size)',
}),
})
);
});

it('uses download size field when size_type=download', async () => {
const api = new MockApiClient();
const widget = WidgetFixture({
widgetType: WidgetType.MOBILE_APP_SIZE,
queries: [
{
conditions: 'app_id=com.example.app&size_type=download',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
},
],
});

const mockRequest = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/preprod/app-size-stats/`,
body: {
data: [[1609459200, [{count: 500000}]]],
start: 1609459200,
end: 1609459200,
meta: {fields: {}},
},
});

await MobileAppSizeConfig.getSeriesRequest!(api, widget, 0, organization, {
datetime: {start: null, end: null, period: '14d', utc: false},
environments: [],
projects: [1],
});

expect(mockRequest).toHaveBeenCalledWith(
`/organizations/${organization.slug}/preprod/app-size-stats/`,
expect.objectContaining({
query: expect.objectContaining({
field: 'max(max_download_size)',
}),
})
);
});
});

describe('getSeriesResultType', () => {
it('returns size_base10 output type for both install and download aggregates', () => {
const data: AppSizeResponse[] = [];
const widgetQuery = {
conditions: '',
aggregates: [],
fields: [],
columns: [],
fieldAliases: [],
name: '',
orderby: '',
};

const result = MobileAppSizeConfig.getSeriesResultType!(data, widgetQuery);

// Both aggregates should be registered to handle multi-query widgets
expect(result).toEqual({
'max(max_install_size)': 'size_base10',
'max(max_download_size)': 'size_base10',
});
});
});
});
Loading
Loading