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/`;