diff --git a/static/app/constants/index.spec.tsx b/static/app/constants/index.spec.tsx new file mode 100644 index 00000000000000..be368d35d58e73 --- /dev/null +++ b/static/app/constants/index.spec.tsx @@ -0,0 +1,102 @@ +import {DATA_CATEGORY_INFO} from 'sentry/constants'; +import {DataCategoryExact} from 'sentry/types/core'; + +describe('DATA_CATEGORY_INFO', () => { + describe('formatting property', () => { + it('all categories have formatting info', () => { + const categories = Object.values(DataCategoryExact); + for (const category of categories) { + expect(DATA_CATEGORY_INFO[category]).toBeDefined(); + expect(DATA_CATEGORY_INFO[category].formatting).toBeDefined(); + } + }); + + it('byte categories have correct formatting', () => { + const byteCategories = [DataCategoryExact.ATTACHMENT, DataCategoryExact.LOG_BYTE]; + + for (const category of byteCategories) { + const {formatting} = DATA_CATEGORY_INFO[category]; + expect(formatting.unitType).toBe('bytes'); + expect(formatting.reservedMultiplier).toBe(10 ** 9); // GIGABYTE + expect(formatting.bigNumUnit).toBe(1); + expect(formatting.priceFormatting.minIntegerDigits).toBe(2); + expect(formatting.priceFormatting.maxIntegerDigits).toBe(2); + } + }); + + it('attachment category uses non-abbreviated projected format', () => { + const {formatting} = DATA_CATEGORY_INFO[DataCategoryExact.ATTACHMENT]; + expect(formatting.projectedAbbreviated).toBe(false); + }); + + it('log byte category uses abbreviated projected format', () => { + const {formatting} = DATA_CATEGORY_INFO[DataCategoryExact.LOG_BYTE]; + expect(formatting.projectedAbbreviated).toBe(true); + }); + + it('duration hour categories have correct formatting', () => { + const durationCategories = [ + DataCategoryExact.PROFILE_DURATION, + DataCategoryExact.PROFILE_DURATION_UI, + ]; + + for (const category of durationCategories) { + const {formatting} = DATA_CATEGORY_INFO[category]; + expect(formatting.unitType).toBe('durationHours'); + expect(formatting.reservedMultiplier).toBe(3_600_000); // MILLISECONDS_IN_HOUR + expect(formatting.bigNumUnit).toBe(0); + expect(formatting.priceFormatting.minIntegerDigits).toBe(5); + expect(formatting.priceFormatting.maxIntegerDigits).toBe(7); + expect(formatting.projectedAbbreviated).toBe(true); + } + }); + + it('count categories have correct formatting', () => { + const countCategories = [ + DataCategoryExact.ERROR, + DataCategoryExact.TRANSACTION, + DataCategoryExact.REPLAY, + DataCategoryExact.SPAN, + DataCategoryExact.MONITOR_SEAT, + ]; + + for (const category of countCategories) { + const {formatting} = DATA_CATEGORY_INFO[category]; + expect(formatting.unitType).toBe('count'); + expect(formatting.reservedMultiplier).toBe(1); + expect(formatting.bigNumUnit).toBe(0); + expect(formatting.priceFormatting.minIntegerDigits).toBe(5); + expect(formatting.priceFormatting.maxIntegerDigits).toBe(7); + expect(formatting.projectedAbbreviated).toBe(true); + } + }); + + it('formatting unitType matches expected categories', () => { + const bytesCategories = [DataCategoryExact.ATTACHMENT, DataCategoryExact.LOG_BYTE]; + const durationHoursCategories = [ + DataCategoryExact.PROFILE_DURATION, + DataCategoryExact.PROFILE_DURATION_UI, + ]; + + // Check bytes categories + for (const category of bytesCategories) { + expect(DATA_CATEGORY_INFO[category].formatting.unitType).toBe('bytes'); + } + + // Check duration categories + for (const category of durationHoursCategories) { + expect(DATA_CATEGORY_INFO[category].formatting.unitType).toBe('durationHours'); + } + + // All other categories should be count + const allCategories = Object.values(DataCategoryExact); + const nonCountCategories = [...bytesCategories, ...durationHoursCategories]; + + for (const category of allCategories) { + if (!nonCountCategories.includes(category)) { + expect(DATA_CATEGORY_INFO[category].formatting.unitType).toBe('count'); + } + } + }); + }); +}); diff --git a/static/app/constants/index.tsx b/static/app/constants/index.tsx index 04287987748ed4..016f7c3af38834 100644 --- a/static/app/constants/index.tsx +++ b/static/app/constants/index.tsx @@ -251,6 +251,43 @@ const DEFAULT_STATS_INFO = { }; const GIGABYTE = 10 ** 9; const KILOBYTE = 10 ** 3; +const MILLISECONDS_IN_HOUR = 3_600_000; + +/** + * Default formatting configuration for count-based categories. + * Most categories use this configuration. + */ +const DEFAULT_COUNT_FORMATTING = { + unitType: 'count' as const, + reservedMultiplier: 1, + bigNumUnit: 0 as const, + priceFormatting: {minIntegerDigits: 5, maxIntegerDigits: 7}, + projectedAbbreviated: true, +}; + +/** + * Formatting configuration for byte-based categories (attachments, logs). + * Reserved values are in GB, raw values are in bytes. + */ +const BYTES_FORMATTING = { + unitType: 'bytes' as const, + reservedMultiplier: GIGABYTE, + bigNumUnit: 1 as const, + priceFormatting: {minIntegerDigits: 2, maxIntegerDigits: 2}, + projectedAbbreviated: true, +}; + +/** + * Formatting configuration for duration-based categories (continuous profiling). + * Reserved values are in hours, raw values are in milliseconds. + */ +const DURATION_HOURS_FORMATTING = { + unitType: 'durationHours' as const, + reservedMultiplier: MILLISECONDS_IN_HOUR, + bigNumUnit: 0 as const, + priceFormatting: {minIntegerDigits: 5, maxIntegerDigits: 7}, + projectedAbbreviated: true, +}; // https://github.com/getsentry/relay/blob/master/relay-base-schema/src/data_category.rs export const DATA_CATEGORY_INFO = { @@ -268,6 +305,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.TRANSACTION]: { name: DataCategoryExact.TRANSACTION, @@ -283,6 +321,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.ATTACHMENT]: { name: DataCategoryExact.ATTACHMENT, @@ -299,6 +338,7 @@ export const DATA_CATEGORY_INFO = { showExternalStats: true, yAxisMinInterval: 0.5 * GIGABYTE, }, + formatting: {...BYTES_FORMATTING, projectedAbbreviated: false}, }, [DataCategoryExact.PROFILE]: { name: DataCategoryExact.PROFILE, @@ -314,6 +354,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.PROFILE_INDEXED]: { name: DataCategoryExact.PROFILE_INDEXED, @@ -325,6 +366,7 @@ export const DATA_CATEGORY_INFO = { uid: 11, isBilledCategory: false, statsInfo: DEFAULT_STATS_INFO, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.REPLAY]: { name: DataCategoryExact.REPLAY, @@ -340,6 +382,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.USER_REPORT_V2]: { name: DataCategoryExact.USER_REPORT_V2, @@ -355,6 +398,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.TRANSACTION_PROCESSED]: { name: DataCategoryExact.TRANSACTION_PROCESSED, @@ -369,6 +413,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showInternalStats: false, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.TRANSACTION_INDEXED]: { name: DataCategoryExact.TRANSACTION_INDEXED, @@ -380,6 +425,7 @@ export const DATA_CATEGORY_INFO = { uid: 9, isBilledCategory: false, statsInfo: DEFAULT_STATS_INFO, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.MONITOR]: { name: DataCategoryExact.MONITOR, @@ -394,6 +440,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.SPAN]: { name: DataCategoryExact.SPAN, @@ -409,6 +456,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.MONITOR_SEAT]: { name: DataCategoryExact.MONITOR_SEAT, @@ -424,6 +472,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showInternalStats: false, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.SPAN_INDEXED]: { name: DataCategoryExact.SPAN_INDEXED, @@ -436,6 +485,7 @@ export const DATA_CATEGORY_INFO = { isBilledCategory: false, docsUrl: 'https://docs.sentry.io/product/performance/', statsInfo: DEFAULT_STATS_INFO, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.PROFILE_DURATION]: { name: DataCategoryExact.PROFILE_DURATION, @@ -452,6 +502,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DURATION_HOURS_FORMATTING, }, [DataCategoryExact.PROFILE_CHUNK]: { name: DataCategoryExact.PROFILE_CHUNK, @@ -463,6 +514,7 @@ export const DATA_CATEGORY_INFO = { uid: 18, isBilledCategory: false, statsInfo: DEFAULT_STATS_INFO, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.PROFILE_DURATION_UI]: { name: DataCategoryExact.PROFILE_DURATION_UI, @@ -479,6 +531,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DURATION_HOURS_FORMATTING, }, [DataCategoryExact.PROFILE_CHUNK_UI]: { name: DataCategoryExact.PROFILE_CHUNK_UI, @@ -490,6 +543,7 @@ export const DATA_CATEGORY_INFO = { uid: 26, isBilledCategory: false, statsInfo: DEFAULT_STATS_INFO, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.UPTIME]: { @@ -506,6 +560,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showInternalStats: false, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.LOG_ITEM]: { name: DataCategoryExact.LOG_ITEM, @@ -520,6 +575,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.LOG_BYTE]: { name: DataCategoryExact.LOG_BYTE, @@ -536,6 +592,7 @@ export const DATA_CATEGORY_INFO = { showExternalStats: true, yAxisMinInterval: 1 * KILOBYTE, }, + formatting: BYTES_FORMATTING, }, [DataCategoryExact.SEER_AUTOFIX]: { name: DataCategoryExact.SEER_AUTOFIX, @@ -550,6 +607,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.SEER_SCANNER]: { name: DataCategoryExact.SEER_SCANNER, @@ -564,6 +622,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.PREVENT_USER]: { name: DataCategoryExact.PREVENT_USER, @@ -578,6 +637,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: false, // TODO(prevent): add external stats when ready }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.PREVENT_REVIEW]: { name: DataCategoryExact.PREVENT_REVIEW, @@ -592,6 +652,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: false, // TODO(prevent): add external stats when ready }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.TRACE_METRIC]: { name: DataCategoryExact.TRACE_METRIC, @@ -606,6 +667,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: true, }, + formatting: DEFAULT_COUNT_FORMATTING, }, [DataCategoryExact.SEER_USER]: { name: DataCategoryExact.SEER_USER, @@ -620,6 +682,7 @@ export const DATA_CATEGORY_INFO = { ...DEFAULT_STATS_INFO, showExternalStats: false, // TODO(seer): add external stats when ready }, + formatting: DEFAULT_COUNT_FORMATTING, }, } as const satisfies Record; diff --git a/static/app/types/core.tsx b/static/app/types/core.tsx index 999c4ebc56fd68..c5f8cfd73cd835 100644 --- a/static/app/types/core.tsx +++ b/static/app/types/core.tsx @@ -134,8 +134,56 @@ export enum DataCategoryExact { TRACE_METRIC = 'trace_metric', } +/** + * Unit type for data category formatting. + * - 'bytes': Categories measured in bytes (e.g., attachments, logs) + * - 'durationHours': Categories measured in hours (e.g., continuous profiling) + * - 'count': Categories measured as simple counts (e.g., errors, transactions) + */ +export type DataCategoryUnitType = 'bytes' | 'durationHours' | 'count'; + +/** + * Formatting configuration for data categories. + * This centralizes category-specific formatting logic that was previously + * scattered across helper functions like isByteCategory() and isContinuousProfiling(). + */ +export interface DataCategoryFormattingInfo { + /** + * BigNum unit type for formatting large numbers. + * 0 = numbers (standard numeric formatting) + * 1 = kiloBytes (byte-based formatting with KB/MB/GB suffixes) + */ + bigNumUnit: 0 | 1; + /** + * Formatting options for price display. + * minIntegerDigits: minimum integer digits (bytes use 2, counts use 5) + * maxIntegerDigits: maximum integer digits (bytes use 2, counts use 7) + */ + priceFormatting: { + maxIntegerDigits: number; + minIntegerDigits: number; + }; + /** + * Whether to use abbreviated formatting for projected values. + * Most categories use true, but ATTACHMENTS uses false for full precision. + */ + projectedAbbreviated: boolean; + /** + * Multiplier to convert reserved/prepaid units to raw values. + * - bytes: GIGABYTE (10^9) - reserved is in GB, raw is in bytes + * - durationHours: MILLISECONDS_IN_HOUR (3,600,000) - reserved is in hours, raw is in ms + * - count: 1 - no conversion needed + */ + reservedMultiplier: number; + /** + * The unit type for this category, determining how values are formatted and displayed. + */ + unitType: DataCategoryUnitType; +} + export interface DataCategoryInfo { displayName: string; + formatting: DataCategoryFormattingInfo; isBilledCategory: boolean; name: DataCategoryExact; plural: DataCategory;