From c3e2154e3fb59f1faab07754eb8ece8c0f2ce972 Mon Sep 17 00:00:00 2001 From: Markus Hintersteiner Date: Wed, 27 May 2026 08:54:42 +0200 Subject: [PATCH] feat(issues): Extend event context formatters for mobile SDKs Add missing keys to existing app and device context formatters, and create new formatters for accessibility, React Native, Flutter, and Dart contexts that were previously handled by the generic fallback. Co-Authored-By: Claude Opus 4.6 --- .../knownContext/accessibility.spec.tsx | 79 +++++++++++++++++ .../contexts/knownContext/accessibility.tsx | 78 +++++++++++++++++ .../events/contexts/knownContext/app.spec.tsx | 16 ++++ .../events/contexts/knownContext/app.tsx | 16 ++++ .../knownContext/dartContext.spec.tsx | 76 ++++++++++++++++ .../contexts/knownContext/dartContext.tsx | 62 +++++++++++++ .../contexts/knownContext/device.spec.tsx | 15 +++- .../events/contexts/knownContext/device.tsx | 42 +++++++++ .../knownContext/flutterContext.spec.tsx | 68 +++++++++++++++ .../contexts/knownContext/flutterContext.tsx | 46 ++++++++++ .../knownContext/reactNativeContext.spec.tsx | 86 +++++++++++++++++++ .../knownContext/reactNativeContext.tsx | 86 +++++++++++++++++++ .../app/components/events/contexts/utils.tsx | 18 ++++ static/app/types/event.tsx | 17 +++- 14 files changed, 701 insertions(+), 4 deletions(-) create mode 100644 static/app/components/events/contexts/knownContext/accessibility.spec.tsx create mode 100644 static/app/components/events/contexts/knownContext/accessibility.tsx create mode 100644 static/app/components/events/contexts/knownContext/dartContext.spec.tsx create mode 100644 static/app/components/events/contexts/knownContext/dartContext.tsx create mode 100644 static/app/components/events/contexts/knownContext/flutterContext.spec.tsx create mode 100644 static/app/components/events/contexts/knownContext/flutterContext.tsx create mode 100644 static/app/components/events/contexts/knownContext/reactNativeContext.spec.tsx create mode 100644 static/app/components/events/contexts/knownContext/reactNativeContext.tsx diff --git a/static/app/components/events/contexts/knownContext/accessibility.spec.tsx b/static/app/components/events/contexts/knownContext/accessibility.spec.tsx new file mode 100644 index 00000000000000..49e9835858f7b2 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/accessibility.spec.tsx @@ -0,0 +1,79 @@ +import {EventFixture} from 'sentry-fixture/event'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ContextCard} from 'sentry/components/events/contexts/contextCard'; +import { + getAccessibilityContextData, + type AccessibilityContext, +} from 'sentry/components/events/contexts/knownContext/accessibility'; + +const MOCK_ACCESSIBILITY_CONTEXT: AccessibilityContext = { + accessible_navigation: false, + bold_text: false, + disable_animations: true, + high_contrast: false, + invert_colors: false, + reduce_motion: false, + // Extra data is still valid and preserved + extra_data: 'something', + unknown_key: 123, +}; + +const MOCK_REDACTION = { + reduce_motion: { + '': { + rem: [['organization:0', 's', 0, 0]], + len: 5, + }, + }, +}; + +describe('AccessibilityContext', () => { + it('returns values according to the parameters', () => { + expect(getAccessibilityContextData({data: MOCK_ACCESSIBILITY_CONTEXT})).toEqual([ + { + key: 'accessible_navigation', + subject: 'Accessible Navigation', + value: false, + }, + {key: 'bold_text', subject: 'Bold Text', value: false}, + {key: 'disable_animations', subject: 'Disable Animations', value: true}, + {key: 'high_contrast', subject: 'High Contrast', value: false}, + {key: 'invert_colors', subject: 'Invert Colors', value: false}, + {key: 'reduce_motion', subject: 'Reduce Motion', value: false}, + { + key: 'extra_data', + subject: 'extra_data', + value: 'something', + meta: undefined, + }, + { + key: 'unknown_key', + subject: 'unknown_key', + value: 123, + meta: undefined, + }, + ]); + }); + + it('renders with meta annotations correctly', () => { + const event = EventFixture({ + _meta: {contexts: {accessibility: MOCK_REDACTION}}, + }); + + render( + + ); + + expect(screen.getByText('Accessibility')).toBeInTheDocument(); + expect(screen.getByText('Disable Animations')).toBeInTheDocument(); + expect(screen.getByText('Reduce Motion')).toBeInTheDocument(); + expect(screen.getByText(/redacted/)).toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/contexts/knownContext/accessibility.tsx b/static/app/components/events/contexts/knownContext/accessibility.tsx new file mode 100644 index 00000000000000..f3c8819e667111 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/accessibility.tsx @@ -0,0 +1,78 @@ +import {getContextKeys} from 'sentry/components/events/contexts/utils'; +import {t} from 'sentry/locale'; +import type {KeyValueListData} from 'sentry/types/group'; + +enum AccessibilityContextKeys { + ACCESSIBLE_NAVIGATION = 'accessible_navigation', + BOLD_TEXT = 'bold_text', + DISABLE_ANIMATIONS = 'disable_animations', + HIGH_CONTRAST = 'high_contrast', + INVERT_COLORS = 'invert_colors', + REDUCE_MOTION = 'reduce_motion', +} + +export interface AccessibilityContext { + [key: string]: any; + [AccessibilityContextKeys.ACCESSIBLE_NAVIGATION]?: boolean; + [AccessibilityContextKeys.BOLD_TEXT]?: boolean; + [AccessibilityContextKeys.DISABLE_ANIMATIONS]?: boolean; + [AccessibilityContextKeys.HIGH_CONTRAST]?: boolean; + [AccessibilityContextKeys.INVERT_COLORS]?: boolean; + [AccessibilityContextKeys.REDUCE_MOTION]?: boolean; +} + +export function getAccessibilityContextData({ + data, + meta, +}: { + data: AccessibilityContext; + meta?: Record; +}): KeyValueListData { + return getContextKeys({data}).map(ctxKey => { + switch (ctxKey) { + case AccessibilityContextKeys.ACCESSIBLE_NAVIGATION: + return { + key: ctxKey, + subject: t('Accessible Navigation'), + value: data.accessible_navigation, + }; + case AccessibilityContextKeys.BOLD_TEXT: + return { + key: ctxKey, + subject: t('Bold Text'), + value: data.bold_text, + }; + case AccessibilityContextKeys.DISABLE_ANIMATIONS: + return { + key: ctxKey, + subject: t('Disable Animations'), + value: data.disable_animations, + }; + case AccessibilityContextKeys.HIGH_CONTRAST: + return { + key: ctxKey, + subject: t('High Contrast'), + value: data.high_contrast, + }; + case AccessibilityContextKeys.INVERT_COLORS: + return { + key: ctxKey, + subject: t('Invert Colors'), + value: data.invert_colors, + }; + case AccessibilityContextKeys.REDUCE_MOTION: + return { + key: ctxKey, + subject: t('Reduce Motion'), + value: data.reduce_motion, + }; + default: + return { + key: ctxKey, + subject: ctxKey, + value: data[ctxKey], + meta: meta?.[ctxKey]?.[''], + }; + } + }); +} diff --git a/static/app/components/events/contexts/knownContext/app.spec.tsx b/static/app/components/events/contexts/knownContext/app.spec.tsx index 8860ba7849daaa..3b4aa44b4e3e31 100644 --- a/static/app/components/events/contexts/knownContext/app.spec.tsx +++ b/static/app/components/events/contexts/knownContext/app.spec.tsx @@ -21,6 +21,12 @@ const MOCK_APP_CONTEXT: AppContext = { is_active: false, app_memory: 1048576 * 12, view_names: ['app.view1', 'app.view2'], + is_split_apks: false, + permissions: { + ACCESS_NETWORK_STATE: 'granted', + CAMERA: 'not_granted', + INTERNET: 'granted', + }, // Extra data is still valid and preserved extra_data: 'something', unknown_key: 123, @@ -73,6 +79,16 @@ describe('AppContext', () => { subject: 'View Names', value: ['app.view1', 'app.view2'], }, + {key: 'is_split_apks', subject: 'Split APKs', value: false}, + { + key: 'permissions', + subject: 'Permissions', + value: { + ACCESS_NETWORK_STATE: 'granted', + CAMERA: 'not_granted', + INTERNET: 'granted', + }, + }, { key: 'extra_data', subject: 'extra_data', diff --git a/static/app/components/events/contexts/knownContext/app.tsx b/static/app/components/events/contexts/knownContext/app.tsx index 4f851e599c24c3..73bdb5a23fcf5e 100644 --- a/static/app/components/events/contexts/knownContext/app.tsx +++ b/static/app/components/events/contexts/knownContext/app.tsx @@ -24,6 +24,8 @@ enum AppContextKeys { // XXX: From https://github.com/getsentry/sentry/issues/87238, not in the schema yet. FREE_MEMORY = 'free_memory', ARCHITECTURE = 'app_arch', + IS_SPLIT_APKS = 'is_split_apks', + PERMISSIONS = 'permissions', } export interface AppContext { @@ -43,6 +45,8 @@ export interface AppContext { [AppContextKeys.VIEW_NAMES]?: string[]; [AppContextKeys.FREE_MEMORY]?: number; [AppContextKeys.ARCHITECTURE]?: string; + [AppContextKeys.IS_SPLIT_APKS]?: boolean; + [AppContextKeys.PERMISSIONS]?: Record; } // https://github.com/getsentry/relay/blob/24.10.0/relay-event-schema/src/protocol/contexts/app.rs#L37 @@ -151,6 +155,18 @@ export function getAppContextData({ subject: t('Architecture'), value: data.app_arch, }; + case AppContextKeys.IS_SPLIT_APKS: + return { + key: ctxKey, + subject: t('Split APKs'), + value: data.is_split_apks, + }; + case AppContextKeys.PERMISSIONS: + return { + key: ctxKey, + subject: t('Permissions'), + value: data.permissions, + }; default: return { key: ctxKey, diff --git a/static/app/components/events/contexts/knownContext/dartContext.spec.tsx b/static/app/components/events/contexts/knownContext/dartContext.spec.tsx new file mode 100644 index 00000000000000..e142d04fe042b2 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/dartContext.spec.tsx @@ -0,0 +1,76 @@ +import {EventFixture} from 'sentry-fixture/event'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ContextCard} from 'sentry/components/events/contexts/contextCard'; +import { + getDartContextData, + type DartContext, +} from 'sentry/components/events/contexts/knownContext/dartContext'; + +const MOCK_DART_CONTEXT: DartContext = { + compile_mode: 'debug', + executable: 'flutter', + resolved_executable: '/system/bin/app_process64', + script: 'file:///main.dart', + // Extra data is still valid and preserved + extra_data: 'something', + unknown_key: 123, +}; + +const MOCK_REDACTION = { + script: { + '': { + rem: [['organization:0', 's', 0, 0]], + len: 20, + }, + }, +}; + +describe('DartContext', () => { + it('returns values according to the parameters', () => { + expect(getDartContextData({data: MOCK_DART_CONTEXT})).toEqual([ + {key: 'compile_mode', subject: 'Compile Mode', value: 'debug'}, + {key: 'executable', subject: 'Executable', value: 'flutter'}, + { + key: 'resolved_executable', + subject: 'Resolved Executable', + value: '/system/bin/app_process64', + }, + {key: 'script', subject: 'Script', value: 'file:///main.dart'}, + { + key: 'extra_data', + subject: 'extra_data', + value: 'something', + meta: undefined, + }, + { + key: 'unknown_key', + subject: 'unknown_key', + value: 123, + meta: undefined, + }, + ]); + }); + + it('renders with meta annotations correctly', () => { + const event = EventFixture({ + _meta: {contexts: {dart_context: MOCK_REDACTION}}, + }); + + render( + + ); + + expect(screen.getByText('Dart')).toBeInTheDocument(); + expect(screen.getByText('Compile Mode')).toBeInTheDocument(); + expect(screen.getByText('debug')).toBeInTheDocument(); + expect(screen.getByText('Script')).toBeInTheDocument(); + expect(screen.getByText(/redacted/)).toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/contexts/knownContext/dartContext.tsx b/static/app/components/events/contexts/knownContext/dartContext.tsx new file mode 100644 index 00000000000000..fc80c0f4220a00 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/dartContext.tsx @@ -0,0 +1,62 @@ +import {getContextKeys} from 'sentry/components/events/contexts/utils'; +import {t} from 'sentry/locale'; +import type {KeyValueListData} from 'sentry/types/group'; + +enum DartContextKeys { + COMPILE_MODE = 'compile_mode', + EXECUTABLE = 'executable', + RESOLVED_EXECUTABLE = 'resolved_executable', + SCRIPT = 'script', +} + +export interface DartContext { + [key: string]: any; + [DartContextKeys.COMPILE_MODE]?: string; + [DartContextKeys.EXECUTABLE]?: string; + [DartContextKeys.RESOLVED_EXECUTABLE]?: string; + [DartContextKeys.SCRIPT]?: string; +} + +export function getDartContextData({ + data, + meta, +}: { + data: DartContext; + meta?: Record; +}): KeyValueListData { + return getContextKeys({data}).map(ctxKey => { + switch (ctxKey) { + case DartContextKeys.COMPILE_MODE: + return { + key: ctxKey, + subject: t('Compile Mode'), + value: data.compile_mode, + }; + case DartContextKeys.EXECUTABLE: + return { + key: ctxKey, + subject: t('Executable'), + value: data.executable, + }; + case DartContextKeys.RESOLVED_EXECUTABLE: + return { + key: ctxKey, + subject: t('Resolved Executable'), + value: data.resolved_executable, + }; + case DartContextKeys.SCRIPT: + return { + key: ctxKey, + subject: t('Script'), + value: data.script, + }; + default: + return { + key: ctxKey, + subject: ctxKey, + value: data[ctxKey], + meta: meta?.[ctxKey]?.[''], + }; + } + }); +} diff --git a/static/app/components/events/contexts/knownContext/device.spec.tsx b/static/app/components/events/contexts/knownContext/device.spec.tsx index fb85bb2da43698..86a38d2e002993 100644 --- a/static/app/components/events/contexts/knownContext/device.spec.tsx +++ b/static/app/components/events/contexts/knownContext/device.spec.tsx @@ -35,6 +35,12 @@ const MOCK_DEVICE_CONTEXT: DeviceContext = { manufacturer: 'Google', free_storage: 508784640, model: 'Android SDK built for x86', + locale: 'en_US', + archs: ['arm64-v8a'], + chipset: 'AOSP ranchu', + connection_type: 'cellular', + low_power_mode: false, + thermal_state: 'nominal', }; const MOCK_REDACTION = { @@ -74,9 +80,8 @@ describe('DeviceContext', () => { }, { key: 'timezone', - subject: 'timezone', + subject: 'Timezone', value: 'America/Los_Angeles', - meta: undefined, }, { key: 'external_storage_size', @@ -132,6 +137,12 @@ describe('DeviceContext', () => { subject: 'Model', value: expect.anything(), }, + {key: 'locale', subject: 'Locale', value: 'en_US'}, + {key: 'archs', subject: 'Architectures', value: ['arm64-v8a']}, + {key: 'chipset', subject: 'Chipset', value: 'AOSP ranchu'}, + {key: 'connection_type', subject: 'Connection Type', value: 'cellular'}, + {key: 'low_power_mode', subject: 'Low Power Mode', value: false}, + {key: 'thermal_state', subject: 'Thermal State', value: 'nominal'}, ]); }); diff --git a/static/app/components/events/contexts/knownContext/device.tsx b/static/app/components/events/contexts/knownContext/device.tsx index cd367bd05e7330..ac6dd0b9069c62 100644 --- a/static/app/components/events/contexts/knownContext/device.tsx +++ b/static/app/components/events/contexts/knownContext/device.tsx @@ -421,6 +421,48 @@ export function getDeviceContextData({ subject: t('Supports Vibration'), value: data.supports_vibration, }; + case DeviceContextKey.TIMEZONE: + return { + key: ctxKey, + subject: t('Timezone'), + value: data.timezone, + }; + case DeviceContextKey.LOCALE: + return { + key: ctxKey, + subject: t('Locale'), + value: data.locale, + }; + case DeviceContextKey.ARCHS: + return { + key: ctxKey, + subject: t('Architectures'), + value: data.archs, + }; + case DeviceContextKey.CHIPSET: + return { + key: ctxKey, + subject: t('Chipset'), + value: data.chipset, + }; + case DeviceContextKey.CONNECTION_TYPE: + return { + key: ctxKey, + subject: t('Connection Type'), + value: data.connection_type, + }; + case DeviceContextKey.LOW_POWER_MODE: + return { + key: ctxKey, + subject: t('Low Power Mode'), + value: data.low_power_mode, + }; + case DeviceContextKey.THERMAL_STATE: + return { + key: ctxKey, + subject: t('Thermal State'), + value: data.thermal_state, + }; default: return { key: ctxKey, diff --git a/static/app/components/events/contexts/knownContext/flutterContext.spec.tsx b/static/app/components/events/contexts/knownContext/flutterContext.spec.tsx new file mode 100644 index 00000000000000..0e159c8da474b8 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/flutterContext.spec.tsx @@ -0,0 +1,68 @@ +import {EventFixture} from 'sentry-fixture/event'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ContextCard} from 'sentry/components/events/contexts/contextCard'; +import { + getFlutterContextData, + type FlutterContext, +} from 'sentry/components/events/contexts/knownContext/flutterContext'; + +const MOCK_FLUTTER_CONTEXT: FlutterContext = { + default_route_name: '/', + has_render_view: 'true', + // Extra data is still valid and preserved + extra_data: 'something', + unknown_key: 123, +}; + +const MOCK_REDACTION = { + default_route_name: { + '': { + rem: [['organization:0', 's', 0, 0]], + len: 1, + }, + }, +}; + +describe('FlutterContext', () => { + it('returns values according to the parameters', () => { + expect(getFlutterContextData({data: MOCK_FLUTTER_CONTEXT})).toEqual([ + {key: 'default_route_name', subject: 'Default Route Name', value: '/'}, + {key: 'has_render_view', subject: 'Has Render View', value: 'true'}, + { + key: 'extra_data', + subject: 'extra_data', + value: 'something', + meta: undefined, + }, + { + key: 'unknown_key', + subject: 'unknown_key', + value: 123, + meta: undefined, + }, + ]); + }); + + it('renders with meta annotations correctly', () => { + const event = EventFixture({ + _meta: {contexts: {flutter_context: MOCK_REDACTION}}, + }); + + render( + + ); + + expect(screen.getByText('Flutter')).toBeInTheDocument(); + expect(screen.getByText('Has Render View')).toBeInTheDocument(); + expect(screen.getByText('true')).toBeInTheDocument(); + expect(screen.getByText('Default Route Name')).toBeInTheDocument(); + expect(screen.getByText(/redacted/)).toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/contexts/knownContext/flutterContext.tsx b/static/app/components/events/contexts/knownContext/flutterContext.tsx new file mode 100644 index 00000000000000..a396e4bfb0ebc2 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/flutterContext.tsx @@ -0,0 +1,46 @@ +import {getContextKeys} from 'sentry/components/events/contexts/utils'; +import {t} from 'sentry/locale'; +import type {KeyValueListData} from 'sentry/types/group'; + +enum FlutterContextKeys { + DEFAULT_ROUTE_NAME = 'default_route_name', + HAS_RENDER_VIEW = 'has_render_view', +} + +export interface FlutterContext { + [key: string]: any; + [FlutterContextKeys.DEFAULT_ROUTE_NAME]?: string; + [FlutterContextKeys.HAS_RENDER_VIEW]?: string; +} + +export function getFlutterContextData({ + data, + meta, +}: { + data: FlutterContext; + meta?: Record; +}): KeyValueListData { + return getContextKeys({data}).map(ctxKey => { + switch (ctxKey) { + case FlutterContextKeys.DEFAULT_ROUTE_NAME: + return { + key: ctxKey, + subject: t('Default Route Name'), + value: data.default_route_name, + }; + case FlutterContextKeys.HAS_RENDER_VIEW: + return { + key: ctxKey, + subject: t('Has Render View'), + value: data.has_render_view, + }; + default: + return { + key: ctxKey, + subject: ctxKey, + value: data[ctxKey], + meta: meta?.[ctxKey]?.[''], + }; + } + }); +} diff --git a/static/app/components/events/contexts/knownContext/reactNativeContext.spec.tsx b/static/app/components/events/contexts/knownContext/reactNativeContext.spec.tsx new file mode 100644 index 00000000000000..536bd10ebd2699 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/reactNativeContext.spec.tsx @@ -0,0 +1,86 @@ +import {EventFixture} from 'sentry-fixture/event'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {ContextCard} from 'sentry/components/events/contexts/contextCard'; +import { + getReactNativeContextData, + type ReactNativeContext, +} from 'sentry/components/events/contexts/knownContext/reactNativeContext'; + +const MOCK_REACT_NATIVE_CONTEXT: ReactNativeContext = { + expo: false, + fabric: true, + hermes_debug_info: false, + hermes_version: '250829098.0.10', + js_engine: 'hermes', + react_native_version: '0.85.1', + turbo_module: true, + // Extra data is still valid and preserved + extra_data: 'something', + unknown_key: 123, +}; + +const MOCK_REDACTION = { + hermes_version: { + '': { + rem: [['organization:0', 's', 0, 0]], + len: 15, + }, + }, +}; + +describe('ReactNativeContext', () => { + it('returns values according to the parameters', () => { + expect(getReactNativeContextData({data: MOCK_REACT_NATIVE_CONTEXT})).toEqual([ + {key: 'expo', subject: 'Expo', value: false}, + {key: 'fabric', subject: 'Fabric', value: true}, + {key: 'hermes_debug_info', subject: 'Hermes Debug Info', value: false}, + { + key: 'hermes_version', + subject: 'Hermes Version', + value: '250829098.0.10', + }, + {key: 'js_engine', subject: 'JS Engine', value: 'hermes'}, + { + key: 'react_native_version', + subject: 'React Native Version', + value: '0.85.1', + }, + {key: 'turbo_module', subject: 'Turbo Module', value: true}, + { + key: 'extra_data', + subject: 'extra_data', + value: 'something', + meta: undefined, + }, + { + key: 'unknown_key', + subject: 'unknown_key', + value: 123, + meta: undefined, + }, + ]); + }); + + it('renders with meta annotations correctly', () => { + const event = EventFixture({ + _meta: {contexts: {react_native_context: MOCK_REDACTION}}, + }); + + render( + + ); + + expect(screen.getByText('React Native')).toBeInTheDocument(); + expect(screen.getByText('JS Engine')).toBeInTheDocument(); + expect(screen.getByText('hermes')).toBeInTheDocument(); + expect(screen.getByText('Hermes Version')).toBeInTheDocument(); + expect(screen.getByText(/redacted/)).toBeInTheDocument(); + }); +}); diff --git a/static/app/components/events/contexts/knownContext/reactNativeContext.tsx b/static/app/components/events/contexts/knownContext/reactNativeContext.tsx new file mode 100644 index 00000000000000..258a5d11c306e4 --- /dev/null +++ b/static/app/components/events/contexts/knownContext/reactNativeContext.tsx @@ -0,0 +1,86 @@ +import {getContextKeys} from 'sentry/components/events/contexts/utils'; +import {t} from 'sentry/locale'; +import type {KeyValueListData} from 'sentry/types/group'; + +enum ReactNativeContextKeys { + EXPO = 'expo', + FABRIC = 'fabric', + HERMES_DEBUG_INFO = 'hermes_debug_info', + HERMES_VERSION = 'hermes_version', + JS_ENGINE = 'js_engine', + REACT_NATIVE_VERSION = 'react_native_version', + TURBO_MODULE = 'turbo_module', +} + +export interface ReactNativeContext { + [key: string]: any; + [ReactNativeContextKeys.EXPO]?: boolean; + [ReactNativeContextKeys.FABRIC]?: boolean; + [ReactNativeContextKeys.HERMES_DEBUG_INFO]?: boolean; + [ReactNativeContextKeys.HERMES_VERSION]?: string; + [ReactNativeContextKeys.JS_ENGINE]?: string; + [ReactNativeContextKeys.REACT_NATIVE_VERSION]?: string; + [ReactNativeContextKeys.TURBO_MODULE]?: boolean; +} + +export function getReactNativeContextData({ + data, + meta, +}: { + data: ReactNativeContext; + meta?: Record; +}): KeyValueListData { + return getContextKeys({data}).map(ctxKey => { + switch (ctxKey) { + case ReactNativeContextKeys.EXPO: + return { + key: ctxKey, + subject: t('Expo'), + value: data.expo, + }; + case ReactNativeContextKeys.FABRIC: + return { + key: ctxKey, + subject: t('Fabric'), + value: data.fabric, + }; + case ReactNativeContextKeys.HERMES_DEBUG_INFO: + return { + key: ctxKey, + subject: t('Hermes Debug Info'), + value: data.hermes_debug_info, + }; + case ReactNativeContextKeys.HERMES_VERSION: + return { + key: ctxKey, + subject: t('Hermes Version'), + value: data.hermes_version, + }; + case ReactNativeContextKeys.JS_ENGINE: + return { + key: ctxKey, + subject: t('JS Engine'), + value: data.js_engine, + }; + case ReactNativeContextKeys.REACT_NATIVE_VERSION: + return { + key: ctxKey, + subject: t('React Native Version'), + value: data.react_native_version, + }; + case ReactNativeContextKeys.TURBO_MODULE: + return { + key: ctxKey, + subject: t('Turbo Module'), + value: data.turbo_module, + }; + 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 d57ba74602a8ab..7b41bfea8a3ce0 100644 --- a/static/app/components/events/contexts/utils.tsx +++ b/static/app/components/events/contexts/utils.tsx @@ -12,16 +12,20 @@ import { getLogoImage, type ContextIconProps, } from 'sentry/components/events/contexts/contextIcon'; +import {getAccessibilityContextData} from 'sentry/components/events/contexts/knownContext/accessibility'; import {getAppContextData} from 'sentry/components/events/contexts/knownContext/app'; 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'; +import {getDartContextData} from 'sentry/components/events/contexts/knownContext/dartContext'; import {getDeviceContextData} from 'sentry/components/events/contexts/knownContext/device'; +import {getFlutterContextData} from 'sentry/components/events/contexts/knownContext/flutterContext'; import {getGPUContextData} from 'sentry/components/events/contexts/knownContext/gpu'; import {getMemoryInfoContext} from 'sentry/components/events/contexts/knownContext/memoryInfo'; import {getMissingInstrumentationContextData} from 'sentry/components/events/contexts/knownContext/missingInstrumentation'; import {getOperatingSystemContextData} from 'sentry/components/events/contexts/knownContext/os'; import {getProfileContextData} from 'sentry/components/events/contexts/knownContext/profile'; +import {getReactNativeContextData} from 'sentry/components/events/contexts/knownContext/reactNativeContext'; import {getReplayContextData} from 'sentry/components/events/contexts/knownContext/replay'; import {getRuntimeContextData} from 'sentry/components/events/contexts/knownContext/runtime'; import {getStateContextData} from 'sentry/components/events/contexts/knownContext/state'; @@ -293,6 +297,12 @@ export function getContextTitle({ return t('OTA Updates'); case 'react_native_context': return t('React Native'); + case 'accessibility': + return t('Accessibility'); + case 'flutter_context': + return t('Flutter'); + case 'dart_context': + return t('Dart'); default: return contextType; } @@ -435,6 +445,14 @@ export function getFormattedContextData({ return getCultureContextData({data: contextValue, meta}); case 'missing_instrumentation': return getMissingInstrumentationContextData({data: contextValue, meta}); + case 'accessibility': + return getAccessibilityContextData({data: contextValue, meta}); + case 'react_native_context': + return getReactNativeContextData({data: contextValue, meta}); + case 'flutter_context': + return getFlutterContextData({data: contextValue, meta}); + case 'dart_context': + return getDartContextData({data: contextValue, meta}); default: return getContextKeys({data: contextValue}).map(ctxKey => ({ key: ctxKey, diff --git a/static/app/types/event.tsx b/static/app/types/event.tsx index e159a6de9b1b2e..ad772db2ae15ad 100644 --- a/static/app/types/event.tsx +++ b/static/app/types/event.tsx @@ -433,6 +433,13 @@ export enum DeviceContextKey { SUPPORTS_LOCATION_SERVICE = 'supports_location_service', SUPPORTS_VIBRATION = 'supports_vibration', USABLE_MEMORY = 'usable_memory', + TIMEZONE = 'timezone', + LOCALE = 'locale', + ARCHS = 'archs', + CHIPSET = 'chipset', + CONNECTION_TYPE = 'connection_type', + LOW_POWER_MODE = 'low_power_mode', + THERMAL_STATE = 'thermal_state', } // https://develop.sentry.dev/sdk/event-payloads/contexts/#device-context @@ -478,10 +485,16 @@ export interface DeviceContext [DeviceContextKey.SUPPORTS_LOCATION_SERVICE]?: boolean; [DeviceContextKey.SUPPORTS_VIBRATION]?: boolean; [DeviceContextKey.USABLE_MEMORY]?: number; + [DeviceContextKey.LOCALE]?: string; + [DeviceContextKey.ARCHS]?: string[]; + [DeviceContextKey.CHIPSET]?: string; + [DeviceContextKey.CONNECTION_TYPE]?: string; + [DeviceContextKey.LOW_POWER_MODE]?: boolean; + // This field is deprecated in favour of timezone field in culture context + [DeviceContextKey.TIMEZONE]?: string; + [DeviceContextKey.THERMAL_STATE]?: string; // This field is deprecated in favour of locale field in culture context language?: string; - // This field is deprecated in favour of timezone field in culture context - timezone?: string; } enum RuntimeContextKey {