diff --git a/static/app/components/events/breadcrumbs/utils.tsx b/static/app/components/events/breadcrumbs/utils.tsx index a5e760394b5e..6103d9ea2436 100644 --- a/static/app/components/events/breadcrumbs/utils.tsx +++ b/static/app/components/events/breadcrumbs/utils.tsx @@ -198,16 +198,39 @@ export interface EnhancedCrumb { export function getEnhancedBreadcrumbs(event: Event, theme: Theme): EnhancedCrumb[] { const breadcrumbEntryIndex = event.entries?.findIndex(entry => entry.type === EntryType.BREADCRUMBS) ?? -1; - const breadcrumbs: any[] = event.entries?.[breadcrumbEntryIndex]?.data?.values ?? []; - - if (breadcrumbs.length === 0) { - return []; - } + const breadcrumbs: RawCrumb[] = + event.entries?.[breadcrumbEntryIndex]?.data?.values ?? []; // Mapping of breadcrumb index -> breadcrumb meta const meta: Record = event._meta?.entries?.[breadcrumbEntryIndex]?.data?.values ?? {}; + return getEnhancedBreadcrumbsFromValues({ + breadcrumbs, + theme, + meta, + virtualCrumb: getVirtualCrumb(event), + }); +} + +/** + * Prefer `getEnhancedBreadcrumbs` when an `Event` is available. + */ +export function getEnhancedBreadcrumbsFromValues({ + breadcrumbs, + theme, + meta = {}, + virtualCrumb, +}: { + breadcrumbs: RawCrumb[]; + theme: Theme; + meta?: Record; + virtualCrumb?: RawCrumb; +}): EnhancedCrumb[] { + if (breadcrumbs.length === 0) { + return []; + } + const enhancedCrumbs = breadcrumbs.map< Pick >((raw, i) => ({ @@ -217,9 +240,6 @@ export function getEnhancedBreadcrumbs(event: Event, theme: Theme): EnhancedCrum breadcrumb: convertCrumbType(raw), })); - // The virtual crumb is a representation of this event, displayed alongside - // the rest of the breadcrumbs for more additional context. - const virtualCrumb = getVirtualCrumb(event); const allCrumbs = virtualCrumb ? [...enhancedCrumbs, {breadcrumb: virtualCrumb}] : enhancedCrumbs; diff --git a/static/app/components/events/contexts/contextCard.tsx b/static/app/components/events/contexts/contextCard.tsx index d4f911b6932e..24a3add320f8 100644 --- a/static/app/components/events/contexts/contextCard.tsx +++ b/static/app/components/events/contexts/contextCard.tsx @@ -25,8 +25,8 @@ import {useOrganization} from 'sentry/utils/useOrganization'; interface ContextCardProps { alias: string; - event: Event; type: string; + event?: Event; project?: Project; value?: ContextValue; } diff --git a/static/app/components/events/contexts/index.tsx b/static/app/components/events/contexts/index.tsx index 2a79279928b3..69fdb67e11cb 100644 --- a/static/app/components/events/contexts/index.tsx +++ b/static/app/components/events/contexts/index.tsx @@ -30,7 +30,25 @@ export interface ContextItem { } export function getOrderedContextItems(event: Event): ContextItem[] { - const {user, contexts} = event; + return getOrderedContextItemsFromContexts({ + contexts: event.contexts, + user: event.user, + hasSyntheticTrace: eventHasSyntheticTrace(event), + }); +} + +/** + * Prefer `getOrderedContextItems` when an `Event` is available. + */ +export function getOrderedContextItemsFromContexts({ + contexts, + user, + hasSyntheticTrace = false, +}: { + contexts: Event['contexts'] | undefined; + hasSyntheticTrace?: boolean; + user?: Event['user']; +}): ContextItem[] { const {data: customUserData, ...userContext} = user ?? {}; // hide `flags` in the contexts section since we display this @@ -64,7 +82,7 @@ export function getOrderedContextItems(event: Event): ContextItem[] { const items = orderedContext .filter(([alias, ctxValue]) => { - if (alias === 'trace' && eventHasSyntheticTrace(event)) { + if (alias === 'trace' && hasSyntheticTrace) { return false; } const contextKeys = Object.keys(ctxValue ?? {}); diff --git a/static/app/components/events/contexts/knownContext/app.tsx b/static/app/components/events/contexts/knownContext/app.tsx index 73bdb5a23fcf..597f0d4e675d 100644 --- a/static/app/components/events/contexts/knownContext/app.tsx +++ b/static/app/components/events/contexts/knownContext/app.tsx @@ -63,7 +63,7 @@ export function getAppContextData({ meta, }: { data: AppContext; - event: Event; + event?: Event; meta?: Record; }): KeyValueListData { return getContextKeys({data}).map(ctxKey => { @@ -79,7 +79,7 @@ export function getAppContextData({ key: ctxKey, subject: t('Start Time'), value: getRelativeTimeFromEventDateCreated( - event.dateCreated ? event.dateCreated : event.dateReceived, + event?.dateCreated ?? event?.dateReceived, data.app_start_time ), }; diff --git a/static/app/components/events/contexts/knownContext/device.tsx b/static/app/components/events/contexts/knownContext/device.tsx index c28d2cc040b0..e25a749c466e 100644 --- a/static/app/components/events/contexts/knownContext/device.tsx +++ b/static/app/components/events/contexts/knownContext/device.tsx @@ -134,7 +134,7 @@ export function getDeviceContextData({ meta, }: { data: DeviceContext; - event: Event; + event?: Event; meta?: Record; }): KeyValueListData { return getContextKeys({data: getInferredData(data)}).map(ctxKey => { @@ -284,7 +284,7 @@ export function getDeviceContextData({ key: ctxKey, subject: t('Boot Time'), value: getRelativeTimeFromEventDateCreated( - event.dateCreated ? event.dateCreated : event.dateReceived, + event?.dateCreated ?? event?.dateReceived, data.boot_time ), }; diff --git a/static/app/components/events/contexts/knownContext/profile.tsx b/static/app/components/events/contexts/knownContext/profile.tsx index 578340664ee5..2f72c8cbfd12 100644 --- a/static/app/components/events/contexts/knownContext/profile.tsx +++ b/static/app/components/events/contexts/knownContext/profile.tsx @@ -20,8 +20,8 @@ export function getProfileContextData({ meta, }: { data: ProfileContext; - event: Event; organization: Organization; + event?: Event; meta?: Record; project?: Project; }): KeyValueListData { @@ -72,7 +72,7 @@ function getProfileIdEntry( function getProfilerIdEntry( data: ProfileContext, - event: Event, + event: Event | undefined, organization: Organization, project?: Project ) { @@ -82,7 +82,7 @@ function getProfilerIdEntry( } const [start, end] = getStartEnd(event); const link = - project?.slug && start && end + event && project?.slug && start && end ? generateContinuousProfileFlamechartRouteWithQuery({ organization, projectSlug: project.slug, diff --git a/static/app/components/events/contexts/knownContext/trace.tsx b/static/app/components/events/contexts/knownContext/trace.tsx index 4e4f151cc541..8854abe4657f 100644 --- a/static/app/components/events/contexts/knownContext/trace.tsx +++ b/static/app/components/events/contexts/knownContext/trace.tsx @@ -47,9 +47,9 @@ export function getTraceContextData({ meta, }: { data: TraceContext; - event: Event; location: Location; organization: Organization; + event?: Event; meta?: Record; }): KeyValueListData { return getContextKeys({data}) @@ -66,7 +66,9 @@ export function getTraceContextData({ const traceWasSampled = data?.sampled ?? true; if (traceWasSampled) { - const link = getTraceTargetFromEvent(event, organization, location); + const link = event + ? getTraceTargetFromEvent(event, organization, location) + : undefined; const hasPerformanceView = organization.features.includes('performance-view'); return { @@ -171,7 +173,7 @@ export function getTraceContextData({ const link = transactionSummaryRouteWithQuery({ organization, transaction: transactionName, - projectID: event.projectID, + projectID: event?.projectID, query: {}, }); diff --git a/static/app/components/events/contexts/utils.tsx b/static/app/components/events/contexts/utils.tsx index 435a9a97a065..d0b26afe2d17 100644 --- a/static/app/components/events/contexts/utils.tsx +++ b/static/app/components/events/contexts/utils.tsx @@ -106,7 +106,7 @@ export function generateIconName( } export function getRelativeTimeFromEventDateCreated( - eventDateCreated: string, + eventDateCreated: string | undefined, timestamp?: string, showTimestamp = true ) { @@ -120,7 +120,14 @@ export function getRelativeTimeFromEventDateCreated( return timestamp; } - const relativeTime = `(${dateTime.from(eventDateCreated, true)} ${t( + // Without a valid reference date (e.g. the event isn't available) we can't + // compute a relative time, so just show the timestamp on its own. + const referenceDate = moment(eventDateCreated); + if (!eventDateCreated || !referenceDate.isValid()) { + return timestamp; + } + + const relativeTime = `(${dateTime.from(referenceDate, true)} ${t( 'before this event' )})`; @@ -314,17 +321,20 @@ export function getContextTitle({ } } -export function getContextMeta(event: Event, contextType: string): Record { - const defaultMeta = event._meta?.contexts?.[contextType] ?? {}; +export function getContextMeta( + event: Event | undefined, + 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; + return event?._meta?.contexts?.['Memory Info'] ?? defaultMeta; case 'threadpool_info': // Current case 'ThreadPool Info': // Legacy - return event._meta?.contexts?.['ThreadPool Info'] ?? defaultMeta; + return event?._meta?.contexts?.['ThreadPool Info'] ?? defaultMeta; case 'user': - return event._meta?.user ?? defaultMeta; + return event?._meta?.user ?? defaultMeta; default: return defaultMeta; } @@ -392,9 +402,9 @@ export function getFormattedContextData({ }: { contextType: string; contextValue: any; - event: Event; location: Location; organization: Organization; + event?: Event; project?: Project; }): KeyValueListData { const meta = getContextMeta(event, contextType); diff --git a/static/app/types/event.tsx b/static/app/types/event.tsx index ffa02bc70372..e37aa69f3d55 100644 --- a/static/app/types/event.tsx +++ b/static/app/types/event.tsx @@ -266,7 +266,7 @@ export type EntryDebugMeta = { type: EntryType.DEBUGMETA; }; -type EntryBreadcrumbs = { +export type EntryBreadcrumbs = { data: { values: RawCrumb[]; }; diff --git a/static/app/views/explore/hooks/useTraceItemDetails.tsx b/static/app/views/explore/hooks/useTraceItemDetails.tsx index 6ac14bf38315..0ffc0078bea2 100644 --- a/static/app/views/explore/hooks/useTraceItemDetails.tsx +++ b/static/app/views/explore/hooks/useTraceItemDetails.tsx @@ -5,6 +5,8 @@ import {skipToken, useQuery, useQueryClient} from '@tanstack/react-query'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; +import type {RawCrumb} from 'sentry/types/breadcrumbs'; +import type {EventTransaction} from 'sentry/types/event'; import type {Meta} from 'sentry/types/group'; import {apiOptions} from 'sentry/utils/api/apiOptions'; import {normalizeTimestampToSeconds} from 'sentry/utils/dates'; @@ -66,6 +68,11 @@ export interface TraceItemDetailsResponse { itemId: string; meta: TraceItemDetailsMeta; timestamp: string; + event?: { + breadcrumbs?: {values: RawCrumb[]}; + contexts?: EventTransaction['contexts']; + extra?: EventTransaction['context']; + }; links?: TraceItemResponseLink[]; } diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/contexts.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/contexts.tsx index 46df8e2d2655..b25e4376d513 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/contexts.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/contexts.tsx @@ -1,8 +1,8 @@ -import {getOrderedContextItems} from 'sentry/components/events/contexts'; +import {getOrderedContextItemsFromContexts} from 'sentry/components/events/contexts'; import {ContextCard} from 'sentry/components/events/contexts/contextCard'; import {KeyValueData} from 'sentry/components/keyValueData'; import {t} from 'sentry/locale'; -import {EntryType, type EventTransaction} from 'sentry/types/event'; +import type {EventTransaction} from 'sentry/types/event'; import type {Project} from 'sentry/types/project'; import {SectionKey} from 'sentry/views/issueDetails/context'; import {FoldSection} from 'sentry/views/issueDetails/foldSection'; @@ -11,7 +11,6 @@ import { AdditionalData, hasAdditionalData, } from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/additionalData'; -import {Request} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/request'; // List of context types that are displayed as span attributes. // These should not be displayed in the contexts section. @@ -31,39 +30,26 @@ const DUPLICATES_FROM_ATTRIBUTES = [ ]; export function Contexts({ - event, + contexts, + extra, project, }: { - event: EventTransaction | undefined; + contexts: EventTransaction['contexts'] | undefined; + extra: EventTransaction['context'] | undefined; project: Project | undefined; }) { - if (!event) { - return null; - } - - const extraContexts = getOrderedContextItems(event).filter( + const extraContexts = getOrderedContextItemsFromContexts({contexts}).filter( ({type}) => !DUPLICATES_FROM_ATTRIBUTES.includes(type) ); - const eventHasExtraContexts = Object.keys(extraContexts).length > 0; - - const eventHasRequestEntry = event?.entries.some( - entry => entry.type === EntryType.REQUEST - ); - const eventHasAdditionalData = event ? hasAdditionalData(event) : false; + const eventHasExtraContexts = extraContexts.length > 0; + const eventHasAdditionalData = hasAdditionalData(extra); - if (!eventHasRequestEntry && !eventHasAdditionalData && !eventHasExtraContexts) { + if (!eventHasExtraContexts && !eventHasAdditionalData) { return null; } const extraContextCards = extraContexts.map(({alias, type, value}) => ( - + )); return ( @@ -79,8 +65,7 @@ export function Contexts({ } disableCollapsePersistence > - {eventHasRequestEntry ? : null} - {eventHasAdditionalData ? : null} + {eventHasAdditionalData ? : null} {eventHasExtraContexts ? ( {extraContextCards} ) : null} diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx index 82621da1c793..22045a54e0b9 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/index.tsx @@ -12,7 +12,11 @@ import {LoadingError} from 'sentry/components/loadingError'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {IconBroadcast} from 'sentry/icons'; import {t} from 'sentry/locale'; -import type {EventTransaction} from 'sentry/types/event'; +import { + EntryType, + type EntryBreadcrumbs, + type EventTransaction, +} from 'sentry/types/event'; import type {NewQuery, Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; @@ -469,6 +473,17 @@ function EAPSpanNodeDetailsContent({ const links = traceItemData.links; const isTransaction = node.value.is_transaction; + // Contexts, breadcrumbs, and extra exist in traceItemData as of Aug 6 2026. Fall back to + // eventTransaction for older data. eventTransaction use can be removed once we're past the + // retention window. + const contexts = traceItemData.event?.contexts ?? eventTransaction?.contexts; + const extra = traceItemData.event?.extra ?? eventTransaction?.context; + const breadcrumbs = + traceItemData.event?.breadcrumbs ?? + eventTransaction?.entries.find( + (entry): entry is EntryBreadcrumbs => entry.type === EntryType.BREADCRUMBS + )?.data; + const threadIdAttribute = attributesMap['thread.id']; const threadId = typeof threadIdAttribute === 'string' ? threadIdAttribute : undefined; @@ -595,8 +610,8 @@ function EAPSpanNodeDetailsContent({ project={project} /> - {isTransaction && eventTransaction ? ( - + {isTransaction && (contexts || extra) ? ( + ) : null} @@ -642,9 +657,7 @@ function EAPSpanNodeDetailsContent({ /> ) : null} - {isTransaction && eventTransaction ? ( - - ) : null} + {isTransaction && breadcrumbs ? : null} {isTransaction && eventTransaction && project ? ( ; } - if (isError) { + if (isError || !event) { return ; } - const project = projects.find(proj => proj.slug === event?.projectSlug); + const project = projects.find(proj => proj.slug === event.projectSlug); + + const breadcrumbEntryIndex = event.entries.findIndex( + entry => entry.type === EntryType.BREADCRUMBS + ); + const breadcrumbs = ( + event.entries[breadcrumbEntryIndex] as EntryBreadcrumbs | undefined + )?.data; + const breadcrumbMeta = event._meta?.entries?.[breadcrumbEntryIndex]?.data?.values; return ( @@ -210,7 +222,9 @@ export function TransactionNodeDetails({ /> )} - + {breadcrumbs ? ( + + ) : null} {project ? ( @@ -261,7 +275,9 @@ function TransactionSpecificSections(props: TransactionSpecificSectionsProps) { {hasSDKContext(event) || cacheMetrics.length > 0 ? ( ) : null} - {hasAdditionalData(event) ? : null} + {hasAdditionalData(event.context) ? ( + + ) : null} {hasMeasurements(event) ? ( ) : null} diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/additionalData.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/additionalData.tsx index 7c8916bb0988..f08622360bc7 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/additionalData.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/additionalData.tsx @@ -11,6 +11,8 @@ import {defined} from 'sentry/utils/defined'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; import {TraceDrawerComponents} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/styles'; +type EventExtra = EventTransaction['context']; + enum EventExtraDataType { CRASHED_PROCESS = 'crashed_process', } @@ -43,21 +45,27 @@ function getEventExtraDataKnownDataDetails({ } } -export function hasAdditionalData(event: EventTransaction) { - return !!event.context && !isEmptyObject(event.context); +export function hasAdditionalData(extra: EventExtra | undefined) { + return !!extra && !isEmptyObject(extra); } -export function AdditionalData({event}: {event: EventTransaction}) { +export function AdditionalData({ + extra, + meta, +}: { + extra: EventExtra | undefined; + meta?: Record; +}) { const [raw, setRaw] = useState(false); - if (!defined(event.context) || isEmptyObject(event.context)) { + if (!defined(extra) || isEmptyObject(extra)) { return null; } const knownData = getKnownData({ - data: event.context, - knownDataTypes: Object.keys(event.context), - meta: event._meta?.context, + data: extra, + knownDataTypes: Object.keys(extra), + meta, onGetKnownDataDetails: v => getEventExtraDataKnownDataDetails(v), }); @@ -72,7 +80,7 @@ export function AdditionalData({event}: {event: EventTransaction}) { withAnnotatedText value={data.value} maxDefaultDepth={2} - meta={event._meta?.context} + meta={meta} /> ), }; diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/breadCrumbs.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/breadCrumbs.tsx index ffbcc8d36567..d73cfeca98e9 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/breadCrumbs.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/transaction/sections/breadCrumbs.tsx @@ -10,7 +10,7 @@ import {BreadcrumbsTimeline} from 'sentry/components/events/breadcrumbs/breadcru import { BREADCRUMB_TIME_DISPLAY_LOCALSTORAGE_KEY, BreadcrumbTimeDisplay, - getEnhancedBreadcrumbs, + getEnhancedBreadcrumbsFromValues, useBreadcrumbFilters, } from 'sentry/components/events/breadcrumbs/utils'; import { @@ -21,13 +21,19 @@ import { } from 'sentry/components/events/interfaces/breadcrumbs'; import {IconFilter, IconSearch, IconSort} from 'sentry/icons'; import {t} from 'sentry/locale'; -import type {EventTransaction} from 'sentry/types/event'; +import type {RawCrumb} from 'sentry/types/breadcrumbs'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; import {FoldSection} from 'sentry/views/issueDetails/foldSection'; const MAX_BREADCRUMBS_HEIGHT = 400; -export function BreadCrumbs({event}: {event: EventTransaction}) { +export function BreadCrumbs({ + breadcrumbs, + meta, +}: { + breadcrumbs: {values: RawCrumb[]}; + meta?: Record; +}) { const theme = useTheme(); const [container, setContainer] = useState(null); const [search, setSearch] = useState(''); @@ -42,8 +48,9 @@ export function BreadCrumbs({event}: {event: EventTransaction}) { ); const enhancedCrumbs = useMemo( - () => getEnhancedBreadcrumbs(event, theme), - [event, theme] + () => + getEnhancedBreadcrumbsFromValues({breadcrumbs: breadcrumbs.values, theme, meta}), + [breadcrumbs, meta, theme] ); const {filterOptions, applyFilters} = useBreadcrumbFilters(enhancedCrumbs);