import {Fragment} from 'react'; import styled from '@emotion/styled'; import type {Location} from 'history'; import moment from 'moment-timezone'; import logoUnknown from 'sentry-logos/logo-unknown.svg'; import UserAvatar from 'sentry/components/avatar/userAvatar'; import {DeviceName} from 'sentry/components/deviceName'; import { ContextIcon, type ContextIconProps, getLogoImage, } from 'sentry/components/events/contexts/contextIcon'; 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 {getDeviceContextData} from 'sentry/components/events/contexts/knownContext/device'; 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 {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'; import {getThreadPoolInfoContext} from 'sentry/components/events/contexts/knownContext/threadPoolInfo'; import {getTraceContextData} from 'sentry/components/events/contexts/knownContext/trace'; import {getUserContextData} from 'sentry/components/events/contexts/knownContext/user'; import { getPlatformContextData, getPlatformContextIcon, getPlatformContextTitle, PLATFORM_CONTEXT_KEYS, } from 'sentry/components/events/contexts/platformContext/utils'; import {userContextToActor} from 'sentry/components/events/interfaces/utils'; import StructuredEventData from 'sentry/components/structuredEventData'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import type {KeyValueListData, KeyValueListDataItem} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import type {AvatarUser} from 'sentry/types/user'; import {defined} from 'sentry/utils'; import commonTheme from 'sentry/utils/theme'; /** * Generates the class name used for contexts */ export function generateIconName( name?: string | boolean | null, version?: string ): string { if (!defined(name) || typeof name === 'boolean') { return ''; } const lowerCaseName = name.toLowerCase(); // amazon fire tv device id changes with version: AFTT, AFTN, AFTS, AFTA, AFTVA (alexa), ... if (lowerCaseName.startsWith('aft')) { return 'amazon'; } if (lowerCaseName.startsWith('sm-') || lowerCaseName.startsWith('st-')) { return 'samsung'; } if (lowerCaseName.startsWith('moto')) { return 'motorola'; } if (lowerCaseName.startsWith('pixel')) { return 'google'; } if (lowerCaseName.startsWith('vercel')) { return 'vercel'; } const formattedName = name .split(/\d/)[0] .toLowerCase() .replace(/[^a-z0-9\-]+/g, '-') .replace(/\-+$/, '') .replace(/^\-+/, ''); if (formattedName === 'edge' && version) { const majorVersion = version.split('.')[0]; const isLegacyEdge = majorVersion >= '12' && majorVersion <= '18'; return isLegacyEdge ? 'legacy-edge' : 'edge'; } if (formattedName.endsWith('-mobile')) { return formattedName.split('-')[0]; } return formattedName; } export function getRelativeTimeFromEventDateCreated( eventDateCreated: string, timestamp?: string, showTimestamp = true ) { if (!defined(timestamp)) { return timestamp; } const dateTime = moment(timestamp); if (!dateTime.isValid()) { return timestamp; } const relativeTime = `(${dateTime.from(eventDateCreated, true)} ${t( 'before this event' )})`; if (!showTimestamp) { return {relativeTime}; } return ( {timestamp} {relativeTime} ); } export type KnownDataDetails = Omit | undefined; export function getKnownData({ data, knownDataTypes, onGetKnownDataDetails, meta, }: { data: Data; knownDataTypes: string[]; onGetKnownDataDetails: (props: {data: Data; type: DataType}) => KnownDataDetails; meta?: Record; }): KeyValueListData { const filteredTypes = knownDataTypes.filter(knownDataType => { if ( typeof data[knownDataType] !== 'number' && typeof data[knownDataType] !== 'boolean' && !data[knownDataType] ) { return !!meta?.[knownDataType]; } return true; }); return filteredTypes .map(type => { const knownDataDetails = onGetKnownDataDetails({ data, type: type as unknown as DataType, }); if (!knownDataDetails) { return null; } return { key: type, ...knownDataDetails, value: knownDataDetails.value, }; }) .filter(defined); } export function getKnownStructuredData( knownData: KeyValueListData, meta: Record ): KeyValueListData { return knownData.map(kd => ({ ...kd, value: ( ), })); } export function getUnknownData({ allData, knownKeys, meta, }: { allData: Record; knownKeys: string[]; meta?: NonNullable[keyof Event['_meta']]; }): KeyValueListData { return Object.entries(allData) .filter( ([key]) => key !== 'type' && key !== 'title' && !knownKeys.includes(key) && (typeof allData[key] !== 'number' && !allData[key] ? !!meta?.[key]?.[''] : true) ) .map(([key, value]) => ({ key, value, subject: key, meta: meta?.[key]?.[''], })); } /** * Returns the type of a given context, after coercing from its type and alias. * - 'type' refers the the `type` key on it's data blob. This is usually overridden by the SDK for known types, but not always. * - 'alias' refers to the key on event.contexts. This can be set by the user, but we have to depend on it for some contexts. */ export function getContextType({alias, type}: {alias: string; type?: string}): string { if (!defined(type)) { return alias; } return type === 'default' ? alias : type; } /** * Omit certain keys from ever being displayed on context items. * All custom context (and some known context) has the type:default so we remove it. */ export function getContextKeys({ data, hiddenKeys = [], }: { data: Record; hiddenKeys?: string[]; }): string[] { const hiddenKeySet = new Set(hiddenKeys); return Object.keys(data).filter( ctxKey => ctxKey !== 'type' && !hiddenKeySet.has(ctxKey) ); } export function getContextTitle({ alias, type, value = {}, }: { alias: string; type: string; value?: Record; }) { if (defined(value.title) && typeof value.title !== 'object') { return value.title; } const contextType = getContextType({alias, type}); if (PLATFORM_CONTEXT_KEYS.has(contextType)) { return getPlatformContextTitle({platform: alias}); } switch (contextType) { case 'app': return t('App'); case 'device': return t('Device'); case 'browser': return t('Browser'); case 'response': return t('Response'); case 'feedback': return t('Feedback'); case 'os': return t('Operating System'); case 'user': return t('User'); case 'gpu': return t('Graphics Processing Unit'); case 'runtime': return t('Runtime'); case 'trace': return t('Trace Details'); case 'otel': return 'OpenTelemetry'; case 'cloud_resource': return t('Cloud Resource'); case 'culture': case 'Current Culture': return t('Culture'); case 'missing_instrumentation': return t('Missing OTEL Instrumentation'); case 'unity': return 'Unity'; case 'memory_info': // Current value for memory info case 'Memory Info': // Legacy for memory info return t('Memory Info'); case 'threadpool_info': // Current value for thread pool info case 'ThreadPool Info': // Legacy value for thread pool info return t('Thread Pool Info'); case 'state': return t('Application State'); case 'laravel': return t('Laravel Context'); case 'profile': return t('Profile'); case 'replay': return t('Replay'); default: return contextType; } } export function getContextMeta(event: Event, contextType: string): Record { const defaultMeta = event._meta?.contexts?.[contextType] ?? {}; switch (contextType) { case 'memory_info': // Current case 'Memory Info': // Legacy return event._meta?.contexts?.['Memory Info'] ?? defaultMeta; case 'threadpool_info': // Current case 'ThreadPool Info': // Legacy return event._meta?.contexts?.['ThreadPool Info'] ?? defaultMeta; case 'user': return event._meta?.user ?? defaultMeta; default: return defaultMeta; } } export function getContextIcon({ alias, type, value = {}, contextIconProps = {}, }: { alias: string; type: string; contextIconProps?: Partial; value?: Record; }): React.ReactNode { const contextType = getContextType({alias, type}); if (PLATFORM_CONTEXT_KEYS.has(contextType)) { return getPlatformContextIcon({ platform: alias, size: contextIconProps?.size ?? 'xl', }); } let iconName = ''; switch (type) { case 'device': iconName = generateIconName(value?.model); break; case 'client_os': case 'os': iconName = generateIconName(value?.name); break; case 'runtime': case 'browser': iconName = generateIconName(value?.name, value?.version); break; case 'user': const user = userContextToActor(value); const iconSize = commonTheme.iconNumberSizes[contextIconProps?.size ?? 'xl']; return ; case 'gpu': iconName = generateIconName(value?.vendor_name ? value?.vendor_name : value?.name); break; default: break; } if (iconName.length === 0) { return null; } const imageName = getLogoImage(iconName); if (imageName === logoUnknown) { return null; } return ; } export function getFormattedContextData({ event, contextType, contextValue, organization, project, location, }: { contextType: string; contextValue: any; event: Event; location: Location; organization: Organization; project?: Project; }): KeyValueListData { const meta = getContextMeta(event, contextType); if (PLATFORM_CONTEXT_KEYS.has(contextType)) { return getPlatformContextData({platform: contextType, data: contextValue}); } switch (contextType) { case 'app': return getAppContextData({data: contextValue, event, meta}); case 'device': return getDeviceContextData({data: contextValue, event, meta}); case 'memory_info': // Current case 'Memory Info': // Legacy return getMemoryInfoContext({data: contextValue, meta}); case 'browser': return getBrowserContextData({data: contextValue, meta}); case 'os': return getOperatingSystemContextData({data: contextValue, meta}); case 'runtime': return getRuntimeContextData({data: contextValue, meta}); case 'user': return getUserContextData({data: contextValue, meta}); case 'gpu': return getGPUContextData({data: contextValue, meta}); case 'trace': return getTraceContextData({ data: contextValue, event, meta, organization, location, }); case 'threadpool_info': // Current case 'ThreadPool Info': // Legacy return getThreadPoolInfoContext({data: contextValue, meta}); case 'state': return getStateContextData({data: contextValue, meta}); case 'profile': return getProfileContextData({data: contextValue, meta, organization, project}); case 'replay': return getReplayContextData({data: contextValue, meta}); case 'cloud_resource': return getCloudResourceContextData({data: contextValue, meta}); case 'culture': case 'Current Culture': return getCultureContextData({data: contextValue, meta}); case 'missing_instrumentation': return getMissingInstrumentationContextData({data: contextValue, meta}); default: return getContextKeys({data: contextValue}).map(ctxKey => ({ key: ctxKey, subject: ctxKey, value: contextValue[ctxKey], meta: meta?.[ctxKey]?.[''], })); } } /** * Reimplemented as util function from legacy summaries deleted in this PR - https://github.com/getsentry/sentry/pull/71695/files * Consildated into one function and neglects any meta annotations since those will be rendered in the proper contexts section. * The only difference is we don't render 'unknown' values, since that doesn't help the user. */ export function getContextSummary({ type, value: data, }: { type: string; value?: Record; }): { subtitle: React.ReactNode; title: React.ReactNode; subtitleType?: string; } { let title: React.ReactNode = null; let subtitle: React.ReactNode = null; let subtitleType: string | undefined = undefined; switch (type) { case 'device': title = ( {deviceName => {deviceName ? deviceName : data?.name}} ); if (defined(data?.arch)) { subtitle = data?.arch; subtitleType = t('Architecture'); } else if (defined(data?.model)) { subtitle = data?.model; subtitleType = t('Model'); } break; case 'gpu': title = data?.name ?? null; if (defined(data?.vendor_name)) { subtitle = data?.vendor_name; subtitleType = t('Vendor'); } break; case 'os': case 'client_os': title = data?.name ?? null; if (defined(data?.version) && typeof data?.version === 'string') { subtitle = data?.version; subtitleType = t('Version'); } else if (defined(data?.kernel_version)) { subtitle = data?.kernel_version; subtitleType = t('Kernel'); } break; case 'user': if (defined(data?.email)) { title = data?.email; } if (defined(data?.ip_address) && !title) { title = data?.ip_address; } if (defined(data?.id)) { title = title ? title : data?.id; subtitle = data?.id; subtitleType = t('ID'); } if (defined(data?.username)) { title = title ? title : data?.username; subtitle = data?.username; subtitleType = t('Username'); } if (title === subtitle) { return { title, subtitle: null, }; } break; case 'runtime': case 'browser': title = data?.name ?? null; if (defined(data?.version)) { subtitle = data?.version; subtitleType = t('Version'); } break; default: break; } return { title, subtitle, subtitleType, }; } const RelativeTime = styled('span')` color: ${p => p.theme.subText}; margin-left: ${space(0.5)}; `; export const CONTEXT_DOCS_LINK = `https://docs.sentry.io/platform-redirect/?next=/enriching-events/context/`;