diff --git a/static/app/components/events/contexts/knownContext/art.spec.tsx b/static/app/components/events/contexts/knownContext/art.spec.tsx new file mode 100644 index 000000000000..78f041feae6a --- /dev/null +++ b/static/app/components/events/contexts/knownContext/art.spec.tsx @@ -0,0 +1,70 @@ +import {EventFixture} from 'sentry-fixture/event'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ContextCard} from 'sentry/components/events/contexts/contextCard'; +import { + getARTContextData, + type ARTContext, +} from 'sentry/components/events/contexts/knownContext/art'; + +const MOCK_ART_CONTEXT: ARTContext = { + 'gc.blocking_count': 5, + 'gc.pre_oome_count': 9, + 'gc.total_time': 600, + 'gc.waiting_time': 200, + 'memory.free': 2048, + 'memory.free_until_gc': 4096, + 'memory.max': 536870912, + 'memory.total': 268435456, + // Extra data is still valid and preserved + extra_data: 'something', + unknown_key: 123, +}; + +const MOCK_REDACTION = { + 'gc.total_time': { + '': { + rem: [['organization:0', 's', 0, 0]], + len: 5, + }, + }, +}; + +describe('ARTContext', () => { + it('returns formatted values', () => { + expect(getARTContextData({data: MOCK_ART_CONTEXT})).toEqual([ + {key: 'gc.blocking_count', subject: 'GC Blocking Count', value: 5}, + {key: 'gc.pre_oome_count', subject: 'GC Pre-OOME Count', value: 9}, + {key: 'gc.total_time', subject: 'GC Total Time', value: '600.00ms'}, + {key: 'gc.waiting_time', subject: 'GC Waiting Time', value: '200.00ms'}, + {key: 'memory.free', subject: 'Memory Free', value: '2.0 KiB'}, + {key: 'memory.free_until_gc', subject: 'Memory Free Until GC', value: '4.0 KiB'}, + {key: 'memory.max', subject: 'Memory Max', value: '512.0 MiB'}, + {key: 'memory.total', subject: 'Memory Total', value: '256.0 MiB'}, + {key: 'extra_data', subject: 'extra_data', value: 'something'}, + {key: 'unknown_key', subject: 'unknown_key', value: 123}, + ]); + }); + + it('renders with meta annotations correctly', () => { + const event = EventFixture({ + _meta: {contexts: {art: MOCK_REDACTION}}, + }); + + render( + + ); + + expect(screen.getByText('Android Runtime')).toBeInTheDocument(); + expect(screen.getByText('GC Blocking Count')).toBeInTheDocument(); + expect(screen.getByText('Memory Free')).toBeInTheDocument(); + expect(screen.getByText('2.0 KiB')).toBeInTheDocument(); + expect(screen.getByText('GC Total Time')).toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/contexts/knownContext/art.tsx b/static/app/components/events/contexts/knownContext/art.tsx new file mode 100644 index 000000000000..41b8887ab838 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/art.tsx @@ -0,0 +1,112 @@ +import {getContextKeys} from 'sentry/components/events/contexts/utils'; +import {t} from 'sentry/locale'; +import type {KeyValueListData} from 'sentry/types/group'; +import {formatBytesBase2} from 'sentry/utils/bytes/formatBytesBase2'; +import {getDuration} from 'sentry/utils/duration/getDuration'; + +enum ARTContextKeys { + GC_BLOCKING_COUNT = 'gc.blocking_count', + GC_PRE_OOME_COUNT = 'gc.pre_oome_count', + GC_TOTAL_TIME = 'gc.total_time', + GC_WAITING_TIME = 'gc.waiting_time', + MEMORY_FREE = 'memory.free', + MEMORY_FREE_UNTIL_GC = 'memory.free_until_gc', + MEMORY_MAX = 'memory.max', + MEMORY_TOTAL = 'memory.total', +} + +export interface ARTContext { + [key: string]: any; + [ARTContextKeys.GC_BLOCKING_COUNT]?: number; + [ARTContextKeys.GC_PRE_OOME_COUNT]?: number; + [ARTContextKeys.GC_TOTAL_TIME]?: number; + [ARTContextKeys.GC_WAITING_TIME]?: number; + [ARTContextKeys.MEMORY_FREE]?: number; + [ARTContextKeys.MEMORY_FREE_UNTIL_GC]?: number; + [ARTContextKeys.MEMORY_MAX]?: number; + [ARTContextKeys.MEMORY_TOTAL]?: number; +} + +function formatMilliseconds(ms: number): string { + return getDuration(ms / 1000, 2, true); +} + +export function getARTContextData({ + data, + meta, +}: { + data: ARTContext; + meta?: Record; +}): KeyValueListData { + return getContextKeys({data}).map(ctxKey => { + switch (ctxKey) { + case ARTContextKeys.GC_BLOCKING_COUNT: + return { + key: ctxKey, + subject: t('GC Blocking Count'), + value: data[ARTContextKeys.GC_BLOCKING_COUNT], + }; + case ARTContextKeys.GC_PRE_OOME_COUNT: + return { + key: ctxKey, + subject: t('GC Pre-OOME Count'), + value: data[ARTContextKeys.GC_PRE_OOME_COUNT], + }; + case ARTContextKeys.GC_TOTAL_TIME: + return { + key: ctxKey, + subject: t('GC Total Time'), + value: data[ARTContextKeys.GC_TOTAL_TIME] + ? formatMilliseconds(data[ARTContextKeys.GC_TOTAL_TIME]) + : data[ARTContextKeys.GC_TOTAL_TIME], + }; + case ARTContextKeys.GC_WAITING_TIME: + return { + key: ctxKey, + subject: t('GC Waiting Time'), + value: data[ARTContextKeys.GC_WAITING_TIME] + ? formatMilliseconds(data[ARTContextKeys.GC_WAITING_TIME]) + : data[ARTContextKeys.GC_WAITING_TIME], + }; + case ARTContextKeys.MEMORY_FREE: + return { + key: ctxKey, + subject: t('Memory Free'), + value: data[ARTContextKeys.MEMORY_FREE] + ? formatBytesBase2(data[ARTContextKeys.MEMORY_FREE]) + : data[ARTContextKeys.MEMORY_FREE], + }; + case ARTContextKeys.MEMORY_FREE_UNTIL_GC: + return { + key: ctxKey, + subject: t('Memory Free Until GC'), + value: data[ARTContextKeys.MEMORY_FREE_UNTIL_GC] + ? formatBytesBase2(data[ARTContextKeys.MEMORY_FREE_UNTIL_GC]) + : data[ARTContextKeys.MEMORY_FREE_UNTIL_GC], + }; + case ARTContextKeys.MEMORY_MAX: + return { + key: ctxKey, + subject: t('Memory Max'), + value: data[ARTContextKeys.MEMORY_MAX] + ? formatBytesBase2(data[ARTContextKeys.MEMORY_MAX]) + : data[ARTContextKeys.MEMORY_MAX], + }; + case ARTContextKeys.MEMORY_TOTAL: + return { + key: ctxKey, + subject: t('Memory Total'), + value: data[ARTContextKeys.MEMORY_TOTAL] + ? formatBytesBase2(data[ARTContextKeys.MEMORY_TOTAL]) + : data[ARTContextKeys.MEMORY_TOTAL], + }; + default: + return { + key: ctxKey, + subject: ctxKey, + value: data[ctxKey], + meta: meta?.[ctxKey]?.[''], + }; + } + }); +} diff --git a/static/app/components/events/contexts/utils.tsx b/static/app/components/events/contexts/utils.tsx index d57ba74602a8..13e63ab882ff 100644 --- a/static/app/components/events/contexts/utils.tsx +++ b/static/app/components/events/contexts/utils.tsx @@ -13,6 +13,7 @@ import { type ContextIconProps, } from 'sentry/components/events/contexts/contextIcon'; import {getAppContextData} from 'sentry/components/events/contexts/knownContext/app'; +import {getARTContextData} from 'sentry/components/events/contexts/knownContext/art'; import {getBrowserContextData} from 'sentry/components/events/contexts/knownContext/browser'; import {getCloudResourceContextData} from 'sentry/components/events/contexts/knownContext/cloudResource'; import {getCultureContextData} from 'sentry/components/events/contexts/knownContext/culture'; @@ -246,6 +247,8 @@ export function getContextTitle({ switch (contextType) { case 'app': return t('App'); + case 'art': + return t('Android Runtime'); case 'device': return t('Device'); case 'browser': @@ -390,6 +393,8 @@ export function getFormattedContextData({ switch (contextType) { case 'app': return getAppContextData({data: contextValue, event, meta}); + case 'art': + return getARTContextData({data: contextValue, meta}); case 'device': return getDeviceContextData({data: contextValue, event, meta}); case 'memory_info': // Current