From 9ad47230c78b141de8be713c6dcb79a093b21155 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 13:59:21 -0300 Subject: [PATCH 01/39] ref(traces): Extract isPartialSpanOrTraceData to shared utility Move the 30-day trace age check from explore/tables/tracesTable/utils to a shared location at utils/trace/isOlderThan30Days. Add timestamp normalization to correctly handle both seconds and milliseconds. Create a reusable DisabledTraceLink component that renders muted text with a tooltip for links to traces older than 30 days. Co-Authored-By: Claude Opus 4.6 --- .../components/links/disabledTraceLink.tsx | 57 +++++++++++++++++++ static/app/utils/trace/isOlderThan30Days.tsx | 19 +++++++ .../views/explore/tables/fieldRenderer.tsx | 6 +- .../explore/tables/tracesTable/utils.tsx | 8 +-- 4 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 static/app/components/links/disabledTraceLink.tsx create mode 100644 static/app/utils/trace/isOlderThan30Days.tsx diff --git a/static/app/components/links/disabledTraceLink.tsx b/static/app/components/links/disabledTraceLink.tsx new file mode 100644 index 00000000000000..98b864d8535144 --- /dev/null +++ b/static/app/components/links/disabledTraceLink.tsx @@ -0,0 +1,57 @@ +import type {LocationDescriptorObject} from 'history'; + +import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; +import {Tooltip} from '@sentry/scraps/tooltip'; + +import {t, tct} from 'sentry/locale'; + +interface DisabledTraceLinkProps { + children: React.ReactNode; + type: 'trace' | 'span'; + similarEventsUrl?: LocationDescriptorObject | string; +} + +/** + * Renders a non-clickable, muted trace/span link with a tooltip + * explaining that the data is older than 30 days. + * + * Optionally includes a "View similar traces/spans" link in the tooltip. + */ +export function DisabledTraceLink({ + children, + type, + similarEventsUrl, +}: DisabledTraceLinkProps) { + let tooltipContent: React.ReactNode; + + if (type === 'trace') { + tooltipContent = similarEventsUrl ? ( + + {tct('Trace is older than 30 days. [similarLink] in the past 24 hours.', { + similarLink: {t('View similar traces')}, + })} + + ) : ( + {t('Trace is older than 30 days')} + ); + } else { + tooltipContent = similarEventsUrl ? ( + + {tct('Span is older than 30 days. [similarLink] in the past 24 hours.', { + similarLink: {t('View similar spans')}, + })} + + ) : ( + {t('Span is older than 30 days')} + ); + } + + return ( + + + {children} + + + ); +} diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx new file mode 100644 index 00000000000000..d8411c6886f2be --- /dev/null +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -0,0 +1,19 @@ +import moment from 'moment-timezone'; + +export const TRACE_DATA_RETENTION_DAYS = 30; + +/** + * Returns true if the given timestamp is older than 30 days, indicating + * that the trace/span data may no longer be available. + * + * Handles timestamps in seconds, milliseconds, or ISO string format. + */ +export function isPartialSpanOrTraceData(timestamp: string | number): boolean { + const now = moment(); + // Numbers < 1e12 are likely seconds (epoch), not milliseconds. + // e.g. 1712002518 is seconds, 1712002518000 is milliseconds. + const normalizedTimestamp = + typeof timestamp === 'number' && timestamp < 1e12 ? timestamp * 1000 : timestamp; + const timestampDate = moment(normalizedTimestamp); + return now.diff(timestampDate, 'days') > TRACE_DATA_RETENTION_DAYS; +} diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx index 67294bb6344c9a..ba1e747b2e1082 100644 --- a/static/app/views/explore/tables/fieldRenderer.tsx +++ b/static/app/views/explore/tables/fieldRenderer.tsx @@ -22,6 +22,7 @@ import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; import {isValidUrl} from 'sentry/utils/string/isValidUrl'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -36,10 +37,7 @@ import { useQueryParamsQuery, useSetQueryParamsQuery, } from 'sentry/views/explore/queryParams/context'; -import { - getSimilarEventsUrl, - isPartialSpanOrTraceData, -} from 'sentry/views/explore/tables/tracesTable/utils'; +import {getSimilarEventsUrl} from 'sentry/views/explore/tables/tracesTable/utils'; import {SpanFields} from 'sentry/views/insights/types'; import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; diff --git a/static/app/views/explore/tables/tracesTable/utils.tsx b/static/app/views/explore/tables/tracesTable/utils.tsx index e0d110be469053..1b5fd84eacdf5b 100644 --- a/static/app/views/explore/tables/tracesTable/utils.tsx +++ b/static/app/views/explore/tables/tracesTable/utils.tsx @@ -1,5 +1,3 @@ -import moment from 'moment-timezone'; - import type {PageFilters} from 'sentry/types/core'; import type {Organization} from 'sentry/types/organization'; import {Mode} from 'sentry/views/explore/queryParams/mode'; @@ -35,11 +33,7 @@ export function getShortenedSdkName(sdkName: string | null) { return sdkNameParts[sdkNameParts.length - 1]; } -export function isPartialSpanOrTraceData(timestamp: string | number) { - const now = moment(); - const timestampDate = moment(timestamp); - return now.diff(timestampDate, 'days') > 30; -} +export {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; interface GetSimilarEventsUrlArgs { organization: Organization; From 4409cec17ff1e109e7be3900480ff1777fcb9620 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 13:59:45 -0300 Subject: [PATCH 02/39] feat(traces): Disable trace links older than 30 days on high-traffic surfaces Add 30-day age checks to trace and span links across issue details, discover, dashboards, logs, agents, transaction summary, and trace drawer. Links to traces older than 30 days now render as disabled text with a tooltip instead of broken clickable links. Co-Authored-By: Claude Opus 4.6 --- .../events/contexts/knownContext/trace.tsx | 19 +++++ .../datasetConfig/errorsAndTransactions.tsx | 18 ++++- .../views/dashboards/datasetConfig/spans.tsx | 14 ++++ static/app/views/discover/table/tableView.tsx | 52 +++++++++---- .../app/views/explore/logs/fieldRenderers.tsx | 16 +++- .../components/tableCells/spanIdCell.tsx | 11 +++ .../pages/agents/components/tracesTable.tsx | 9 +++ .../issueDetails/traceTimeline/traceLink.tsx | 16 ++++ .../traceTimeline/traceTimelineTooltip.tsx | 74 ++++++++++++------- .../span/eapSections/traceSpanLinks.tsx | 9 +++ .../transactionEvents/eventsTable.tsx | 17 ++++- .../transactionTags/tagsHeatMap.tsx | 42 +++++++---- 12 files changed, 231 insertions(+), 66 deletions(-) diff --git a/static/app/components/events/contexts/knownContext/trace.tsx b/static/app/components/events/contexts/knownContext/trace.tsx index 62e6c5d34fcebf..b3af7f7f0ab28b 100644 --- a/static/app/components/events/contexts/knownContext/trace.tsx +++ b/static/app/components/events/contexts/knownContext/trace.tsx @@ -3,12 +3,14 @@ import type {Location} from 'history'; import {Tooltip} from '@sentry/scraps/tooltip'; import {getContextKeys} from 'sentry/components/events/contexts/utils'; +import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import type {KeyValueListData} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils'; enum TraceContextKeys { @@ -66,6 +68,23 @@ export function getTraceContextData({ const traceWasSampled = data?.sampled ?? true; if (traceWasSampled) { + const eventTimestamp = getEventTimestampInSeconds(event); + const isOld = eventTimestamp + ? isPartialSpanOrTraceData(eventTimestamp) + : false; + + if (isOld) { + return { + key: ctxKey, + subject: t('Trace ID'), + value: ( + + {traceId} + + ), + }; + } + const link = generateTraceTarget(event, organization, location); const hasPerformanceView = organization.features.includes('performance-view'); diff --git a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx index e2ffcfd7ff1d2f..201e13d0222619 100644 --- a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx +++ b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx @@ -2,6 +2,7 @@ import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {doEventsRequest} from 'sentry/actionCreators/events'; @@ -44,6 +45,7 @@ import { import {getShortEventId} from 'sentry/utils/events'; import {FieldKey} from 'sentry/utils/fields'; import {getMeasurements} from 'sentry/utils/measurements/measurements'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import type {DatasetConfig} from 'sentry/views/dashboards/datasetConfig/base'; import {handleOrderByReset} from 'sentry/views/dashboards/datasetConfig/base'; import type {DashboardFilters, Widget, WidgetQuery} from 'sentry/views/dashboards/types'; @@ -354,12 +356,26 @@ export function renderTraceAsLinkable(widget?: Widget) { if (!eventView || typeof id !== 'string') { return {emptyStringValue}; } + const traceTimestamp = getTimeStampFromTableDateField( + data['max(timestamp)'] ?? data.timestamp + ); + + if (traceTimestamp && isPartialSpanOrTraceData(traceTimestamp)) { + return ( + + + {getShortEventId(id)} + + + ); + } + const dateSelection = eventView.normalizeDateSelection(location); const target = getTraceDetailsUrl({ organization, traceSlug: String(data.trace), dateSelection, - timestamp: getTimeStampFromTableDateField(data['max(timestamp)'] ?? data.timestamp), + timestamp: traceTimestamp, location: widget ? { ...location, diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index 9eebcf9027ae04..99fd1370391a9f 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -1,7 +1,10 @@ import pickBy from 'lodash/pickBy'; import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; +import {Tooltip} from '@sentry/scraps/tooltip'; +import {t} from 'sentry/locale'; import type {TagCollection} from 'sentry/types/group'; import type { EventsStats, @@ -30,6 +33,7 @@ import { NO_ARGUMENT_SPAN_AGGREGATES, } from 'sentry/utils/fields'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useOrganization} from 'sentry/utils/useOrganization'; import { handleOrderByReset, @@ -428,6 +432,16 @@ function renderEventInTraceView( return {getShortEventId(spanId)}; } + if (data.timestamp && isPartialSpanOrTraceData(data.timestamp)) { + return ( + + + {getShortEventId(spanId)} + + + ); + } + const target = generateLinkToEventInTraceView({ traceSlug: data.trace, timestamp: data.timestamp, diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index 3c39f68ebdac7d..c85c5b177fe6ab 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -6,6 +6,7 @@ import * as Sentry from '@sentry/react'; import type {Location, LocationDescriptorObject} from 'history'; import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {openModal} from 'sentry/actionCreators/modal'; @@ -46,6 +47,7 @@ import {getShortEventId} from 'sentry/utils/events'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {decodeList} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useProjects} from 'sentry/utils/useProjects'; @@ -200,6 +202,18 @@ export function TableView(props: TableViewProps) { ); } + if (dataRow.timestamp && isPartialSpanOrTraceData(dataRow.timestamp)) { + return [ + + {value} + , + ]; + } + target = generateLinkToEventInTraceView({ traceSlug: dataRow.trace, eventId: dataRow.id, @@ -385,22 +399,30 @@ export function TableView(props: TableViewProps) { ); const dateSelection = eventView.normalizeDateSelection(location); if (dataRow.trace) { - const target = getTraceDetailsUrl({ - organization, - traceSlug: String(dataRow.trace), - dateSelection, - timestamp, - location, - source: TraceViewSources.DISCOVER, - }); + if (timestamp && isPartialSpanOrTraceData(timestamp)) { + cell = ( + + {cell} + + ); + } else { + const target = getTraceDetailsUrl({ + organization, + traceSlug: String(dataRow.trace), + dateSelection, + timestamp, + location, + source: TraceViewSources.DISCOVER, + }); - cell = ( - - - {cell} - - - ); + cell = ( + + + {cell} + + + ); + } } } else if (columnKey === 'replayId') { if (dataRow.replayId) { diff --git a/static/app/views/explore/logs/fieldRenderers.tsx b/static/app/views/explore/logs/fieldRenderers.tsx index 88b9d487449705..d4a48669c0e474 100644 --- a/static/app/views/explore/logs/fieldRenderers.tsx +++ b/static/app/views/explore/logs/fieldRenderers.tsx @@ -9,6 +9,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {DateTime} from 'sentry/components/dateTime'; import {Duration} from 'sentry/components/duration/duration'; import {useStacktraceLink} from 'sentry/components/events/interfaces/frame/useStacktraceLink'; +import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; import {Version} from 'sentry/components/version'; import {IconPlay} from 'sentry/icons'; import {tct} from 'sentry/locale'; @@ -23,6 +24,7 @@ import {type ColumnValueType} from 'sentry/utils/discover/fields'; import {VersionContainer} from 'sentry/utils/discover/styles'; import {ViewReplayLink} from 'sentry/utils/discover/viewReplayLink'; import {getShortEventId} from 'sentry/utils/events'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useRelease} from 'sentry/utils/useRelease'; import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper'; @@ -404,12 +406,18 @@ function TraceIDRenderer(props: LogFieldRendererProps) { const traceId = adjustLogTraceID(props.item.value as string); const location = stripLogParamsFromLocation(props.extra.location); const timestamp = props.extra.attributes?.[OurLogKnownFieldKey.TIMESTAMP]; + const validTimestamp = + typeof timestamp === 'string' || typeof timestamp === 'number' + ? timestamp + : undefined; + + if (validTimestamp && isPartialSpanOrTraceData(validTimestamp)) { + return {props.basicRendered}; + } + const target = getTraceDetailsUrl({ traceSlug: traceId, - timestamp: - typeof timestamp === 'string' || typeof timestamp === 'number' - ? timestamp - : undefined, + timestamp: validTimestamp, organization: props.extra.organization, dateSelection: props.extra.location, location, diff --git a/static/app/views/insights/common/components/tableCells/spanIdCell.tsx b/static/app/views/insights/common/components/tableCells/spanIdCell.tsx index 8cdb6c85298a4d..8ac95ada4d3f3a 100644 --- a/static/app/views/insights/common/components/tableCells/spanIdCell.tsx +++ b/static/app/views/insights/common/components/tableCells/spanIdCell.tsx @@ -2,8 +2,10 @@ import type {Location} from 'history'; import {Link} from '@sentry/scraps/link'; +import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; import {trackAnalytics} from 'sentry/utils/analytics'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SPAN_ID_DISPLAY_LENGTH} from 'sentry/views/insights/http/settings'; @@ -34,6 +36,15 @@ export function SpanIdCell({ }: Props) { const organization = useOrganization(); const domainViewFilters = useDomainViewFilters(); + + if (isPartialSpanOrTraceData(timestamp)) { + return ( + + {spanId.slice(0, SPAN_ID_DISPLAY_LENGTH)} + + ); + } + const url = normalizeUrl( generateLinkToEventInTraceView({ eventId: transactionId, diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index 64af2928fa5998..ceb9d6c8f4373e 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -9,6 +9,7 @@ import {Link} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {Pagination} from 'sentry/components/pagination'; @@ -23,6 +24,7 @@ import {useStateBasedColumnResize} from 'sentry/components/tables/gridEditable/u import {TimeSince} from 'sentry/components/timeSince'; import {IconArrow} from 'sentry/icons'; import {t} from 'sentry/locale'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {isOverflown} from 'sentry/utils/useHoverOverlay'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -351,6 +353,13 @@ const BodyCell = memo(function BodyCell({ switch (column.key) { case 'traceId': if (linkToTraceView || !openTraceViewDrawer) { + if (isPartialSpanOrTraceData(dataRow.timestamp)) { + return ( + + {dataRow.traceId.slice(0, 8)} + + ); + } const traceUrl = getTraceDetailsUrl({ organization, traceSlug: dataRow.traceId, diff --git a/static/app/views/issueDetails/traceTimeline/traceLink.tsx b/static/app/views/issueDetails/traceTimeline/traceLink.tsx index 31fb27b3472505..a6ee290e756162 100644 --- a/static/app/views/issueDetails/traceTimeline/traceLink.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceLink.tsx @@ -1,14 +1,18 @@ import styled from '@emotion/styled'; import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; +import {Tooltip} from '@sentry/scraps/tooltip'; import {useAnalyticsArea} from 'sentry/components/analyticsArea'; +import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; import {QuestionTooltip} from 'sentry/components/questionTooltip'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import {IconChevron} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; @@ -53,6 +57,18 @@ export function TraceLink({event}: TraceLinkProps) { ); } + const eventTimestamp = getEventTimestampInSeconds(event); + if (eventTimestamp && isPartialSpanOrTraceData(eventTimestamp)) { + return ( + + + {t('View Full Trace')} + + + + ); + } + return ( {filteredTimelineEvents.length > 3 && ( - { - if (area.startsWith('issue_details')) { - // Track this event for backwards compatibility. TODO: remove after issues team dashboards/queries are migrated - trackAnalytics( - 'issue_details.issue_tab.trace_timeline_more_events_clicked', - { - organization, - num_hidden: filteredTimelineEvents.length - 3, - } - ); - } - trackAnalytics('trace_timeline_more_events_clicked', { - organization, - num_hidden: filteredTimelineEvents.length - 3, - area, - }); - }} - > - {tn( - 'View %s more event', - 'View %s more events', - filteredTimelineEvents.length - 3 - )} - + )} @@ -143,6 +119,48 @@ function EventItem({timelineEvent, location}: EventItemProps) { ); } +function MoreEventsLink({ + event, + filteredTimelineEvents, +}: { + event: Event; + filteredTimelineEvents: TimelineEvent[]; +}) { + const organization = useOrganization(); + const location = useLocation(); + const area = useAnalyticsArea(); + const eventTimestamp = getEventTimestampInSeconds(event); + + if (eventTimestamp && isPartialSpanOrTraceData(eventTimestamp)) { + return ( + + {tn('%s more event', '%s more events', filteredTimelineEvents.length - 3)} + + ); + } + + return ( + { + if (area.startsWith('issue_details')) { + trackAnalytics('issue_details.issue_tab.trace_timeline_more_events_clicked', { + organization, + num_hidden: filteredTimelineEvents.length - 3, + }); + } + trackAnalytics('trace_timeline_more_events_clicked', { + organization, + num_hidden: filteredTimelineEvents.length - 3, + area, + }); + }} + > + {tn('View %s more event', 'View %s more events', filteredTimelineEvents.length - 3)} + + ); +} + const UnstyledUnorderedList = styled('div')` display: flex; flex-direction: column; diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx index c36ae58e3e9827..01dc9a30b6fb44 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx @@ -8,6 +8,7 @@ import { } from 'sentry/utils/discover/fieldRenderers'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import type {Theme} from 'sentry/utils/theme'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useNavigate} from 'sentry/utils/useNavigate'; import { AttributesTree, @@ -81,6 +82,10 @@ export function TraceSpanLinks({ const prefix = `span_link_${linkIndex + 1}`; customRenderers[`${prefix}.trace_id`] = () => { + if (isPartialSpanOrTraceData(node.value.start_timestamp)) { + return {traceIdRenderer({trace: link.traceId}, renderBaggage)}; + } + const traceTarget = generateLinkToEventInTraceView({ organization, location, @@ -106,6 +111,10 @@ export function TraceSpanLinks({ }; customRenderers[`${prefix}.span_id`] = () => { + if (isPartialSpanOrTraceData(node.value.start_timestamp)) { + return {spanIdRenderer({span_id: link.itemId}, renderBaggage)}; + } + const spanTarget = generateLinkToEventInTraceView({ organization, location, diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 6f5a7478590da6..19c5eadef762a7 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -37,6 +37,7 @@ import {ViewReplayLink} from 'sentry/utils/discover/viewReplayLink'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; import {parseLinkHeader} from 'sentry/utils/parseLinkHeader'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useApi} from 'sentry/utils/useApi'; import {useNavigate} from 'sentry/utils/useNavigate'; import {Actions, CellAction, updateQuery} from 'sentry/views/discover/table/cellAction'; @@ -231,8 +232,12 @@ export function EventsTable({ if (field === 'id' || field === 'trace') { const isIssue = !!issueId; + const isOld = dataRow.timestamp && isPartialSpanOrTraceData(dataRow.timestamp); let target: LocationDescriptor | null = null; - if (isIssue && !isRegressionIssue && field === 'id') { + if (isOld) { + // Trace data older than 30 days is no longer available + target = null; + } else if (isIssue && !isRegressionIssue && field === 'id') { target = { pathname: `/organizations/${organization.slug}/issues/${issueId}/events/${dataRow.id}/`, }; @@ -261,7 +266,15 @@ export function EventsTable({ handleCellAction={cellActionHandler} allowActions={allowActions} > - {target ? {rendered} : rendered} + {target ? ( + {rendered} + ) : isOld ? ( + + {rendered} + + ) : ( + rendered + )} ); } diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx index ae7ef1f8ab7e56..cbc494aa146d3e 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx @@ -39,6 +39,7 @@ import type { } from 'sentry/utils/performance/segmentExplorer/tagKeyHistogramQuery'; import {TagTransactionsQuery} from 'sentry/utils/performance/segmentExplorer/tagTransactionsQuery'; import {decodeScalar} from 'sentry/utils/queryString'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useDomainViewFilters} from 'sentry/views/insights/pages/useFilters'; import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; import {Tab} from 'sentry/views/performance/transactionSummary/tabs'; @@ -355,24 +356,33 @@ export function TagsHeatMap(
{transactionTableData?.data.length ? null : } {[...(transactionTableData?.data ?? [])].slice(0, 3).map(row => { - const target = generateLinkToEventInTraceView({ - eventId: row.id, - traceSlug: row.trace?.toString()!, - timestamp: row.timestamp!, - location: { - ...location, - query: { - ...location.query, - tab: Tab.TAGS, - }, - }, - organization, - source: TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY, - view, - }); + const isOld = + row.timestamp && isPartialSpanOrTraceData(row.timestamp); + const target = isOld + ? null + : generateLinkToEventInTraceView({ + eventId: row.id, + traceSlug: row.trace?.toString()!, + timestamp: row.timestamp!, + location: { + ...location, + query: { + ...location.query, + tab: Tab.TAGS, + }, + }, + organization, + source: TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY, + view, + }); return ( - + From 7a47352e2c5199a0015eb0e0ced92162925ead43 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:01:37 -0300 Subject: [PATCH 03/39] feat(traces): Disable trace links older than 30 days on insights and samples surfaces Add 30-day age checks to trace links in insights modules (web vitals, mobile, agents), span samples tables, span summary sample list, metrics samples, and trace drawer span attributes. Co-Authored-By: Claude Opus 4.6 --- .../metricInfoTabs/metricsSamplesTableRow.tsx | 11 +++++++ .../pageOverviewWebVitalsDetailPanel.tsx | 3 ++ .../tables/pageSamplePerformanceTable.tsx | 31 +++++++++++++------ .../samplesTable/spanSamplesTable.tsx | 18 +++++++++++ .../spanSummaryPage/sampleList/index.tsx | 4 +++ .../components/spanSamplesPanelContainer.tsx | 4 +++ .../components/tables/eventSamplesTable.tsx | 8 +++++ .../pages/agents/components/drawer.tsx | 9 ++++-- .../pages/agents/hooks/useNodeDetailsLink.tsx | 11 +++++-- .../details/span/eapSections/attributes.tsx | 4 +++ 10 files changed, 89 insertions(+), 14 deletions(-) diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx index 73c2fc349ecdd5..a23c11f1223ce1 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx @@ -16,6 +16,7 @@ import type {EventsMetaType} from 'sentry/utils/discover/eventView'; import type {ColumnValueType} from 'sentry/utils/discover/fields'; import {getShortEventId} from 'sentry/utils/events'; import {FieldValueType} from 'sentry/utils/fields'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -149,6 +150,16 @@ export function SampleTableRow({ const spanIdToUse = oldSpanId || spanId; const strippedLocation = stripMetricParamsFromLocation(location); + if (timestamp && isPartialSpanOrTraceData(timestamp)) { + return ( + + + {getShortEventId(traceId)} + + + ); + } + const hasSpans = (telemetry?.spansCount ?? 0) > 0; const shouldGoToSpans = spanIdToUse && hasSpans; diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index 23d6181383d7ae..e70567d1a82212 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -21,6 +21,7 @@ import {PageAlert, PageAlertProvider} from 'sentry/utils/performance/contexts/pa import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {useReplayExists} from 'sentry/utils/replayCount/useReplayExists'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -282,7 +283,9 @@ export function PageOverviewWebVitalsDetailPanel({ return {NO_VALUE}; } if (key === 'id') { + const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); const eventTarget = + !isOld && project?.slug && generateLinkToEventInTraceView({ eventId: row.id, diff --git a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx index 68de04da3628be..ca5d670151a95b 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx @@ -31,6 +31,7 @@ import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {decodeList, decodeScalar} from 'sentry/utils/queryString'; import {useReplayExists} from 'sentry/utils/replayCount/useReplayExists'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -478,21 +479,31 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro } if (key === 'id' || key === SpanFields.SPAN_DESCRIPTION) { - const traceViewLink = generateLinkToEventInTraceView({ - traceSlug: row.trace, - eventId: row.id, - timestamp: row.timestamp, - organization, - location, - view: domainViewFilters.view, - source: TraceViewSources.WEB_VITALS_MODULE, - }); + const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); + const traceViewLink = isOld + ? null + : generateLinkToEventInTraceView({ + traceSlug: row.trace, + eventId: row.id, + timestamp: row.timestamp, + organization, + location, + view: domainViewFilters.view, + source: TraceViewSources.WEB_VITALS_MODULE, + }); if (key === 'id' && 'id' in row) { + if (isOld) { + return ( + + {getShortEventId(row.trace)} + + ); + } return ( - {getShortEventId(row.trace)} + {getShortEventId(row.trace)} ); diff --git a/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx b/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx index cf293cb0f1f086..56dc858e5d3231 100644 --- a/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx +++ b/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx @@ -4,6 +4,7 @@ import {LinkButton} from '@sentry/scraps/button'; import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; import type {GridColumnHeader} from 'sentry/components/tables/gridEditable'; import {COL_WIDTH_UNDEFINED, GridEditable} from 'sentry/components/tables/gridEditable'; import {IconProfiling} from 'sentry/icons/iconProfiling'; @@ -14,6 +15,7 @@ import { generateContinuousProfileFlamechartRouteWithQuery, generateProfileFlamechartRoute, } from 'sentry/utils/profiling/routes'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {DurationComparisonCell} from 'sentry/views/insights/common/components/samplesTable/common'; @@ -126,6 +128,15 @@ export function SpanSamplesTable({ row: SpanTableRow ): React.ReactNode { if (column.key === 'transaction_id') { + if (isPartialSpanOrTraceData(row.timestamp)) { + return ( + + + {row['transaction.span_id'].slice(0, 8)} + + + ); + } return ( + {row.span_id} + + ); + } return ( { + if (isPartialSpanOrTraceData(span.timestamp)) { + return; + } navigate( generateLinkToEventInTraceView({ targetId: span['transaction.span_id'], diff --git a/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.tsx b/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.tsx index b253ab85b48435..3665e736760fa2 100644 --- a/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.tsx +++ b/static/app/views/insights/mobile/common/components/spanSamplesPanelContainer.tsx @@ -13,6 +13,7 @@ import {DurationUnit} from 'sentry/utils/discover/fields'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -154,6 +155,9 @@ export function SpanSamplesContainer({ const handleClickSample = useCallback( (span: SpanSample) => { + if (isPartialSpanOrTraceData(span.timestamp)) { + return; + } navigate( generateLinkToEventInTraceView({ targetId: span['transaction.span_id'], diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx index 4147100c30cc2d..ebcf0a4cdc5dcb 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx @@ -26,6 +26,7 @@ import type {Sort} from 'sentry/utils/discover/fields'; import {fieldAlignment} from 'sentry/utils/discover/fields'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -75,6 +76,13 @@ export function EventSamplesTable({ } if (column.key === eventIdKey) { + if (row.timestamp && isPartialSpanOrTraceData(row.timestamp)) { + return ( + + {row[eventIdKey].slice(0, 8)} + + ); + } return ( node.id === selectedNodeKey)) || getDefaultSelectedNode(nodes); - const nodeDetailsLink = useNodeDetailsLink({ + const {url: nodeDetailsLink, isDisabled: isTraceLinkDisabled} = useNodeDetailsLink({ node: selectedNode, traceSlug, source: TraceViewSources.AGENT_MONITORING, @@ -79,7 +79,12 @@ const TraceViewDrawer = memo(function TraceViewDrawer({ {t('Abbreviated Trace')} - + {t('View in Full Trace')} diff --git a/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx b/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx index 872bc19054585c..0f135a1d0fec93 100644 --- a/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx +++ b/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx @@ -1,5 +1,8 @@ +import type {LocationDescriptorObject} from 'history'; + import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import type {AITraceSpanNode} from 'sentry/views/insights/pages/agents/utils/types'; @@ -14,7 +17,7 @@ export function useNodeDetailsLink({ node: AITraceSpanNode | undefined; source: TraceViewSources; traceSlug: string; -}) { +}): {isDisabled: boolean; url: LocationDescriptorObject} { const organization = useOrganization(); const {selection} = usePageFilters(); const location = useLocation(); @@ -23,7 +26,9 @@ export function useNodeDetailsLink({ const targetId = node?.transactionId; const timestamp = node?.startTimestamp; - return getTraceDetailsUrl({ + const isDisabled = timestamp ? isPartialSpanOrTraceData(timestamp) : false; + + const url = getTraceDetailsUrl({ source, organization, location: { @@ -37,4 +42,6 @@ export function useNodeDetailsLink({ timestamp, dateSelection: normalizeDateTimeParams(selection), }); + + return {url, isDisabled}; } diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx index 195038a93bba35..2a7de6e96a9f48 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx @@ -22,6 +22,7 @@ import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {ellipsize} from 'sentry/utils/string/ellipsize'; import {looksLikeAJSONArray} from 'sentry/utils/string/looksLikeAJSONArray'; import {looksLikeAJSONObject} from 'sentry/utils/string/looksLikeAJSONObject'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {AssertionFailureTree} from 'sentry/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree'; import type {AttributesFieldRendererProps} from 'sentry/views/explore/components/traceItemAttributes/attributesTree'; @@ -137,6 +138,9 @@ export function Attributes({ ); }, [FieldKey.TRACE]: (props: CustomRenderersProps) => { + if (isPartialSpanOrTraceData(node.value.start_timestamp)) { + return {props.item.value}; + } const traceSlug = String(props.item.value); const target = getTraceDetailsUrl({ organization, From 06a3ac6947dd5797adfa7224672cc8bef4aa23dd Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:02:26 -0300 Subject: [PATCH 04/39] feat(traces): Disable trace links older than 30 days on profiling and event surfaces Add 30-day age checks to trace links in profiling headers, flamegraph, profile events table, span evidence key-value list, profile event evidence, and statistical detector event display. Co-Authored-By: Claude Opus 4.6 --- .../eventComparison/eventDisplay.tsx | 46 +++++++++++++++---- .../performance/spanEvidenceKeyValueList.tsx | 28 ++++++++--- .../events/profileEventEvidence.tsx | 34 +++++++++----- .../profiling/continuousProfileHeader.tsx | 20 ++++++-- .../flamegraphDrawer/profileDetails.tsx | 14 +++--- .../profiling/flamegraph/flamegraphSpans.tsx | 6 +++ .../profiling/profileEventsTable.tsx | 10 ++++ .../components/profiling/profileHeader.tsx | 20 ++++++-- 8 files changed, 140 insertions(+), 38 deletions(-) diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx index c52241a8b18cb6..a2e916665326e5 100644 --- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx @@ -34,6 +34,7 @@ import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; import {useApiQuery} from 'sentry/utils/queryClient'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -208,6 +209,7 @@ function EventDisplay({ const waterfallModel = new WaterfallModel(eventData); const traceSlug = eventData.contexts?.trace?.trace_id ?? ''; + const isOld = isPartialSpanOrTraceData(eventData.endTimestamp); const fullEventTarget = generateLinkToEventInTraceView({ eventId: eventData.id, traceSlug, @@ -245,13 +247,19 @@ function EventDisplay({ )} /> - } - /> + + } + /> +
@@ -279,7 +287,7 @@ function EventDisplay({
- + {isOld ? ( - + ) : ( + + + + + + + + )} diff --git a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx index 670007744daf49..e9706d41ea2275 100644 --- a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx +++ b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx @@ -24,6 +24,7 @@ import { getSpanSubTimings, SpanSubTimingName, } from 'sentry/components/events/interfaces/spans/utils'; +import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; import {t} from 'sentry/locale'; import type {Entry, EntryRequest, Event, EventTransaction} from 'sentry/types/event'; @@ -40,6 +41,7 @@ import {formatBytesBase2} from 'sentry/utils/bytes/formatBytesBase2'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {toRoundedPercent} from 'sentry/utils/number/toRoundedPercent'; import {SQLishFormatter} from 'sentry/utils/sqlish'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {safeURL} from 'sentry/utils/url/safeURL'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -374,10 +376,17 @@ function AIDetectedSpanEvidence({ organization, }); + const eventTimestamp = getEventTimestampInSeconds(event); + const isOld = eventTimestamp ? isPartialSpanOrTraceData(eventTimestamp) : false; const actionButton = projectSlug ? ( - - {t('View Full Trace')} - + + + {t('View Full Trace')} + + ) : undefined; const transactionRow = makeRow( @@ -616,10 +625,17 @@ const makeTransactionNameRow = ( organization, }); + const eventTimestamp = getEventTimestampInSeconds(event); + const isOld = eventTimestamp ? isPartialSpanOrTraceData(eventTimestamp) : false; const actionButton = projectSlug ? ( - - {t('View Full Trace')} - + + + {t('View Full Trace')} + + ) : undefined; return makeRow( diff --git a/static/app/components/events/profileEventEvidence.tsx b/static/app/components/events/profileEventEvidence.tsx index 6acbfbb5bc1d3d..45375ba38f1a03 100644 --- a/static/app/components/events/profileEventEvidence.tsx +++ b/static/app/components/events/profileEventEvidence.tsx @@ -1,4 +1,5 @@ import {LinkButton} from '@sentry/scraps/button'; +import {Tooltip} from '@sentry/scraps/tooltip'; import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; import {IconProfiling} from 'sentry/icons'; @@ -6,6 +7,7 @@ import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {generateProfileFlamechartRouteWithHighlightFrame} from 'sentry/utils/profiling/routes'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; @@ -19,6 +21,7 @@ export function ProfileEventEvidence({event, projectSlug}: ProfileEvidenceProps) const evidenceData = event.occurrence?.evidenceData ?? {}; const evidenceDisplay = event.occurrence?.evidenceDisplay ?? []; const traceSlug = event.contexts?.trace?.trace_id ?? ''; + const isOld = isPartialSpanOrTraceData(evidenceData.timestamp); const keyValueListData = [ ...(evidenceData.transactionId && evidenceData.transactionName @@ -28,18 +31,27 @@ export function ProfileEventEvidence({event, projectSlug}: ProfileEvidenceProps) key: 'Transaction Name', value: evidenceData.transactionName, actionButton: traceSlug ? ( - - {t('View Transaction')} - + + {t('View Transaction')} + + ) : null, }, ] diff --git a/static/app/components/profiling/continuousProfileHeader.tsx b/static/app/components/profiling/continuousProfileHeader.tsx index 0c3efebf108d21..4159c0ff64aca7 100644 --- a/static/app/components/profiling/continuousProfileHeader.tsx +++ b/static/app/components/profiling/continuousProfileHeader.tsx @@ -2,6 +2,7 @@ import {useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; +import {Tooltip} from '@sentry/scraps/tooltip'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import * as Layout from 'sentry/components/layouts/thirds'; @@ -11,6 +12,7 @@ import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import {trackAnalytics} from 'sentry/utils/analytics'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -27,6 +29,8 @@ export function ContinuousProfileHeader({transaction}: ContinuousProfileHeader) return [{type: 'landing', payload: {query: {}}}]; }, []); + const isOld = isPartialSpanOrTraceData(transaction?.endTimestamp); + const transactionTarget = transaction?.id ? generateLinkToEventInTraceView({ timestamp: transaction.endTimestamp ?? '', @@ -53,9 +57,19 @@ export function ContinuousProfileHeader({transaction}: ContinuousProfileHeader) {transactionTarget && ( - - {t('Go to Trace')} - + + + {t('Go to Trace')} + + )} diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index 0c6b76abea22a5..09b2b62e0845a8 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -19,6 +19,7 @@ import type {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flam import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences'; import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; import {makeFormatter} from 'sentry/utils/profiling/units/units'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -283,11 +284,12 @@ function TransactionEventDetails({ { key: 'transaction', label: t('Transaction'), - value: transactionTarget ? ( - {transaction.title} - ) : ( - transaction.title - ), + value: + transactionTarget && !isPartialSpanOrTraceData(transaction.endTimestamp) ? ( + {transaction.title} + ) : ( + transaction.title + ), }, { key: 'timestamp', @@ -410,7 +412,7 @@ function ProfileEventDetails({ organization, }) : null; - if (transactionTarget) { + if (transactionTarget && !isPartialSpanOrTraceData(transaction?.endTimestamp)) { return ( {label}: diff --git a/static/app/components/profiling/flamegraph/flamegraphSpans.tsx b/static/app/components/profiling/flamegraph/flamegraphSpans.tsx index e27993b61e988b..1f72f4c77ee95a 100644 --- a/static/app/components/profiling/flamegraph/flamegraphSpans.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphSpans.tsx @@ -26,6 +26,7 @@ import {SpanChartRenderer2D} from 'sentry/utils/profiling/renderers/spansRendere import {SpansTextRenderer} from 'sentry/utils/profiling/renderers/spansTextRenderer'; import type {SpanChart, SpanChartNode} from 'sentry/utils/profiling/spanChart'; import {Rect} from 'sentry/utils/profiling/speedscope'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; @@ -451,6 +452,11 @@ export function FlamegraphSpans({ return; } + if (isPartialSpanOrTraceData(node.span.timestamp)) { + addErrorMessage(t('Trace data is only available for the last 30 days')); + return; + } + const nodePath: Record = {}; if (node.span.op === 'transaction' && node.span.event_id) { diff --git a/static/app/components/profiling/profileEventsTable.tsx b/static/app/components/profiling/profileEventsTable.tsx index 23aa26d2648507..4ebe389dc9689e 100644 --- a/static/app/components/profiling/profileEventsTable.tsx +++ b/static/app/components/profiling/profileEventsTable.tsx @@ -28,6 +28,7 @@ import {getShortEventId} from 'sentry/utils/events'; import type {EventsResults} from 'sentry/utils/profiling/hooks/types'; import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {renderTableHead} from 'sentry/utils/profiling/tableRenderer'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -175,6 +176,10 @@ function ProfileEventsCell(props: ProfileEventsCellProps props.baggage.location ).normalizeDateSelection(props.baggage.location); + if (isPartialSpanOrTraceData(timestamp)) { + return {getShortEventId(traceId)}; + } + return ( (props: ProfileEventsCellProps return {transactionId}; } + const txTimestamp = getTimeStampFromTableDateField(props.dataRow.timestamp); + if (isPartialSpanOrTraceData(txTimestamp)) { + return {transactionId}; + } + return ( {transactionTarget && ( - - {t('Go to Trace')} - + + + {t('Go to Trace')} + + )} From 67b2e74aeab7a00ed7d124d1d768c1e6c5e0e615 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:07:44 -0300 Subject: [PATCH 05/39] ref(traces): Remove unused export from TRACE_DATA_RETENTION_DAYS The constant is only used internally within the module. Removing the export fixes the knip unused-export warning. Co-Authored-By: Claude Opus 4.6 --- static/app/utils/trace/isOlderThan30Days.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx index d8411c6886f2be..70544cd6495841 100644 --- a/static/app/utils/trace/isOlderThan30Days.tsx +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -1,6 +1,6 @@ import moment from 'moment-timezone'; -export const TRACE_DATA_RETENTION_DAYS = 30; +const TRACE_DATA_RETENTION_DAYS = 30; /** * Returns true if the given timestamp is older than 30 days, indicating From 36d92f8c6a10d707f06b9b7f0236e3c0e1f28589 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:12:25 -0300 Subject: [PATCH 06/39] fix(traces): Resolve tsgo type errors in trace utilities Accept undefined in isPartialSpanOrTraceData to match callers that pass number | undefined timestamps. Add missing disabled prop to DropdownItemProps in tagsHeatMap and pass it through to MenuItem. Co-Authored-By: Claude Opus 4.6 --- static/app/utils/trace/isOlderThan30Days.tsx | 7 ++++++- .../transactionSummary/transactionTags/tagsHeatMap.tsx | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx index 70544cd6495841..37cdcf3fe05ac4 100644 --- a/static/app/utils/trace/isOlderThan30Days.tsx +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -8,7 +8,12 @@ const TRACE_DATA_RETENTION_DAYS = 30; * * Handles timestamps in seconds, milliseconds, or ISO string format. */ -export function isPartialSpanOrTraceData(timestamp: string | number): boolean { +export function isPartialSpanOrTraceData( + timestamp: string | number | undefined +): boolean { + if (timestamp === undefined) { + return false; + } const now = moment(); // Numbers < 1e12 are likely seconds (epoch), not milliseconds. // e.g. 1712002518 is seconds, 1712002518000 is milliseconds. diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx index cbc494aa146d3e..a4f9609d2550ba 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx @@ -478,6 +478,7 @@ const StyledMenuItem = styled(MenuItem)<{width: 'small' | 'large'}>` type DropdownItemProps = { children: React.ReactNode; allowDefaultEvent?: boolean; + disabled?: boolean; onSelect?: (eventKey: any) => void; to?: LocationDescriptor; width?: 'small' | 'large'; @@ -487,6 +488,7 @@ function DropdownItem({ children, onSelect, allowDefaultEvent, + disabled, to, width = 'large', }: DropdownItemProps) { @@ -496,6 +498,7 @@ function DropdownItem({ to={to} onSelect={onSelect} width={width} + disabled={disabled} allowDefaultEvent={allowDefaultEvent} > From 50bface06c9ea1cd5a11afb3f3c0f2e4bfbe9d5b Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:15:53 -0300 Subject: [PATCH 07/39] fix(traces): Remove incorrect retention check from span links The isPartialSpanOrTraceData check was using the current span's timestamp to decide whether to disable links to other traces. Span links connect to external traces that may have completely different ages, so a recent span could correctly link to an old trace while an old span could incorrectly have its links disabled even when the target traces are still within retention. Since TraceItemResponseLink does not carry the linked trace's timestamp, we cannot determine the target's age. Remove the check so links are always clickable. Co-Authored-By: Claude Opus 4.6 --- .../details/span/eapSections/traceSpanLinks.tsx | 9 --------- 1 file changed, 9 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx index 01dc9a30b6fb44..c36ae58e3e9827 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx @@ -8,7 +8,6 @@ import { } from 'sentry/utils/discover/fieldRenderers'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import type {Theme} from 'sentry/utils/theme'; -import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useNavigate} from 'sentry/utils/useNavigate'; import { AttributesTree, @@ -82,10 +81,6 @@ export function TraceSpanLinks({ const prefix = `span_link_${linkIndex + 1}`; customRenderers[`${prefix}.trace_id`] = () => { - if (isPartialSpanOrTraceData(node.value.start_timestamp)) { - return {traceIdRenderer({trace: link.traceId}, renderBaggage)}; - } - const traceTarget = generateLinkToEventInTraceView({ organization, location, @@ -111,10 +106,6 @@ export function TraceSpanLinks({ }; customRenderers[`${prefix}.span_id`] = () => { - if (isPartialSpanOrTraceData(node.value.start_timestamp)) { - return {spanIdRenderer({span_id: link.itemId}, renderBaggage)}; - } - const spanTarget = generateLinkToEventInTraceView({ organization, location, From 6b54030c13173b82a2dc2b8c2bf66d6438bbe868 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:19:39 -0300 Subject: [PATCH 08/39] fix(traces): Remove retention check from trace attribute link in trace view The trace view already has the trace loaded, so the trace ID attribute link in the span details drawer should always be clickable. The isPartialSpanOrTraceData check was incorrectly disabling the link based on the current span's age. Co-Authored-By: Claude Opus 4.6 --- .../traceDrawer/details/span/eapSections/attributes.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx index 2a7de6e96a9f48..195038a93bba35 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx @@ -22,7 +22,6 @@ import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {ellipsize} from 'sentry/utils/string/ellipsize'; import {looksLikeAJSONArray} from 'sentry/utils/string/looksLikeAJSONArray'; import {looksLikeAJSONObject} from 'sentry/utils/string/looksLikeAJSONObject'; -import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {AssertionFailureTree} from 'sentry/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree'; import type {AttributesFieldRendererProps} from 'sentry/views/explore/components/traceItemAttributes/attributesTree'; @@ -138,9 +137,6 @@ export function Attributes({ ); }, [FieldKey.TRACE]: (props: CustomRenderersProps) => { - if (isPartialSpanOrTraceData(node.value.start_timestamp)) { - return {props.item.value}; - } const traceSlug = String(props.item.value); const target = getTraceDetailsUrl({ organization, From 047dda4d2a3da9ec39329d8568eb140e4e405cac Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:22:09 -0300 Subject: [PATCH 09/39] ref(traces): Deduplicate minimap JSX in event comparison display Extract the shared MinimapContainer block into a variable and conditionally wrap it with a Link, removing ~15 lines of duplicated JSX that could drift apart during future edits. Co-Authored-By: Claude Opus 4.6 --- .../eventComparison/eventDisplay.tsx | 66 ++++++++----------- 1 file changed, 27 insertions(+), 39 deletions(-) diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx index a2e916665326e5..03e40b9076c533 100644 --- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx @@ -217,6 +217,32 @@ function EventDisplay({ location, organization, }); + + const minimapContent = ( + + + + + + ); + const minimap = isOld ? ( + minimapContent + ) : ( + {minimapContent} + ); + return (
@@ -287,45 +313,7 @@ function EventDisplay({
- {isOld ? ( - - - - - - ) : ( - - - - - - - - )} + {minimap} From f1357256bd54b2fda831058af9059aabe9b6e551 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 14:26:27 -0300 Subject: [PATCH 10/39] ref(traces): Move DisabledTraceLink to components/explore Move DisabledTraceLink from components/links/ to components/explore/ where it better fits with other explore-related components. Co-Authored-By: Claude Opus 4.6 --- static/app/components/{links => explore}/disabledTraceLink.tsx | 0 static/app/views/explore/logs/fieldRenderers.tsx | 2 +- .../common/components/samplesTable/spanSamplesTable.tsx | 2 +- .../views/insights/common/components/tableCells/spanIdCell.tsx | 2 +- .../app/views/insights/pages/agents/components/tracesTable.tsx | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename static/app/components/{links => explore}/disabledTraceLink.tsx (100%) diff --git a/static/app/components/links/disabledTraceLink.tsx b/static/app/components/explore/disabledTraceLink.tsx similarity index 100% rename from static/app/components/links/disabledTraceLink.tsx rename to static/app/components/explore/disabledTraceLink.tsx diff --git a/static/app/views/explore/logs/fieldRenderers.tsx b/static/app/views/explore/logs/fieldRenderers.tsx index d4a48669c0e474..1711b637f43051 100644 --- a/static/app/views/explore/logs/fieldRenderers.tsx +++ b/static/app/views/explore/logs/fieldRenderers.tsx @@ -9,7 +9,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {DateTime} from 'sentry/components/dateTime'; import {Duration} from 'sentry/components/duration/duration'; import {useStacktraceLink} from 'sentry/components/events/interfaces/frame/useStacktraceLink'; -import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {Version} from 'sentry/components/version'; import {IconPlay} from 'sentry/icons'; import {tct} from 'sentry/locale'; diff --git a/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx b/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx index 56dc858e5d3231..8ca95e78499ff5 100644 --- a/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx +++ b/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx @@ -4,7 +4,7 @@ import {LinkButton} from '@sentry/scraps/button'; import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; -import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import type {GridColumnHeader} from 'sentry/components/tables/gridEditable'; import {COL_WIDTH_UNDEFINED, GridEditable} from 'sentry/components/tables/gridEditable'; import {IconProfiling} from 'sentry/icons/iconProfiling'; diff --git a/static/app/views/insights/common/components/tableCells/spanIdCell.tsx b/static/app/views/insights/common/components/tableCells/spanIdCell.tsx index 8ac95ada4d3f3a..f6ac6ce4ff7f38 100644 --- a/static/app/views/insights/common/components/tableCells/spanIdCell.tsx +++ b/static/app/views/insights/common/components/tableCells/spanIdCell.tsx @@ -2,7 +2,7 @@ import type {Location} from 'history'; import {Link} from '@sentry/scraps/link'; -import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {trackAnalytics} from 'sentry/utils/analytics'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index ceb9d6c8f4373e..29424000cfd36d 100644 --- a/static/app/views/insights/pages/agents/components/tracesTable.tsx +++ b/static/app/views/insights/pages/agents/components/tracesTable.tsx @@ -9,7 +9,7 @@ import {Link} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; -import {DisabledTraceLink} from 'sentry/components/links/disabledTraceLink'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {Pagination} from 'sentry/components/pagination'; From f28fcb4720b3ed09f92d45e064d863d17aa072f3 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 15:09:18 -0300 Subject: [PATCH 11/39] chore(explore): Add CODEOWNERS entry for components/explore Co-Authored-By: Claude Opus 4.6 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d1bfce0f68a086..d6d3392670098e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -340,6 +340,7 @@ tests/sentry/api/endpoints/test_organization_attribute_mappings.py @get /static/app/views/explore/logs/logsTabSeerComboBox.tsx @getsentry/explore @getsentry/machine-learning-ai /static/app/views/explore/spans/spansTabSeerComboBox.tsx @getsentry/explore @getsentry/machine-learning-ai /static/app/views/traces/ @getsentry/explore +/static/app/components/explore/ @getsentry/explore /static/app/components/quickTrace/ @getsentry/explore /static/app/components/dnd/ @getsentry/explore /src/sentry/insights/ @getsentry/data-browsing From e29aa026fee8fc49d0ef6130c7846957cdaa6833 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 15:11:40 -0300 Subject: [PATCH 12/39] fix(traces): Preserve issue event links for old trace data The isOld check ran before the isIssue check, preventing issue event links from being generated for events older than 30 days. Issue event links (/issues/{id}/events/{eventId}/) do not depend on trace data and should always be available regardless of age. Co-Authored-By: Claude Opus 4.6 --- .../transactionSummary/transactionEvents/eventsTable.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 19c5eadef762a7..8cf00cb8eccc71 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -234,13 +234,13 @@ export function EventsTable({ const isIssue = !!issueId; const isOld = dataRow.timestamp && isPartialSpanOrTraceData(dataRow.timestamp); let target: LocationDescriptor | null = null; - if (isOld) { - // Trace data older than 30 days is no longer available - target = null; - } else if (isIssue && !isRegressionIssue && field === 'id') { + if (isIssue && !isRegressionIssue && field === 'id') { target = { pathname: `/organizations/${organization.slug}/issues/${issueId}/events/${dataRow.id}/`, }; + } else if (isOld) { + // Trace data older than 30 days is no longer available + target = null; } else if (field === 'id') { target = generateLinkToEventInTraceView({ traceSlug: dataRow.trace?.toString()!, From 1c0e26c75766a1778bb6da2b88c7abd75025b4be Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 15:27:00 -0300 Subject: [PATCH 13/39] fix(traces): Add tooltip to disabled trace link in agents drawer The 'View in Full Trace' button was disabled for traces older than 30 days but provided no explanation to the user. Wrap the button in a Tooltip that shows 'Trace data is only available for the last 30 days', matching the pattern used in other disabled trace links in this PR. Co-Authored-By: Claude Sonnet 4.6 --- .../pages/agents/components/drawer.tsx | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/static/app/views/insights/pages/agents/components/drawer.tsx b/static/app/views/insights/pages/agents/components/drawer.tsx index 054c250743ad11..587781676d32cb 100644 --- a/static/app/views/insights/pages/agents/components/drawer.tsx +++ b/static/app/views/insights/pages/agents/components/drawer.tsx @@ -8,6 +8,7 @@ import {Flex, Stack} from '@sentry/scraps/layout'; import {EmptyMessage} from 'sentry/components/emptyMessage'; import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; +import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -79,14 +80,19 @@ const TraceViewDrawer = memo(function TraceViewDrawer({ {t('Abbreviated Trace')} - - {t('View in Full Trace')} - + + {t('View in Full Trace')} + + From 1814fe4132ad373cb5536f9af5d497e43448b931 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 15:30:57 -0300 Subject: [PATCH 14/39] ref(traces): Remove dead re-export and fix import paths Remove the isPartialSpanOrTraceData re-export from tracesTable/utils since fieldRenderers.tsx was still importing from that indirection. Update fieldRenderers.tsx to import directly from the canonical location (sentry/utils/trace/isOlderThan30Days) and fix the Tooltip import in drawer.tsx to use @sentry/scraps/tooltip. Co-Authored-By: Claude Sonnet 4.6 --- .../views/explore/tables/tracesTable/fieldRenderers.tsx | 8 ++------ static/app/views/explore/tables/tracesTable/utils.tsx | 2 -- .../app/views/insights/pages/agents/components/drawer.tsx | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx b/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx index 050f23c2788b01..fc2aafa7bb4568 100644 --- a/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx +++ b/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx @@ -21,6 +21,7 @@ import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; import {Projects} from 'sentry/utils/projects'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useProjects} from 'sentry/utils/useProjects'; @@ -32,12 +33,7 @@ import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHe import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; import type {Field} from './data'; -import { - getShortenedSdkName, - getSimilarEventsUrl, - getStylingSliceName, - isPartialSpanOrTraceData, -} from './utils'; +import {getShortenedSdkName, getSimilarEventsUrl, getStylingSliceName} from './utils'; export const ProjectBadgeWrapper = styled('span')` /** diff --git a/static/app/views/explore/tables/tracesTable/utils.tsx b/static/app/views/explore/tables/tracesTable/utils.tsx index 1b5fd84eacdf5b..d43f9bdaf5340e 100644 --- a/static/app/views/explore/tables/tracesTable/utils.tsx +++ b/static/app/views/explore/tables/tracesTable/utils.tsx @@ -33,8 +33,6 @@ export function getShortenedSdkName(sdkName: string | null) { return sdkNameParts[sdkNameParts.length - 1]; } -export {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; - interface GetSimilarEventsUrlArgs { organization: Organization; projectIds: number[]; diff --git a/static/app/views/insights/pages/agents/components/drawer.tsx b/static/app/views/insights/pages/agents/components/drawer.tsx index 587781676d32cb..291d9845ec14dc 100644 --- a/static/app/views/insights/pages/agents/components/drawer.tsx +++ b/static/app/views/insights/pages/agents/components/drawer.tsx @@ -4,11 +4,11 @@ import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; import {Flex, Stack} from '@sentry/scraps/layout'; +import {Tooltip} from '@sentry/scraps/tooltip'; import {EmptyMessage} from 'sentry/components/emptyMessage'; import {DrawerBody, DrawerHeader} from 'sentry/components/globalDrawer/components'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; -import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; import {trackAnalytics} from 'sentry/utils/analytics'; import {useOrganization} from 'sentry/utils/useOrganization'; From 16cd1bf2db43b4a939d8b838e8e23f1228db0d46 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 15:47:12 -0300 Subject: [PATCH 15/39] fix(traces): Add tooltips to disabled trace links missing explanations Two locations rendered plain text when trace data was older than 30 days, giving users no indication why the link was absent. Add a Tooltip with 'Trace is older than 30 days' to match the pattern used throughout the rest of this PR. Co-Authored-By: Claude Sonnet 4.6 --- static/app/components/profiling/profileEventsTable.tsx | 7 ++++++- .../components/pageOverviewWebVitalsDetailPanel.tsx | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/static/app/components/profiling/profileEventsTable.tsx b/static/app/components/profiling/profileEventsTable.tsx index 4ebe389dc9689e..ab7b7cd9848873 100644 --- a/static/app/components/profiling/profileEventsTable.tsx +++ b/static/app/components/profiling/profileEventsTable.tsx @@ -2,6 +2,7 @@ import {useCallback} from 'react'; import type {Location} from 'history'; import {Link} from '@sentry/scraps/link'; +import {Tooltip} from '@sentry/scraps/tooltip'; import {Count} from 'sentry/components/count'; import {DateTime} from 'sentry/components/dateTime'; @@ -177,7 +178,11 @@ function ProfileEventsCell(props: ProfileEventsCellProps ).normalizeDateSelection(props.baggage.location); if (isPartialSpanOrTraceData(timestamp)) { - return {getShortEventId(traceId)}; + return ( + + {getShortEventId(traceId)} + + ); } return ( diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index e70567d1a82212..3b37c4c92593d2 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -296,6 +296,13 @@ export function PageOverviewWebVitalsDetailPanel({ view: domainViewFilters.view, source: TraceViewSources.WEB_VITALS_MODULE, }); + if (isOld) { + return ( + + {getShortEventId(row.trace)} + + ); + } return ( {eventTarget ? ( From 842ae0e4d49c3ba64c77a0d5d2d8bb874fdd5b7e Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 16:03:16 -0300 Subject: [PATCH 16/39] ref(traces): Migrate simple disabled trace links to DisabledTraceLink Replace manual Tooltip + plain text patterns with the DisabledTraceLink component for consistency, accessibility (aria-disabled, role="link"), and muted text styling across 10 files. Co-Authored-By: Claude Sonnet 4.6 --- .../events/contexts/knownContext/trace.tsx | 7 ++----- .../flamegraphDrawer/profileDetails.tsx | 3 +++ .../profiling/profileEventsTable.tsx | 6 ++---- .../datasetConfig/errorsAndTransactions.tsx | 10 ++++------ static/app/views/discover/table/tableView.tsx | 18 +++++------------- .../metricInfoTabs/metricsSamplesTableRow.tsx | 5 ++--- .../pageOverviewWebVitalsDetailPanel.tsx | 5 ++--- .../tables/pageSamplePerformanceTable.tsx | 7 ++++--- .../components/tables/eventSamplesTable.tsx | 7 ++++--- .../transactionEvents/eventsTable.tsx | 5 ++--- 10 files changed, 30 insertions(+), 43 deletions(-) diff --git a/static/app/components/events/contexts/knownContext/trace.tsx b/static/app/components/events/contexts/knownContext/trace.tsx index b3af7f7f0ab28b..dffafeb542bb45 100644 --- a/static/app/components/events/contexts/knownContext/trace.tsx +++ b/static/app/components/events/contexts/knownContext/trace.tsx @@ -4,6 +4,7 @@ import {Tooltip} from '@sentry/scraps/tooltip'; import {getContextKeys} from 'sentry/components/events/contexts/utils'; import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; @@ -77,11 +78,7 @@ export function getTraceContextData({ return { key: ctxKey, subject: t('Trace ID'), - value: ( - - {traceId} - - ), + value: {traceId}, }; } diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index 09b2b62e0845a8..bbb1fbb7b3fb3b 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -7,6 +7,7 @@ import {Button} from '@sentry/scraps/button'; import {Link} from '@sentry/scraps/link'; import {DateTime} from 'sentry/components/dateTime'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {Version} from 'sentry/components/version'; import {t} from 'sentry/locale'; @@ -287,6 +288,8 @@ function TransactionEventDetails({ value: transactionTarget && !isPartialSpanOrTraceData(transaction.endTimestamp) ? ( {transaction.title} + ) : isPartialSpanOrTraceData(transaction.endTimestamp) ? ( + {transaction.title} ) : ( transaction.title ), diff --git a/static/app/components/profiling/profileEventsTable.tsx b/static/app/components/profiling/profileEventsTable.tsx index ab7b7cd9848873..bc53cae413b5ec 100644 --- a/static/app/components/profiling/profileEventsTable.tsx +++ b/static/app/components/profiling/profileEventsTable.tsx @@ -2,10 +2,10 @@ import {useCallback} from 'react'; import type {Location} from 'history'; import {Link} from '@sentry/scraps/link'; -import {Tooltip} from '@sentry/scraps/tooltip'; import {Count} from 'sentry/components/count'; import {DateTime} from 'sentry/components/dateTime'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {PerformanceDuration} from 'sentry/components/performanceDuration'; import type { @@ -179,9 +179,7 @@ function ProfileEventsCell(props: ProfileEventsCellProps if (isPartialSpanOrTraceData(timestamp)) { return ( - - {getShortEventId(traceId)} - + {getShortEventId(traceId)} ); } diff --git a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx index 201e13d0222619..ece2cd94791000 100644 --- a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx +++ b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx @@ -2,12 +2,12 @@ import styled from '@emotion/styled'; import * as Sentry from '@sentry/react'; import {Link} from '@sentry/scraps/link'; -import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {doEventsRequest} from 'sentry/actionCreators/events'; import type {ResponseMeta} from 'sentry/api'; import {isMultiSeriesStats} from 'sentry/components/charts/utils'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {t} from 'sentry/locale'; import type {SelectValue} from 'sentry/types/core'; import type {TagCollection} from 'sentry/types/group'; @@ -362,11 +362,9 @@ export function renderTraceAsLinkable(widget?: Widget) { if (traceTimestamp && isPartialSpanOrTraceData(traceTimestamp)) { return ( - - - {getShortEventId(id)} - - + + {getShortEventId(id)} + ); } diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index c85c5b177fe6ab..e43a55437bf93a 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -6,10 +6,10 @@ import * as Sentry from '@sentry/react'; import type {Location, LocationDescriptorObject} from 'history'; import {Link} from '@sentry/scraps/link'; -import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; import {openModal} from 'sentry/actionCreators/modal'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {COL_WIDTH_MINIMUM, GridEditable} from 'sentry/components/tables/gridEditable'; import {SortLink} from 'sentry/components/tables/gridEditable/sortLink'; import {useQueryBasedColumnResize} from 'sentry/components/tables/gridEditable/useQueryBasedColumnResize'; @@ -204,13 +204,9 @@ export function TableView(props: TableViewProps) { if (dataRow.timestamp && isPartialSpanOrTraceData(dataRow.timestamp)) { return [ - - {value} - , + + {value} + , ]; } @@ -400,11 +396,7 @@ export function TableView(props: TableViewProps) { const dateSelection = eventView.normalizeDateSelection(location); if (dataRow.trace) { if (timestamp && isPartialSpanOrTraceData(timestamp)) { - cell = ( - - {cell} - - ); + cell = {cell}; } else { const target = getTraceDetailsUrl({ organization, diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx index a23c11f1223ce1..6195139df343f6 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx @@ -7,6 +7,7 @@ import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Count} from 'sentry/components/count'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {IconChevron} from 'sentry/icons'; @@ -153,9 +154,7 @@ export function SampleTableRow({ if (timestamp && isPartialSpanOrTraceData(timestamp)) { return ( - - {getShortEventId(traceId)} - + {getShortEventId(traceId)} ); } diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index 3b37c4c92593d2..c3c2b7b205d462 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -5,6 +5,7 @@ import styled from '@emotion/styled'; import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {DrawerHeader} from 'sentry/components/globalDrawer/components'; import type { GridColumnHeader, @@ -298,9 +299,7 @@ export function PageOverviewWebVitalsDetailPanel({ }); if (isOld) { return ( - - {getShortEventId(row.trace)} - + {getShortEventId(row.trace)} ); } return ( diff --git a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx index ca5d670151a95b..900667b13f7eea 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx @@ -10,6 +10,7 @@ import {ExternalLink, Link} from '@sentry/scraps/link'; import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {Pagination} from 'sentry/components/pagination'; import {TransactionSearchQueryBuilder} from 'sentry/components/performance/transactionSearchQueryBuilder'; import type { @@ -495,9 +496,9 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro if (key === 'id' && 'id' in row) { if (isOld) { return ( - - {getShortEventId(row.trace)} - + + {getShortEventId(row.trace)} + ); } return ( diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx index ebcf0a4cdc5dcb..365d1c21428f11 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx @@ -6,6 +6,7 @@ import {LinkButton} from '@sentry/scraps/button'; import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import type {CursorHandler} from 'sentry/components/pagination'; import {Pagination} from 'sentry/components/pagination'; import type { @@ -78,9 +79,9 @@ export function EventSamplesTable({ if (column.key === eventIdKey) { if (row.timestamp && isPartialSpanOrTraceData(row.timestamp)) { return ( - - {row[eventIdKey].slice(0, 8)} - + + {row[eventIdKey].slice(0, 8)} + ); } return ( diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 8cf00cb8eccc71..2648c31c73f29c 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -10,6 +10,7 @@ import {LinkButton} from '@sentry/scraps/button'; import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {Pagination} from 'sentry/components/pagination'; import {QuestionTooltip} from 'sentry/components/questionTooltip'; import {GridEditable} from 'sentry/components/tables/gridEditable'; @@ -269,9 +270,7 @@ export function EventsTable({ {target ? ( {rendered} ) : isOld ? ( - - {rendered} - + {rendered} ) : ( rendered )} From ef0d6e0c6e6bb7ac3c66ad3479ec2f43108784a7 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 16:04:05 -0300 Subject: [PATCH 17/39] ref(traces): Migrate complex disabled trace links to DisabledTraceLink Replace manual Tooltip + tct + Link patterns with DisabledTraceLink's similarEventsUrl prop in the explore field renderers and dashboards spans config. Remove now-unused Text, tct, and t imports. Co-Authored-By: Claude Sonnet 4.6 --- .../views/dashboards/datasetConfig/spans.tsx | 12 +-- .../views/explore/tables/fieldRenderer.tsx | 82 ++++++------------ .../tables/tracesTable/fieldRenderers.tsx | 86 ++++++------------- 3 files changed, 57 insertions(+), 123 deletions(-) diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index 99fd1370391a9f..1181256da04d9d 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -1,10 +1,8 @@ import pickBy from 'lodash/pickBy'; import {Link} from '@sentry/scraps/link'; -import {Text} from '@sentry/scraps/text'; -import {Tooltip} from '@sentry/scraps/tooltip'; -import {t} from 'sentry/locale'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import type {TagCollection} from 'sentry/types/group'; import type { EventsStats, @@ -434,11 +432,9 @@ function renderEventInTraceView( if (data.timestamp && isPartialSpanOrTraceData(data.timestamp)) { return ( - - - {getShortEventId(spanId)} - - + + {getShortEventId(spanId)} + ); } diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx index ba1e747b2e1082..cfa9bf7234cc5d 100644 --- a/static/app/views/explore/tables/fieldRenderer.tsx +++ b/static/app/views/explore/tables/fieldRenderer.tsx @@ -4,13 +4,12 @@ import styled from '@emotion/styled'; import {Container as ScrapsContainer} from '@sentry/scraps/layout'; import {ExternalLink, Link} from '@sentry/scraps/link'; -import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {TimeSince} from 'sentry/components/timeSince'; -import {t, tct} from 'sentry/locale'; import type {Project} from 'sentry/types/project'; import {defined} from 'sentry/utils'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; @@ -168,36 +167,20 @@ function BaseExploreFieldRenderer({ } rendered = ( - - {tct( - 'Trace is older than 30 days. [similarTraces] in the past 24 hours.', - { - similarTraces: ( - - {t('View similar traces')} - - ), - } - )} - - } + - {rendered} - + {rendered} + ); } else { @@ -234,32 +217,19 @@ function BaseExploreFieldRenderer({ rendered = ( - - {tct('Span is older than 30 days. [similarSpans] in the past 24 hours.', { - similarSpans: ( - - {t('View similar spans')} - - ), - })} - - } + - {rendered} - + {rendered} + ); } else { diff --git a/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx b/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx index fc2aafa7bb4568..d67b7860f38512 100644 --- a/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx +++ b/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx @@ -6,16 +6,16 @@ import type {Location} from 'history'; import {Tag, type TagProps} from '@sentry/scraps/badge'; import {Container, Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; -import {Text} from '@sentry/scraps/text'; import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar'; import {pickBarColor} from 'sentry/components/performance/waterfall/utils'; import {PerformanceDuration} from 'sentry/components/performanceDuration'; import {TimeSince} from 'sentry/components/timeSince'; -import {t, tct, tn} from 'sentry/locale'; +import {t, tn} from 'sentry/locale'; import {defined} from 'sentry/utils'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {getShortEventId} from 'sentry/utils/events'; @@ -435,34 +435,21 @@ export function SpanIdRenderer({ } return ( - - {tct('Span is older than 30 days. [similarSpans] in the past 24 hours.', { - similarSpans: ( - - {t('View similar spans')} - - ), - })} - - } + - {shortSpanId} - + {shortSpanId} + ); } @@ -525,37 +512,18 @@ export function TraceIdRenderer({ } return ( - - {tct('Trace is older than 30 days. [similarTraces] in the past 24 hours.', { - similarTraces: ( - - {t('View similar traces')} - - ), - })} - - } + - - {props => ( - - {shortId} - - )} - - + {shortId} + ); } From 60ec9a6bd94154974ef946664ba27b175a3109dc Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Tue, 7 Apr 2026 16:04:40 -0300 Subject: [PATCH 18/39] test(traces): Update fieldRenderer tests for DisabledTraceLink DisabledTraceLink renders role="link" with aria-disabled="true", so update assertions to check for the disabled link element instead of asserting no link exists. Use specific link names to distinguish the disabled element from the "View similar" tooltip link. Co-Authored-By: Claude Sonnet 4.6 --- .../explore/tables/fieldRenderer.spec.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/static/app/views/explore/tables/fieldRenderer.spec.tsx b/static/app/views/explore/tables/fieldRenderer.spec.tsx index 6ca21d0d7df7f5..bc06a9760357b5 100644 --- a/static/app/views/explore/tables/fieldRenderer.spec.tsx +++ b/static/app/views/explore/tables/fieldRenderer.spec.tsx @@ -128,7 +128,7 @@ describe('FieldRenderer tests', () => { ); expect(screen.getByText('spanId')).toBeInTheDocument(); - expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveAttribute('aria-disabled', 'true'); await userEvent.hover(screen.getByText('spanId')); expect(await screen.findByText(/Span is older than 30 days/)).toBeInTheDocument(); @@ -136,7 +136,9 @@ describe('FieldRenderer tests', () => { const queryString = encodeURIComponent( 'span.name:"HTTP GET /foo" span.description:"GET /foo"' ); - expect(await screen.findByRole('link')).toHaveAttribute( + expect( + await screen.findByRole('link', {name: 'View similar spans'}) + ).toHaveAttribute( 'href', `/organizations/org-slug/explore/traces/?mode=samples&project=1&query=${queryString}&referrer=partial-trace&statsPeriod=24h` ); @@ -186,7 +188,7 @@ describe('FieldRenderer tests', () => { ); expect(screen.getByText('transactionId')).toBeInTheDocument(); - expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveAttribute('aria-disabled', 'true'); await userEvent.hover(screen.getByText('transactionId')); expect(await screen.findByText(/Span is older than 30 days/)).toBeInTheDocument(); @@ -194,7 +196,9 @@ describe('FieldRenderer tests', () => { const queryString = encodeURIComponent( 'is_transaction:true span.name:"HTTP GET /foo" span.description:"GET /foo"' ); - expect(await screen.findByRole('link')).toHaveAttribute( + expect( + await screen.findByRole('link', {name: 'View similar spans'}) + ).toHaveAttribute( 'href', `/organizations/org-slug/explore/traces/?mode=samples&project=1&query=${queryString}&referrer=partial-trace&statsPeriod=24h` ); @@ -244,7 +248,7 @@ describe('FieldRenderer tests', () => { ); expect(screen.getByText('traceId')).toBeInTheDocument(); - expect(screen.queryByRole('link')).not.toBeInTheDocument(); + expect(screen.getByRole('link')).toHaveAttribute('aria-disabled', 'true'); await userEvent.hover(screen.getByText('traceId')); expect(await screen.findByText(/Trace is older than 30 days/)).toBeInTheDocument(); @@ -252,7 +256,9 @@ describe('FieldRenderer tests', () => { const queryString = encodeURIComponent( 'span.name:"HTTP GET /foo" span.description:"GET /foo"' ); - expect(await screen.findByRole('link')).toHaveAttribute( + expect( + await screen.findByRole('link', {name: 'View similar traces'}) + ).toHaveAttribute( 'href', `/organizations/org-slug/explore/traces/?mode=samples&project=1&query=${queryString}&referrer=partial-trace&statsPeriod=24h&table=trace` ); From bf19f916a25dfdafc128146e493bd0c885e129e8 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 07:05:56 -0300 Subject: [PATCH 19/39] fix(traces): Only disable old trace links for event ID column The isOld check was incorrectly applied to both the 'id' and 'trace' columns. The 'id' column links to the trace waterfall which has 30-day retention, but the 'trace' column links to the transaction summary via generateTraceLink, which remains valid beyond 30 days. Scope the retention check to only the 'id' field. Co-Authored-By: Claude Opus 4.6 --- .../transactionSummary/transactionEvents/eventsTable.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 2648c31c73f29c..155740f24e0387 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -239,8 +239,8 @@ export function EventsTable({ target = { pathname: `/organizations/${organization.slug}/issues/${issueId}/events/${dataRow.id}/`, }; - } else if (isOld) { - // Trace data older than 30 days is no longer available + } else if (field === 'id' && isOld) { + // Trace waterfall data older than 30 days is no longer available target = null; } else if (field === 'id') { target = generateLinkToEventInTraceView({ From a1c9caa1d3c0a82359070d6c1ef868b4ff5edaf0 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 07:12:28 -0300 Subject: [PATCH 20/39] fix(traces): Fix minor bugs in disabled trace link rollout - Add validity check for unparseable timestamps in isPartialSpanOrTraceData - Eliminate duplicate isPartialSpanOrTraceData calls in profileDetails - Show event ID instead of trace ID in disabled links for the event ID column in web vitals tables Co-Authored-By: Claude Opus 4.6 --- .../flamegraphDrawer/profileDetails.tsx | 20 +++++++++++-------- static/app/utils/trace/isOlderThan30Days.tsx | 3 +++ .../pageOverviewWebVitalsDetailPanel.tsx | 2 +- .../tables/pageSamplePerformanceTable.tsx | 4 +--- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index bbb1fbb7b3fb3b..f1cbf50214b63b 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -285,14 +285,18 @@ function TransactionEventDetails({ { key: 'transaction', label: t('Transaction'), - value: - transactionTarget && !isPartialSpanOrTraceData(transaction.endTimestamp) ? ( - {transaction.title} - ) : isPartialSpanOrTraceData(transaction.endTimestamp) ? ( - {transaction.title} - ) : ( - transaction.title - ), + value: (() => { + const isOld = isPartialSpanOrTraceData(transaction.endTimestamp); + if (transactionTarget && !isOld) { + return {transaction.title}; + } + if (isOld) { + return ( + {transaction.title} + ); + } + return transaction.title; + })(), }, { key: 'timestamp', diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx index 37cdcf3fe05ac4..10bbbca589d91c 100644 --- a/static/app/utils/trace/isOlderThan30Days.tsx +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -20,5 +20,8 @@ export function isPartialSpanOrTraceData( const normalizedTimestamp = typeof timestamp === 'number' && timestamp < 1e12 ? timestamp * 1000 : timestamp; const timestampDate = moment(normalizedTimestamp); + if (!timestampDate.isValid()) { + return false; + } return now.diff(timestampDate, 'days') > TRACE_DATA_RETENTION_DAYS; } diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index c3c2b7b205d462..bfc06beaa72f9e 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -299,7 +299,7 @@ export function PageOverviewWebVitalsDetailPanel({ }); if (isOld) { return ( - {getShortEventId(row.trace)} + {getShortEventId(row.id)} ); } return ( diff --git a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx index 900667b13f7eea..defe48c9dbbc3c 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx @@ -496,9 +496,7 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro if (key === 'id' && 'id' in row) { if (isOld) { return ( - - {getShortEventId(row.trace)} - + {getShortEventId(row.id)} ); } return ( From 9c8af0a7e40049da24fbb2a838129f89d48ca91c Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 07:40:47 -0300 Subject: [PATCH 21/39] fix(traces): Disable trace column links for data older than 30 days The trace column was missing an isOld guard, so old trace data still rendered clickable links to broken waterfall pages. Add the same null-target pattern used by the id column so old traces show a DisabledTraceLink instead. Co-Authored-By: Claude Opus 4.6 --- .../transactionSummary/transactionEvents/eventsTable.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 155740f24e0387..83456d52fd5640 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -252,6 +252,8 @@ export function EventsTable({ source: TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY, view: domainViewFilters?.view, }); + } else if (field === 'trace' && isOld) { + target = null; } else if (dataRow.trace) { target = generateTraceLink(transactionName, domainViewFilters?.view)( organization, From 91414cea65bf482934ce9e114d4b80fb1f3291fd Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 07:44:37 -0300 Subject: [PATCH 22/39] ref(traces): Scope traceViewLink to the id branch in web vitals table Move isOld check and traceViewLink computation into the id key branch where they are actually used. The SPAN_DESCRIPTION branch never references traceViewLink, so computing it there was wasted work and required a non-null assertion. Co-Authored-By: Claude Opus 4.6 --- .../tables/pageSamplePerformanceTable.tsx | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx index defe48c9dbbc3c..ff76725af83924 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx @@ -480,29 +480,26 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro } if (key === 'id' || key === SpanFields.SPAN_DESCRIPTION) { - const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); - const traceViewLink = isOld - ? null - : generateLinkToEventInTraceView({ - traceSlug: row.trace, - eventId: row.id, - timestamp: row.timestamp, - organization, - location, - view: domainViewFilters.view, - source: TraceViewSources.WEB_VITALS_MODULE, - }); - if (key === 'id' && 'id' in row) { + const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); if (isOld) { return ( {getShortEventId(row.id)} ); } + const traceViewLink = generateLinkToEventInTraceView({ + traceSlug: row.trace, + eventId: row.id, + timestamp: row.timestamp, + organization, + location, + view: domainViewFilters.view, + source: TraceViewSources.WEB_VITALS_MODULE, + }); return ( - {getShortEventId(row.trace)} + {getShortEventId(row.trace)} ); From 3354073c8c1dcd276a0385f3a76887db4295dc86 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 08:03:17 -0300 Subject: [PATCH 23/39] fix(traces): Close old trace link regressions Use the event timestamp when profile evidence omits trace timing and block Discover transaction IDs once trace data ages out. Normalize numeric string timestamps so the 30-day guard behaves consistently. Co-Authored-By: GPT-5.4 Made-with: Cursor --- .../events/profileEventEvidence.spec.tsx | 46 ++++++++++- .../events/profileEventEvidence.tsx | 6 +- .../utils/trace/isOlderThan30Days.spec.tsx | 25 ++++++ static/app/utils/trace/isOlderThan30Days.tsx | 19 +++-- .../views/discover/table/tableView.spec.tsx | 76 +++++++++++++++++++ static/app/views/discover/table/tableView.tsx | 57 ++++++++------ 6 files changed, 194 insertions(+), 35 deletions(-) create mode 100644 static/app/utils/trace/isOlderThan30Days.spec.tsx diff --git a/static/app/components/events/profileEventEvidence.spec.tsx b/static/app/components/events/profileEventEvidence.spec.tsx index e632b24f7b6e1c..0469729f5a466b 100644 --- a/static/app/components/events/profileEventEvidence.spec.tsx +++ b/static/app/components/events/profileEventEvidence.spec.tsx @@ -1,7 +1,8 @@ import {EventFixture} from 'sentry-fixture/event'; import {GroupFixture} from 'sentry-fixture/group'; -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {ProfileEventEvidence} from 'sentry/components/events/profileEventEvidence'; import {IssueType} from 'sentry/types/group'; @@ -33,6 +34,10 @@ describe('ProfileEventEvidence', () => { projectSlug: 'project-slug', }; + afterEach(() => { + resetMockDate(); + }); + it('displays profile ID and data in evidence display', () => { render(); @@ -58,9 +63,44 @@ describe('ProfileEventEvidence', () => { it('correctly links to the transaction', () => { render(); - expect(screen.getByRole('button', {name: 'View Transaction'})).toHaveAttribute( + const button = screen.getByRole('button', {name: 'View Transaction'}); + expect(button).toHaveAttribute( 'href', - '/organizations/org-slug/explore/traces/trace/trace-id/?referrer=issue&statsPeriod=14d' + expect.stringContaining('/organizations/org-slug/explore/traces/trace/trace-id/?') + ); + expect(button).toHaveAttribute( + 'href', + expect.stringContaining('eventId=transaction-id') + ); + expect(button).toHaveAttribute('href', expect.stringContaining('timestamp=')); + }); + + it('disables the transaction link when the event is older than 30 days', async () => { + setMockDate(new Date('2025-10-06T00:00:00').getTime()); + + render( + ); + + const button = screen.getByRole('button', {name: 'View Transaction'}); + expect(button).toHaveAttribute('aria-disabled', 'true'); + + await userEvent.hover(button); + expect( + await screen.findByText('Trace data is only available for the last 30 days') + ).toBeInTheDocument(); }); }); diff --git a/static/app/components/events/profileEventEvidence.tsx b/static/app/components/events/profileEventEvidence.tsx index 45375ba38f1a03..5e55601ddb66e3 100644 --- a/static/app/components/events/profileEventEvidence.tsx +++ b/static/app/components/events/profileEventEvidence.tsx @@ -2,6 +2,7 @@ import {LinkButton} from '@sentry/scraps/button'; import {Tooltip} from '@sentry/scraps/tooltip'; import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; +import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; import {IconProfiling} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; @@ -21,7 +22,8 @@ export function ProfileEventEvidence({event, projectSlug}: ProfileEvidenceProps) const evidenceData = event.occurrence?.evidenceData ?? {}; const evidenceDisplay = event.occurrence?.evidenceDisplay ?? []; const traceSlug = event.contexts?.trace?.trace_id ?? ''; - const isOld = isPartialSpanOrTraceData(evidenceData.timestamp); + const traceTimestamp = evidenceData.timestamp ?? getEventTimestampInSeconds(event); + const isOld = isPartialSpanOrTraceData(traceTimestamp); const keyValueListData = [ ...(evidenceData.transactionId && evidenceData.transactionName @@ -40,7 +42,7 @@ export function ProfileEventEvidence({event, projectSlug}: ProfileEvidenceProps) disabled={isOld} to={generateLinkToEventInTraceView({ traceSlug, - timestamp: evidenceData.timestamp, + timestamp: traceTimestamp, eventId: evidenceData.transactionId, location: { ...location, diff --git a/static/app/utils/trace/isOlderThan30Days.spec.tsx b/static/app/utils/trace/isOlderThan30Days.spec.tsx new file mode 100644 index 00000000000000..834a60831763b7 --- /dev/null +++ b/static/app/utils/trace/isOlderThan30Days.spec.tsx @@ -0,0 +1,25 @@ +import {resetMockDate, setMockDate} from 'sentry-test/utils'; + +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; + +describe('isPartialSpanOrTraceData', () => { + beforeEach(() => { + setMockDate(new Date('2025-10-06T00:00:00').getTime()); + }); + + afterEach(() => { + resetMockDate(); + }); + + it('handles unix timestamps in seconds as strings', () => { + expect(isPartialSpanOrTraceData('1751328000')).toBe(true); + }); + + it('handles unix timestamps in milliseconds as strings', () => { + expect(isPartialSpanOrTraceData('1751328000000')).toBe(true); + }); + + it('does not mark invalid timestamps as partial trace data', () => { + expect(isPartialSpanOrTraceData('not-a-timestamp')).toBe(false); + }); +}); diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx index 10bbbca589d91c..fcaef5ae03f76a 100644 --- a/static/app/utils/trace/isOlderThan30Days.tsx +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -2,6 +2,19 @@ import moment from 'moment-timezone'; const TRACE_DATA_RETENTION_DAYS = 30; +function normalizeTimestamp(timestamp: string | number): string | number { + if (typeof timestamp === 'number') { + return timestamp < 1e12 ? timestamp * 1000 : timestamp; + } + + const numericTimestamp = Number(timestamp); + if (timestamp.trim() !== '' && Number.isFinite(numericTimestamp)) { + return numericTimestamp < 1e12 ? numericTimestamp * 1000 : numericTimestamp; + } + + return timestamp; +} + /** * Returns true if the given timestamp is older than 30 days, indicating * that the trace/span data may no longer be available. @@ -15,11 +28,7 @@ export function isPartialSpanOrTraceData( return false; } const now = moment(); - // Numbers < 1e12 are likely seconds (epoch), not milliseconds. - // e.g. 1712002518 is seconds, 1712002518000 is milliseconds. - const normalizedTimestamp = - typeof timestamp === 'number' && timestamp < 1e12 ? timestamp * 1000 : timestamp; - const timestampDate = moment(normalizedTimestamp); + const timestampDate = moment(normalizeTimestamp(timestamp)); if (!timestampDate.isValid()) { return false; } diff --git a/static/app/views/discover/table/tableView.spec.tsx b/static/app/views/discover/table/tableView.spec.tsx index bf93622896bdbd..4eb756aca88a75 100644 --- a/static/app/views/discover/table/tableView.spec.tsx +++ b/static/app/views/discover/table/tableView.spec.tsx @@ -10,6 +10,7 @@ import { waitFor, within, } from 'sentry-test/reactTestingLibrary'; +import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {ProjectsStore} from 'sentry/stores/projectsStore'; import {TagStore} from 'sentry/stores/tagStore'; @@ -134,6 +135,7 @@ describe('TableView > CellActions', () => { afterEach(() => { ProjectsStore.reset(); + resetMockDate(); }); it('updates sort order on equation fields', () => { @@ -414,6 +416,8 @@ describe('TableView > CellActions', () => { }); it('renders trace view link', () => { + setMockDate(new Date('2019-05-24T00:00:00Z').getTime()); + const traceRows: TableData = { meta: { trace: 'string', @@ -490,6 +494,78 @@ describe('TableView > CellActions', () => { ); }); + it('renders a disabled trace link for old transaction ids', () => { + setMockDate(new Date('2019-07-01T00:00:00Z').getTime()); + + const traceRows: TableData = { + meta: { + trace: 'string', + id: 'string', + transaction: 'string', + timestamp: 'date', + project: 'string', + }, + data: [ + { + trace: '7fdf8efed85a4f9092507063ced1995b', + id: '509663014077465b8981b65225bdec0f', + transaction: '/organizations/', + timestamp: '2019-05-23T22:12:48+00:00', + project: 'project-slug', + }, + ], + }; + + const traceQuery = { + id: '42', + name: 'best query', + field: ['id', 'transaction', 'timestamp'], + queryDataset: 'transaction-like', + sort: ['transaction'], + query: '', + project: ['123'], + statsPeriod: '14d', + environment: ['staging'], + yAxis: 'p95', + }; + + const traceLocation = LocationFixture({ + pathname: '/organizations/org-slug/explore/discover/results/', + query: traceQuery, + }); + + render( + , + { + organization, + initialRouterConfig: { + location: { + pathname: traceLocation.pathname, + query: traceQuery, + }, + }, + } + ); + + const firstRow = screen.getAllByRole('row')[1]!; + const idCell = within(firstRow).getAllByRole('cell')[0]!; + expect(within(idCell).queryByTestId('view-event')).not.toBeInTheDocument(); + expect(within(idCell).getByRole('link')).toHaveAttribute('aria-disabled', 'true'); + }); + it('handles go to release', async () => { const {router} = renderComponent(rows, eventView); await openContextMenu(5); diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index e43a55437bf93a..86586040ddd332 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -346,34 +346,41 @@ export function TableView(props: TableViewProps) { ); } - target = generateLinkToEventInTraceView({ - traceSlug: dataRow.trace?.toString(), - eventId: dataRow.id, - timestamp: dataRow.timestamp!, - organization, - location, - eventView, - source: TraceViewSources.DISCOVER, - }); + const traceTimestamp = getTimeStampFromTableDateField(dataRow.timestamp); + if (traceTimestamp && isPartialSpanOrTraceData(traceTimestamp)) { + cell = {cell}; + } else { + target = generateLinkToEventInTraceView({ + traceSlug: dataRow.trace?.toString(), + eventId: dataRow.id, + timestamp: dataRow.timestamp!, + organization, + location, + eventView, + source: TraceViewSources.DISCOVER, + }); + } } - const idLink = ( - - {cell} - - ); + if (target) { + const idLink = ( + + {cell} + + ); - cell = ( - - {idLink} - - ); + cell = ( + + {idLink} + + ); + } } else if (columnKey === 'transaction' && dataRow.transaction) { cell = ( Date: Wed, 8 Apr 2026 09:13:00 -0300 Subject: [PATCH 24/39] fix(traces): Preserve truncation for disabled web vitals links Wrap the disabled old-trace ID cell in the same overflow container as the other web vitals table states so narrow columns still ellipsize consistently. Co-Authored-By: GPT-5.4 Made-with: Cursor --- .../webVitals/components/pageOverviewWebVitalsDetailPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index bfc06beaa72f9e..d936794cc814f5 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -299,7 +299,9 @@ export function PageOverviewWebVitalsDetailPanel({ }); if (isOld) { return ( - {getShortEventId(row.id)} + + {getShortEventId(row.id)} + ); } return ( From e908096ee95ab5793a9e9e7a6190192742c2e52f Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 09:36:03 -0300 Subject: [PATCH 25/39] ref(traces): Block old trace redirects in event detail pages Prevent navigation to trace details for events older than 30 days in both projectEventRedirect and discover eventDetails pages. Shows a retention message instead of redirecting. --- static/app/views/discover/eventDetails.tsx | 25 +++++++++-- .../app/views/projectEventRedirect.spec.tsx | 42 +++++++++++++++++++ static/app/views/projectEventRedirect.tsx | 25 +++++++++-- 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/static/app/views/discover/eventDetails.tsx b/static/app/views/discover/eventDetails.tsx index c51e684696025f..36cb212cf49d6e 100644 --- a/static/app/views/discover/eventDetails.tsx +++ b/static/app/views/discover/eventDetails.tsx @@ -12,6 +12,7 @@ import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {useApiQuery} from 'sentry/utils/queryClient'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -47,6 +48,9 @@ export default function EventDetails() { {staleTime: 2 * 60 * 1000} // 2 minutes in milliseonds ); + const traceTimestamp = event ? getEventTimestampInSeconds(event) : undefined; + const isOldTrace = traceTimestamp ? isPartialSpanOrTraceData(traceTimestamp) : false; + useEffect(() => { if (!event) return; @@ -59,20 +63,27 @@ export default function EventDetails() { } const traceId = event.contexts?.trace?.trace_id; - if (traceId) { - const timestamp = getEventTimestampInSeconds(event); + if (traceId && !isOldTrace) { navigate( getTraceDetailsUrl({ organization, traceSlug: traceId, dateSelection: datetimeSelection, - timestamp, + timestamp: traceTimestamp, eventId: event.eventID, location, }) ); } - }, [event, organization, datetimeSelection, location, navigate]); + }, [ + event, + organization, + datetimeSelection, + isOldTrace, + location, + navigate, + traceTimestamp, + ]); if (error) { const notFound = error.status === 404; @@ -95,6 +106,12 @@ export default function EventDetails() { ); } + if (event && isOldTrace) { + return ( + + ); + } + if ( isPending || (!isPending && event) // Prevents flash of loading error below once event is loaded successfully diff --git a/static/app/views/projectEventRedirect.spec.tsx b/static/app/views/projectEventRedirect.spec.tsx index 628438a813e514..4b9cb594ac471e 100644 --- a/static/app/views/projectEventRedirect.spec.tsx +++ b/static/app/views/projectEventRedirect.spec.tsx @@ -2,6 +2,7 @@ import {EventFixture} from 'sentry-fixture/event'; import {OrganizationFixture} from 'sentry-fixture/organization'; import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; +import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {IssueCategory} from 'sentry/types/group'; import {ProjectEventRedirect} from 'sentry/views/projectEventRedirect'; @@ -13,6 +14,10 @@ describe('ProjectEventRedirect', () => { MockApiClient.clearMockResponses(); }); + afterEach(() => { + resetMockDate(); + }); + it('redirects to issue event page when event has groupID', async () => { const event = EventFixture({ eventID: 'abc123', @@ -79,6 +84,43 @@ describe('ProjectEventRedirect', () => { }); }); + it('shows a retention message instead of redirecting for old transaction events', async () => { + setMockDate(new Date('2025-10-06T00:00:00Z').getTime()); + + const event = EventFixture({ + eventID: 'abc123', + groupID: undefined, + contexts: { + trace: { + trace_id: 'trace-123', + }, + }, + dateCreated: '2025-08-01T00:00:00.000Z', + }); + + MockApiClient.addMockResponse({ + url: `/organizations/${organization.slug}/events/my-project:event-id/`, + body: event, + }); + + const {router} = render(, { + organization, + initialRouterConfig: { + location: { + pathname: `/organizations/${organization.slug}/projects/my-project/events/event-id/`, + }, + route: '/organizations/:orgId/projects/:projectId/events/:eventId/', + }, + }); + + expect( + await screen.findByText('Trace data is only available for the last 30 days') + ).toBeInTheDocument(); + expect(router.location.pathname).toBe( + `/organizations/${organization.slug}/projects/my-project/events/event-id/` + ); + }); + it('shows NotFound for 404 errors', async () => { MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/events/my-project:event-id/`, diff --git a/static/app/views/projectEventRedirect.tsx b/static/app/views/projectEventRedirect.tsx index 34d2ef4f54ea48..fff54973b8c4f6 100644 --- a/static/app/views/projectEventRedirect.tsx +++ b/static/app/views/projectEventRedirect.tsx @@ -12,6 +12,7 @@ import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {useApiQuery} from 'sentry/utils/queryClient'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -58,6 +59,9 @@ export function ProjectEventRedirect() { {staleTime: 2 * 60 * 1000} // 2 minutes in milliseconds ); + const traceTimestamp = event ? getEventTimestampInSeconds(event) : undefined; + const isOldTrace = traceTimestamp ? isPartialSpanOrTraceData(traceTimestamp) : false; + useEffect(() => { if (!event) { return; @@ -98,21 +102,28 @@ export function ProjectEventRedirect() { // For events without a group ID (e.g., transaction events), try to navigate to trace details const traceId = event.contexts?.trace?.trace_id; - if (traceId) { - const timestamp = getEventTimestampInSeconds(event); + if (traceId && !isOldTrace) { navigate( getTraceDetailsUrl({ organization, traceSlug: traceId, dateSelection: datetimeSelection, - timestamp, + timestamp: traceTimestamp, eventId: event.eventID, location, }), {replace: true} ); } - }, [event, organization, datetimeSelection, location, navigate]); + }, [ + event, + organization, + datetimeSelection, + isOldTrace, + location, + navigate, + traceTimestamp, + ]); if (error) { const notFound = error.status === 404; @@ -137,6 +148,12 @@ export function ProjectEventRedirect() { ); } + if (event && isOldTrace) { + return ( + + ); + } + if ( isPending || (!isPending && event) // Prevents flash of loading error below once event is loaded successfully From 6241dde75a79f03af34a2d39bcc2b409563301e3 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 09:36:15 -0300 Subject: [PATCH 26/39] ref(traces): Disable trace preview and waterfall overlay for old events Disable the "View Full Trace" button with a tooltip and hide the issues trace waterfall overlay when events are older than 30 days. --- .../performance/eventTraceView.spec.tsx | 31 ++++++++++++++++++- .../interfaces/performance/eventTraceView.tsx | 24 +++++++++----- .../issuesTraceWaterfallOverlay.tsx | 8 +++++ 3 files changed, 55 insertions(+), 8 deletions(-) diff --git a/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx b/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx index ad64bccd1e31f0..712a7d022fa862 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.spec.tsx @@ -1,8 +1,10 @@ import {EventFixture} from 'sentry-fixture/event'; import {GroupFixture} from 'sentry-fixture/group'; +import {OrganizationFixture} from 'sentry-fixture/organization'; import {initializeData} from 'sentry-test/performance/initializePerformanceData'; -import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; +import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {EntryType} from 'sentry/types/event'; import type {TraceEventResponse} from 'sentry/views/issueDetails/traceTimeline/useTraceTimelineEvents'; @@ -39,6 +41,10 @@ describe('EventTraceView', () => { }); }); + afterEach(() => { + resetMockDate(); + }); + it('renders a trace', async () => { const size = 20; MockApiClient.addMockResponse({ @@ -146,4 +152,27 @@ describe('EventTraceView', () => { expect(await screen.findByText('Trace Preview')).toBeInTheDocument(); }); + + it('disables the trace preview button when the trace is older than 30 days', async () => { + setMockDate(new Date('2025-10-06T00:00:00').getTime()); + + render( + + ); + + const button = screen.getByRole('button', {name: 'View Full Trace'}); + expect(button).toHaveAttribute('aria-disabled', 'true'); + + await userEvent.hover(button); + expect( + await screen.findByText('Trace data is only available for the last 30 days') + ).toBeInTheDocument(); + }); }); diff --git a/static/app/components/events/interfaces/performance/eventTraceView.tsx b/static/app/components/events/interfaces/performance/eventTraceView.tsx index b8b7d1912344fc..85d38a2f75187d 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; import {Grid} from '@sentry/scraps/layout'; +import {Tooltip} from '@sentry/scraps/tooltip'; import { isWebVitalsEvent, @@ -16,6 +17,7 @@ import {type Group} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig'; import {useRouteAnalyticsParams} from 'sentry/utils/routeAnalytics/useRouteAnalyticsParams'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {SectionKey} from 'sentry/views/issueDetails/streamline/context'; import {InterimSection} from 'sentry/views/issueDetails/streamline/interimSection'; @@ -175,6 +177,8 @@ export function EventTraceView({group, event, organization}: EventTraceViewProps ); const hasTracePreviewFeature = organization.features.includes('profiling'); + const eventTimestamp = getEventTimestampInSeconds(event); + const isOld = eventTimestamp ? isPartialSpanOrTraceData(eventTimestamp) : false; return ( - - {t('View Full Trace')} - + + {t('View Full Trace')} + + } > diff --git a/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx b/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx index 9ce2708c090649..6424892ff539b4 100644 --- a/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx +++ b/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx @@ -7,10 +7,12 @@ import * as qs from 'query-string'; import {Link} from '@sentry/scraps/link'; +import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import type {Event} from 'sentry/types/event'; import {defined} from 'sentry/utils'; import {trackAnalytics} from 'sentry/utils/analytics'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {isCollapsedNode} from 'sentry/views/performance/newTraceDetails/traceGuards'; @@ -131,6 +133,12 @@ export function IssueTraceWaterfallOverlay({ : [`txn-${event.eventID}`]; const baseLink = getTraceLinkForIssue(traceTarget, baseNodePath); + const eventTimestamp = getEventTimestampInSeconds(event); + const isOld = eventTimestamp ? isPartialSpanOrTraceData(eventTimestamp) : false; + if (isOld) { + return null; + } + return ( Date: Wed, 8 Apr 2026 09:36:40 -0300 Subject: [PATCH 27/39] ref(traces): Disable links in trace drawer for old trace data Use DisabledTraceLink in trace drawer attributes, span links, and trace summary sections when data is older than 30 days. --- .../details/span/eapSections/attributes.tsx | 8 +++++ .../span/eapSections/traceSpanLinks.tsx | 24 ++++++++++++--- .../newTraceDetails/traceSummary.tsx | 30 ++++++++++++------- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx index 195038a93bba35..51d665e6b91e9f 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx @@ -7,6 +7,7 @@ import {Stack} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {SearchBar as BaseSearchBar} from 'sentry/components/searchBar'; @@ -22,6 +23,7 @@ import {generateProfileFlamechartRoute} from 'sentry/utils/profiling/routes'; import {ellipsize} from 'sentry/utils/string/ellipsize'; import {looksLikeAJSONArray} from 'sentry/utils/string/looksLikeAJSONArray'; import {looksLikeAJSONObject} from 'sentry/utils/string/looksLikeAJSONObject'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {AssertionFailureTree} from 'sentry/views/alerts/rules/uptime/assertions/assertionFailure/assertionFailureTree'; import type {AttributesFieldRendererProps} from 'sentry/views/explore/components/traceItemAttributes/attributesTree'; @@ -138,6 +140,12 @@ export function Attributes({ }, [FieldKey.TRACE]: (props: CustomRenderersProps) => { const traceSlug = String(props.item.value); + const isOld = isPartialSpanOrTraceData(node.value.start_timestamp); + + if (isOld) { + return {props.item.value}; + } + const target = getTraceDetailsUrl({ organization, traceSlug, diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx index c36ae58e3e9827..1f1de70e50a320 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx @@ -1,5 +1,6 @@ import type {Location} from 'history'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {t} from 'sentry/locale'; import type {Organization} from 'sentry/types/organization'; import { @@ -8,6 +9,7 @@ import { } from 'sentry/utils/discover/fieldRenderers'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import type {Theme} from 'sentry/utils/theme'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useNavigate} from 'sentry/utils/useNavigate'; import { AttributesTree, @@ -79,8 +81,16 @@ export function TraceSpanLinks({ const linksAsAttributes: TraceItemResponseAttribute[] = links.flatMap( (link, linkIndex) => { const prefix = `span_link_${linkIndex + 1}`; + const isCrossTraceLink = !tree || link.traceId !== traceId; + const isOld = isPartialSpanOrTraceData(node.value.start_timestamp); customRenderers[`${prefix}.trace_id`] = () => { + const traceLabel = traceIdRenderer({trace: link.traceId}, renderBaggage); + + if (isOld && isCrossTraceLink) { + return {traceLabel}; + } + const traceTarget = generateLinkToEventInTraceView({ organization, location, @@ -94,18 +104,24 @@ export function TraceSpanLinks({ onClick={() => { // If we are outside the traceview, or the link is to a different trace, we navigate to the trace // otherwise we do nothing - if (!tree || link.traceId !== traceId) { + if (isCrossTraceLink) { closeSpanDetailsDrawer(); navigate(traceTarget); } }} > - {traceIdRenderer({trace: link.traceId}, renderBaggage)} + {traceLabel} ); }; customRenderers[`${prefix}.span_id`] = () => { + const spanLabel = spanIdRenderer({span_id: link.itemId}, renderBaggage); + + if (isOld && isCrossTraceLink) { + return {spanLabel}; + } + const spanTarget = generateLinkToEventInTraceView({ organization, location, @@ -119,7 +135,7 @@ export function TraceSpanLinks({ { // If we are outside the trace waterfall, or the link is to a span in a different trace, we navigate - if (!tree || link.traceId !== traceId) { + if (isCrossTraceLink) { closeSpanDetailsDrawer(); navigate(spanTarget); return; @@ -132,7 +148,7 @@ export function TraceSpanLinks({ } }} > - {spanIdRenderer({span_id: link.itemId}, renderBaggage)} + {spanLabel} ); }; diff --git a/static/app/views/performance/newTraceDetails/traceSummary.tsx b/static/app/views/performance/newTraceDetails/traceSummary.tsx index 72c2e7d7cd5717..4fd83d90a17d18 100644 --- a/static/app/views/performance/newTraceDetails/traceSummary.tsx +++ b/static/app/views/performance/newTraceDetails/traceSummary.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import {Flex} from '@sentry/scraps/layout'; import {Link} from '@sentry/scraps/link'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import {useFeedbackSDKIntegration} from 'sentry/components/feedbackButton/useFeedbackSDKIntegration'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; @@ -15,6 +16,7 @@ import {getApiUrl} from 'sentry/utils/api/getApiUrl'; import {MarkedText} from 'sentry/utils/marked/markedText'; import type {ApiQueryKey} from 'sentry/utils/queryClient'; import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; @@ -78,6 +80,8 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { const {feedback} = useFeedbackSDKIntegration(); const organization = useOrganization(); const location = useLocation(); + const isOld = + !!location.query.timestamp && isPartialSpanOrTraceData(location.query.timestamp); if (traceContent.isPending) { return ; @@ -140,17 +144,21 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { {investigations.map((span, idx) => ( - - {span.spanOp} - + {isOld ? ( + {span.spanOp} + ) : ( + + {span.spanOp} + + )} - {span.explanation} ))} From 266e5158e36966d9b8f2c0276387e1c863c7c292 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 09:36:56 -0300 Subject: [PATCH 28/39] ref(traces): Disable trace links in transaction tables for old data Disable trace column links in discover transactionsTable, EAP sampled events table, and transaction summary link generators when data is older than 30 days. --- .../components/discover/transactionsTable.tsx | 9 +++++ .../eapSampledEventsTable.spec.tsx | 34 +++++++++++++++++++ .../eapSampledEventsTable.tsx | 31 ++++++++++------- .../performance/transactionSummary/utils.tsx | 10 +++++- 4 files changed, 71 insertions(+), 13 deletions(-) diff --git a/static/app/components/discover/transactionsTable.tsx b/static/app/components/discover/transactionsTable.tsx index 967bf22fb576ba..be91a62d8d7cdb 100644 --- a/static/app/components/discover/transactionsTable.tsx +++ b/static/app/components/discover/transactionsTable.tsx @@ -6,6 +6,7 @@ import type {Location, LocationDescriptor} from 'history'; import {LinkButton} from '@sentry/scraps/button'; import {Link} from '@sentry/scraps/link'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {PanelTable} from 'sentry/components/panels/panelTable'; import {QuestionTooltip} from 'sentry/components/questionTooltip'; @@ -21,6 +22,7 @@ import {fieldAlignment, getAggregateAlias} from 'sentry/utils/discover/fields'; import {ViewReplayLink} from 'sentry/utils/discover/viewReplayLink'; import {isEmptyObject} from 'sentry/utils/object/isEmptyObject'; import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import type {Actions} from 'sentry/views/discover/table/cellAction'; import {CellAction} from 'sentry/views/discover/table/cellAction'; import type {TableColumn} from 'sentry/views/discover/table/types'; @@ -147,6 +149,11 @@ export function TransactionsTable(props: Props) { let rendered = fieldRenderer(row, {organization, location, theme}); const target = generateLink?.[field]?.(organization, row, location); + const isOldTraceTarget = + ['id', 'trace'].includes(field) && + !!row.trace && + !!row.timestamp && + isPartialSpanOrTraceData(row.timestamp); if (fields[index] === 'profile.id') { rendered = ( @@ -160,6 +167,8 @@ export function TransactionsTable(props: Props) { ); + } else if (isOldTraceTarget) { + rendered = {rendered}; } else if (target && !isEmptyObject(target)) { if (fields[index] === 'replayId') { rendered = ( diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx index 56e1d56361d3a6..50e2d0538a1c17 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.spec.tsx @@ -4,6 +4,7 @@ import {PageFiltersFixture} from 'sentry-fixture/pageFilters'; import {ProjectFixture} from 'sentry-fixture/project'; import {render, screen} from 'sentry-test/reactTestingLibrary'; +import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {PageFiltersStore} from 'sentry/components/pageFilters/store'; import {ProjectsStore} from 'sentry/stores/projectsStore'; @@ -85,6 +86,7 @@ describe('EAP SampledEventsTable', () => { afterEach(() => { MockApiClient.clearMockResponses(); ProjectsStore.reset(); + resetMockDate(); }); it('renders the table with data', async () => { @@ -126,4 +128,36 @@ describe('EAP SampledEventsTable', () => { // Verify ops breakdown renders expect(screen.getByTestId('relative-ops-breakdown')).toBeInTheDocument(); }); + + it('renders a disabled trace link when the trace is older than 30 days', async () => { + setMockDate(new Date('2025-02-15T00:00:00Z').getTime()); + + const eventView = EventView.fromNewQueryWithLocation( + { + id: undefined, + version: 2, + name: 'test', + fields: ['span_id', 'trace', 'timestamp'], + query: '', + projects: [Number(project.id)], + orderby: '-timestamp', + }, + LocationFixture({pathname: '/'}) + ); + + render( + {}} + />, + {organization} + ); + + const traceLink = await screen.findByRole('link', {name: 'trace123'}); + expect(traceLink).toHaveAttribute('aria-disabled', 'true'); + }); }); diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx index 5bba4b515d7ac0..0cdc74e1c0d703 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx @@ -9,6 +9,7 @@ import {Link} from '@sentry/scraps/link'; import {Tooltip} from '@sentry/scraps/tooltip'; import {Duration} from 'sentry/components/duration'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {Pagination, type CursorHandler} from 'sentry/components/pagination'; @@ -34,6 +35,7 @@ import {decodeScalar, decodeSorts} from 'sentry/utils/queryString'; import {projectSupportsReplay} from 'sentry/utils/replays/projectSupportsReplay'; import type {Theme} from 'sentry/utils/theme'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -283,6 +285,23 @@ function renderBodyCell( if (column.key === 'trace') { const traceId = row.trace?.toString() ?? ''; if (traceId) { + const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); + let rendered: React.ReactNode = traceId; + + if (meta?.fields) { + const renderer = getFieldRenderer('trace', meta.fields, false); + rendered = renderer(row, { + location, + organization, + theme, + unit: meta.units?.trace, + }); + } + + if (isOld) { + return {rendered}; + } + const target = getTraceDetailsUrl({ organization, traceSlug: traceId, @@ -292,18 +311,6 @@ function renderBodyCell( source: TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY, }); - if (!meta?.fields) { - return {traceId}; - } - - const renderer = getFieldRenderer('trace', meta.fields, false); - const rendered = renderer(row, { - location, - organization, - theme, - unit: meta.units?.trace, - }); - return {rendered}; } } diff --git a/static/app/views/performance/transactionSummary/utils.tsx b/static/app/views/performance/transactionSummary/utils.tsx index 447834de0645da..a91ae65c74dd80 100644 --- a/static/app/views/performance/transactionSummary/utils.tsx +++ b/static/app/views/performance/transactionSummary/utils.tsx @@ -12,6 +12,7 @@ import { generateProfileFlamechartRoute, } from 'sentry/utils/profiling/routes'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {normalizeUrl} from 'sentry/utils/url/normalizeUrl'; import {DOMAIN_VIEW_BASE_URL} from 'sentry/views/insights/pages/settings'; import type {DomainView} from 'sentry/views/insights/pages/useFilters'; @@ -139,7 +140,10 @@ export function generateTraceLink(dateSelection: any, view?: DomainView) { location: Location ): LocationDescriptor => { const traceId = tableRow.trace ? `${tableRow.trace}` : ''; - if (!traceId) { + if ( + !traceId || + (tableRow.timestamp && isPartialSpanOrTraceData(tableRow.timestamp)) + ) { return {}; } @@ -162,6 +166,10 @@ export function generateTransactionIdLink(view?: DomainView) { location: Location, spanId?: string ): LocationDescriptor => { + if (tableRow.timestamp && isPartialSpanOrTraceData(tableRow.timestamp)) { + return {}; + } + return generateLinkToEventInTraceView({ eventId: tableRow.id, timestamp: tableRow.timestamp!, From 168f92890ecfee11e890dd0e959b3eb04b326148 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 09:45:12 -0300 Subject: [PATCH 29/39] fix(traces): Use >= for 30-day boundary and improve test coverage Use >= instead of > for the retention check so data at exactly 30 days is conservatively marked as old, preventing broken links in the boundary window. Expand test coverage from 3 to 10 cases including ISO strings, numeric types, boundary, undefined, and future timestamps. Extract IIFE in profileDetails to a local variable for readability. Co-Authored-By: Claude Opus 4.6 --- .../flamegraphDrawer/profileDetails.tsx | 23 +++++------ .../utils/trace/isOlderThan30Days.spec.tsx | 41 +++++++++++++++++-- static/app/utils/trace/isOlderThan30Days.tsx | 2 +- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index f1cbf50214b63b..5f5ba305ddd240 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -277,6 +277,16 @@ function TransactionEventDetails({ }) : null; + const isOldTransaction = isPartialSpanOrTraceData(transaction.endTimestamp); + let transactionValue: React.ReactNode = transaction.title; + if (transactionTarget && !isOldTransaction) { + transactionValue = {transaction.title}; + } else if (isOldTransaction) { + transactionValue = ( + {transaction.title} + ); + } + const details: Array<{ key: string; label: string; @@ -285,18 +295,7 @@ function TransactionEventDetails({ { key: 'transaction', label: t('Transaction'), - value: (() => { - const isOld = isPartialSpanOrTraceData(transaction.endTimestamp); - if (transactionTarget && !isOld) { - return {transaction.title}; - } - if (isOld) { - return ( - {transaction.title} - ); - } - return transaction.title; - })(), + value: transactionValue, }, { key: 'timestamp', diff --git a/static/app/utils/trace/isOlderThan30Days.spec.tsx b/static/app/utils/trace/isOlderThan30Days.spec.tsx index 834a60831763b7..6b897e8b930f29 100644 --- a/static/app/utils/trace/isOlderThan30Days.spec.tsx +++ b/static/app/utils/trace/isOlderThan30Days.spec.tsx @@ -3,23 +3,58 @@ import {resetMockDate, setMockDate} from 'sentry-test/utils'; import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; describe('isPartialSpanOrTraceData', () => { + // 2025-10-06T00:00:00Z beforeEach(() => { - setMockDate(new Date('2025-10-06T00:00:00').getTime()); + setMockDate(new Date('2025-10-06T00:00:00Z').getTime()); }); afterEach(() => { resetMockDate(); }); + it('returns false for undefined', () => { + expect(isPartialSpanOrTraceData(undefined)).toBe(false); + }); + + it('returns false for invalid timestamps', () => { + expect(isPartialSpanOrTraceData('not-a-timestamp')).toBe(false); + }); + + it('handles ISO date strings', () => { + expect(isPartialSpanOrTraceData('2025-08-01T00:00:00Z')).toBe(true); + expect(isPartialSpanOrTraceData('2025-10-05T00:00:00Z')).toBe(false); + }); + it('handles unix timestamps in seconds as strings', () => { + // 2025-07-01 — clearly old expect(isPartialSpanOrTraceData('1751328000')).toBe(true); }); it('handles unix timestamps in milliseconds as strings', () => { + // 2025-07-01 — clearly old expect(isPartialSpanOrTraceData('1751328000000')).toBe(true); }); - it('does not mark invalid timestamps as partial trace data', () => { - expect(isPartialSpanOrTraceData('not-a-timestamp')).toBe(false); + it('handles numeric timestamps in seconds', () => { + // 2025-07-01 — clearly old + expect(isPartialSpanOrTraceData(1751328000)).toBe(true); + }); + + it('handles numeric timestamps in milliseconds', () => { + // 2025-07-01 — clearly old + expect(isPartialSpanOrTraceData(1751328000000)).toBe(true); + }); + + it('marks data as old at exactly 30 days', () => { + // Exactly 30 days before 2025-10-06T00:00:00Z + expect(isPartialSpanOrTraceData('2025-09-06T00:00:00Z')).toBe(true); + }); + + it('does not mark data as old at 29 days', () => { + expect(isPartialSpanOrTraceData('2025-09-07T00:00:00Z')).toBe(false); + }); + + it('returns false for future timestamps', () => { + expect(isPartialSpanOrTraceData('2025-12-01T00:00:00Z')).toBe(false); }); }); diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx index fcaef5ae03f76a..970f9d89e17ea9 100644 --- a/static/app/utils/trace/isOlderThan30Days.tsx +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -32,5 +32,5 @@ export function isPartialSpanOrTraceData( if (!timestampDate.isValid()) { return false; } - return now.diff(timestampDate, 'days') > TRACE_DATA_RETENTION_DAYS; + return now.diff(timestampDate, 'days') >= TRACE_DATA_RETENTION_DAYS; } From 820202fa15faad8d441e87cb965e175022fee650 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 11:21:18 -0300 Subject: [PATCH 30/39] fix(traces): Normalize query timestamp to string for type safety location.query.timestamp can be string | string[], but isPartialSpanOrTraceData expects string | number | undefined. Extract and normalize the value before passing it to the function. Co-Authored-By: Claude Opus 4.6 --- .../app/views/performance/newTraceDetails/traceSummary.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/static/app/views/performance/newTraceDetails/traceSummary.tsx b/static/app/views/performance/newTraceDetails/traceSummary.tsx index 4fd83d90a17d18..cc9dc519f1ae9f 100644 --- a/static/app/views/performance/newTraceDetails/traceSummary.tsx +++ b/static/app/views/performance/newTraceDetails/traceSummary.tsx @@ -80,8 +80,10 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { const {feedback} = useFeedbackSDKIntegration(); const organization = useOrganization(); const location = useLocation(); - const isOld = - !!location.query.timestamp && isPartialSpanOrTraceData(location.query.timestamp); + const timestamp = Array.isArray(location.query.timestamp) + ? location.query.timestamp[0] + : location.query.timestamp; + const isOld = !!timestamp && isPartialSpanOrTraceData(timestamp); if (traceContent.isPending) { return ; From 21be9cae8104f1d8fd9f104220025cc881b4ba94 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 11:26:20 -0300 Subject: [PATCH 31/39] fix(traces): Hoist isOld check out of flatMap and document normalizeTimestamp Move the `isOld` computation outside the `.flatMap()` loop in traceSpanLinks since `node.value.start_timestamp` is constant across iterations. Add JSDoc to `normalizeTimestamp` documenting expected input formats and known edge case with pure numeric date strings. --- static/app/utils/trace/isOlderThan30Days.tsx | 5 +++++ .../traceDrawer/details/span/eapSections/traceSpanLinks.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/static/app/utils/trace/isOlderThan30Days.tsx b/static/app/utils/trace/isOlderThan30Days.tsx index 970f9d89e17ea9..d7e27253701685 100644 --- a/static/app/utils/trace/isOlderThan30Days.tsx +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -2,6 +2,11 @@ import moment from 'moment-timezone'; const TRACE_DATA_RETENTION_DAYS = 30; +/** + * Converts a timestamp to milliseconds for moment.js consumption. + * Expects Unix epoch seconds, Unix epoch milliseconds, or ISO 8601 strings. + * Pure numeric strings like "20250801" will be misinterpreted as epoch values. + */ function normalizeTimestamp(timestamp: string | number): string | number { if (typeof timestamp === 'number') { return timestamp < 1e12 ? timestamp * 1000 : timestamp; diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx index 1f1de70e50a320..d11669795667da 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/traceSpanLinks.tsx @@ -78,11 +78,12 @@ export function TraceSpanLinks({ theme, }; + const isOld = isPartialSpanOrTraceData(node.value.start_timestamp); + const linksAsAttributes: TraceItemResponseAttribute[] = links.flatMap( (link, linkIndex) => { const prefix = `span_link_${linkIndex + 1}`; const isCrossTraceLink = !tree || link.traceId !== traceId; - const isOld = isPartialSpanOrTraceData(node.value.start_timestamp); customRenderers[`${prefix}.trace_id`] = () => { const traceLabel = traceIdRenderer({trace: link.traceId}, renderBaggage); From 2bdabe5ad76e5926fa2b347c5d62dc043ffdc7ff Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 11:37:56 -0300 Subject: [PATCH 32/39] fix(traces): Add missing disabled tooltips and simplify old-data checks - Add DisabledTraceLink fallback in ProfileEventDetails for old transactions - Add tooltip to disabled dropdown items in tagsHeatMap - Remove redundant && guard before isPartialSpanOrTraceData calls since the utility already returns false for undefined --- .../flamegraphDrawer/profileDetails.tsx | 8 ++++++++ .../views/dashboards/datasetConfig/spans.tsx | 2 +- static/app/views/discover/table/tableView.tsx | 2 +- .../pageOverviewWebVitalsDetailPanel.tsx | 2 +- .../tables/pageSamplePerformanceTable.tsx | 2 +- .../components/tables/eventSamplesTable.tsx | 2 +- .../eapSampledEventsTable.tsx | 2 +- .../transactionEvents/eventsTable.tsx | 2 +- .../transactionTags/tagsHeatMap.tsx | 19 ++++++++++++++++--- .../performance/transactionSummary/utils.tsx | 7 ++----- 10 files changed, 33 insertions(+), 15 deletions(-) diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index 5f5ba305ddd240..e148525c22a824 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -426,6 +426,14 @@ function ProfileEventDetails({
); } + if (isPartialSpanOrTraceData(transaction?.endTimestamp)) { + return ( + + {label}: + {value} + + ); + } } if (key === 'projectID') { if (project && organization) { diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index 1181256da04d9d..9c1a85d8818fca 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -430,7 +430,7 @@ function renderEventInTraceView( return {getShortEventId(spanId)}; } - if (data.timestamp && isPartialSpanOrTraceData(data.timestamp)) { + if (isPartialSpanOrTraceData(data.timestamp)) { return ( {getShortEventId(spanId)} diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index 86586040ddd332..6e0da5edca4a1a 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -202,7 +202,7 @@ export function TableView(props: TableViewProps) { ); } - if (dataRow.timestamp && isPartialSpanOrTraceData(dataRow.timestamp)) { + if (isPartialSpanOrTraceData(dataRow.timestamp)) { return [ {value} diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index d936794cc814f5..8d360cbb366fd2 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -284,7 +284,7 @@ export function PageOverviewWebVitalsDetailPanel({ return {NO_VALUE}; } if (key === 'id') { - const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); + const isOld = isPartialSpanOrTraceData(row.timestamp); const eventTarget = !isOld && project?.slug && diff --git a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx index ff76725af83924..af04aab6a38975 100644 --- a/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx +++ b/static/app/views/insights/browser/webVitals/components/tables/pageSamplePerformanceTable.tsx @@ -481,7 +481,7 @@ export function PageSamplePerformanceTable({transaction, search, limit = 9}: Pro if (key === 'id' || key === SpanFields.SPAN_DESCRIPTION) { if (key === 'id' && 'id' in row) { - const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); + const isOld = isPartialSpanOrTraceData(row.timestamp); if (isOld) { return ( {getShortEventId(row.id)} diff --git a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx index 365d1c21428f11..20699af9139426 100644 --- a/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx +++ b/static/app/views/insights/mobile/screenload/components/tables/eventSamplesTable.tsx @@ -77,7 +77,7 @@ export function EventSamplesTable({ } if (column.key === eventIdKey) { - if (row.timestamp && isPartialSpanOrTraceData(row.timestamp)) { + if (isPartialSpanOrTraceData(row.timestamp)) { return ( {row[eventIdKey].slice(0, 8)} diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx index 0cdc74e1c0d703..f8ff29c543e315 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eapSampledEventsTable.tsx @@ -285,7 +285,7 @@ function renderBodyCell( if (column.key === 'trace') { const traceId = row.trace?.toString() ?? ''; if (traceId) { - const isOld = row.timestamp && isPartialSpanOrTraceData(row.timestamp); + const isOld = isPartialSpanOrTraceData(row.timestamp); let rendered: React.ReactNode = traceId; if (meta?.fields) { diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 83456d52fd5640..3e54333289e8f4 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -233,7 +233,7 @@ export function EventsTable({ if (field === 'id' || field === 'trace') { const isIssue = !!issueId; - const isOld = dataRow.timestamp && isPartialSpanOrTraceData(dataRow.timestamp); + const isOld = isPartialSpanOrTraceData(dataRow.timestamp); let target: LocationDescriptor | null = null; if (isIssue && !isRegressionIssue && field === 'id') { target = { diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx index a4f9609d2550ba..1fced81343c494 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx @@ -11,6 +11,7 @@ import type {Location, LocationDescriptor} from 'history'; import memoize from 'lodash/memoize'; import {Flex} from '@sentry/scraps/layout'; +import {Tooltip} from '@sentry/scraps/tooltip'; import {HeatMapChart} from 'sentry/components/charts/heatMapChart'; import {HeaderTitleLegend} from 'sentry/components/charts/styles'; @@ -356,8 +357,7 @@ export function TagsHeatMap(
{transactionTableData?.data.length ? null : } {[...(transactionTableData?.data ?? [])].slice(0, 3).map(row => { - const isOld = - row.timestamp && isPartialSpanOrTraceData(row.timestamp); + const isOld = isPartialSpanOrTraceData(row.timestamp); const target = isOld ? null : generateLinkToEventInTraceView({ @@ -376,7 +376,7 @@ export function TagsHeatMap( view, }); - return ( + const dropdownItem = ( ); + + if (isOld) { + return ( + + {dropdownItem} + + ); + } + + return dropdownItem; })} {moreEventsTarget && transactionTableData && diff --git a/static/app/views/performance/transactionSummary/utils.tsx b/static/app/views/performance/transactionSummary/utils.tsx index a91ae65c74dd80..17081cddebb3d4 100644 --- a/static/app/views/performance/transactionSummary/utils.tsx +++ b/static/app/views/performance/transactionSummary/utils.tsx @@ -140,10 +140,7 @@ export function generateTraceLink(dateSelection: any, view?: DomainView) { location: Location ): LocationDescriptor => { const traceId = tableRow.trace ? `${tableRow.trace}` : ''; - if ( - !traceId || - (tableRow.timestamp && isPartialSpanOrTraceData(tableRow.timestamp)) - ) { + if (!traceId || isPartialSpanOrTraceData(tableRow.timestamp)) { return {}; } @@ -166,7 +163,7 @@ export function generateTransactionIdLink(view?: DomainView) { location: Location, spanId?: string ): LocationDescriptor => { - if (tableRow.timestamp && isPartialSpanOrTraceData(tableRow.timestamp)) { + if (isPartialSpanOrTraceData(tableRow.timestamp)) { return {}; } From 4390ae47e4bb8ee1d92ab3223a24055d69c5f594 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 11:55:57 -0300 Subject: [PATCH 33/39] fix(traces): Skip old-trace error when redirecting to issue events Events with a groupID and eventID redirect to the issue event page, so the old-trace check should not block them. Extract the redirect condition into a variable and gate the error screen accordingly. Co-Authored-By: Claude Opus 4.6 --- static/app/views/discover/eventDetails.tsx | 6 ++++-- static/app/views/projectEventRedirect.tsx | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/static/app/views/discover/eventDetails.tsx b/static/app/views/discover/eventDetails.tsx index 36cb212cf49d6e..cc4775fea91869 100644 --- a/static/app/views/discover/eventDetails.tsx +++ b/static/app/views/discover/eventDetails.tsx @@ -50,11 +50,12 @@ export default function EventDetails() { const traceTimestamp = event ? getEventTimestampInSeconds(event) : undefined; const isOldTrace = traceTimestamp ? isPartialSpanOrTraceData(traceTimestamp) : false; + const willRedirectToIssueEvent = !!event?.groupID && !!event?.eventID; useEffect(() => { if (!event) return; - if (event.groupID && event.eventID) { + if (willRedirectToIssueEvent) { navigate({ pathname: `/organizations/${organization.slug}/issues/${event.groupID}/events/${event.eventID}`, }); @@ -83,6 +84,7 @@ export default function EventDetails() { location, navigate, traceTimestamp, + willRedirectToIssueEvent, ]); if (error) { @@ -106,7 +108,7 @@ export default function EventDetails() { ); } - if (event && isOldTrace) { + if (event && isOldTrace && !willRedirectToIssueEvent) { return ( ); diff --git a/static/app/views/projectEventRedirect.tsx b/static/app/views/projectEventRedirect.tsx index fff54973b8c4f6..e2ccaf3e292d3c 100644 --- a/static/app/views/projectEventRedirect.tsx +++ b/static/app/views/projectEventRedirect.tsx @@ -61,6 +61,7 @@ export function ProjectEventRedirect() { const traceTimestamp = event ? getEventTimestampInSeconds(event) : undefined; const isOldTrace = traceTimestamp ? isPartialSpanOrTraceData(traceTimestamp) : false; + const willRedirectToIssueEvent = !!event?.groupID && !!event?.eventID; useEffect(() => { if (!event) { @@ -68,7 +69,7 @@ export function ProjectEventRedirect() { } // If the event has a group ID, navigate to the issue event page - if (event.groupID && event.eventID) { + if (willRedirectToIssueEvent) { if ('feedback' in event.contexts) { navigate( { @@ -123,6 +124,7 @@ export function ProjectEventRedirect() { location, navigate, traceTimestamp, + willRedirectToIssueEvent, ]); if (error) { @@ -148,7 +150,7 @@ export function ProjectEventRedirect() { ); } - if (event && isOldTrace) { + if (event && isOldTrace && !willRedirectToIssueEvent) { return ( ); From a236ec11034bdee955a9f264e05e8ba3b2c75ba3 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 11:59:13 -0300 Subject: [PATCH 34/39] fix(profiling): Disable trace link for old data in profile events table Replace plain container with DisabledTraceLink for trace IDs when the transaction timestamp indicates old/partial data, consistent with the app-wide pattern for disabling links to old traces. Refs EXP-647 Co-Authored-By: Claude Sonnet 4.6 --- static/app/components/profiling/profileEventsTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/components/profiling/profileEventsTable.tsx b/static/app/components/profiling/profileEventsTable.tsx index bc53cae413b5ec..8ad4263d46d5e5 100644 --- a/static/app/components/profiling/profileEventsTable.tsx +++ b/static/app/components/profiling/profileEventsTable.tsx @@ -210,7 +210,7 @@ function ProfileEventsCell(props: ProfileEventsCellProps const txTimestamp = getTimeStampFromTableDateField(props.dataRow.timestamp); if (isPartialSpanOrTraceData(txTimestamp)) { - return {transactionId}; + return {transactionId}; } return ( From d167f9ed3bf806c4cef8c0e2193f624401f4cd19 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 12:08:10 -0300 Subject: [PATCH 35/39] fix(web-vitals): Show trace ID consistently in disabled and linked states In the Web Vitals detail panel, the disabled trace link was showing the span ID (row.id) while the active link showed the trace ID (row.trace), causing the same column to display different identifiers depending on data age. Co-Authored-By: Claude Sonnet 4.6 --- .../webVitals/components/pageOverviewWebVitalsDetailPanel.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index 8d360cbb366fd2..f83ab9d4d3e32a 100644 --- a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx +++ b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx @@ -300,7 +300,9 @@ export function PageOverviewWebVitalsDetailPanel({ if (isOld) { return ( - {getShortEventId(row.id)} + + {getShortEventId(row.trace)} + ); } From 2b65aaccee63732844cc53b99c17142605e8976b Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 13:06:58 -0300 Subject: [PATCH 36/39] Tidy up things --- .../app/components/events/contexts/knownContext/trace.tsx | 7 +------ .../eventComparison/eventDisplay.tsx | 3 ++- .../events/interfaces/performance/eventTraceView.tsx | 3 +-- .../interfaces/performance/spanEvidenceKeyValueList.tsx | 6 ++---- static/app/components/events/interfaces/utils.tsx | 4 +++- static/app/utils/trace/isOlderThan30Days.tsx | 4 ++-- .../dashboards/datasetConfig/errorsAndTransactions.tsx | 2 +- static/app/views/discover/eventDetails.tsx | 4 ++-- .../metrics/metricInfoTabs/metricsSamplesTableRow.tsx | 2 +- .../insights/pages/agents/hooks/useNodeDetailsLink.tsx | 2 +- static/app/views/issueDetails/traceTimeline/traceLink.tsx | 3 +-- .../issueDetails/traceTimeline/traceTimelineTooltip.tsx | 3 +-- .../newTraceDetails/issuesTraceWaterfallOverlay.tsx | 4 +--- .../traceDrawer/details/span/eapSections/attributes.tsx | 3 +-- .../app/views/performance/newTraceDetails/traceSummary.tsx | 3 +-- .../transactionEvents/eapSampledEventsTable.tsx | 3 +-- .../transactionSummary/transactionEvents/eventsTable.tsx | 1 + static/app/views/projectEventRedirect.tsx | 4 ++-- 18 files changed, 25 insertions(+), 36 deletions(-) diff --git a/static/app/components/events/contexts/knownContext/trace.tsx b/static/app/components/events/contexts/knownContext/trace.tsx index dffafeb542bb45..6d3451581fbb6c 100644 --- a/static/app/components/events/contexts/knownContext/trace.tsx +++ b/static/app/components/events/contexts/knownContext/trace.tsx @@ -69,12 +69,7 @@ export function getTraceContextData({ const traceWasSampled = data?.sampled ?? true; if (traceWasSampled) { - const eventTimestamp = getEventTimestampInSeconds(event); - const isOld = eventTimestamp - ? isPartialSpanOrTraceData(eventTimestamp) - : false; - - if (isOld) { + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { return { key: ctxKey, subject: t('Trace ID'), diff --git a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx index 03e40b9076c533..db184296f27922 100644 --- a/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx +++ b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx @@ -209,7 +209,6 @@ function EventDisplay({ const waterfallModel = new WaterfallModel(eventData); const traceSlug = eventData.contexts?.trace?.trace_id ?? ''; - const isOld = isPartialSpanOrTraceData(eventData.endTimestamp); const fullEventTarget = generateLinkToEventInTraceView({ eventId: eventData.id, traceSlug, @@ -237,6 +236,8 @@ function EventDisplay({ ); + + const isOld = isPartialSpanOrTraceData(eventData.endTimestamp); const minimap = isOld ? ( minimapContent ) : ( diff --git a/static/app/components/events/interfaces/performance/eventTraceView.tsx b/static/app/components/events/interfaces/performance/eventTraceView.tsx index 85d38a2f75187d..569df846c5805e 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.tsx @@ -177,8 +177,7 @@ export function EventTraceView({group, event, organization}: EventTraceViewProps ); const hasTracePreviewFeature = organization.features.includes('profiling'); - const eventTimestamp = getEventTimestampInSeconds(event); - const isOld = eventTimestamp ? isPartialSpanOrTraceData(eventTimestamp) : false; + const isOld = isPartialSpanOrTraceData(getEventTimestampInSeconds(event)); return ( {getShortEventId(id)} diff --git a/static/app/views/discover/eventDetails.tsx b/static/app/views/discover/eventDetails.tsx index cc4775fea91869..7ce8bdb840dd42 100644 --- a/static/app/views/discover/eventDetails.tsx +++ b/static/app/views/discover/eventDetails.tsx @@ -48,8 +48,8 @@ export default function EventDetails() { {staleTime: 2 * 60 * 1000} // 2 minutes in milliseonds ); - const traceTimestamp = event ? getEventTimestampInSeconds(event) : undefined; - const isOldTrace = traceTimestamp ? isPartialSpanOrTraceData(traceTimestamp) : false; + const traceTimestamp = getEventTimestampInSeconds(event); + const isOldTrace = isPartialSpanOrTraceData(traceTimestamp); const willRedirectToIssueEvent = !!event?.groupID && !!event?.eventID; useEffect(() => { diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx index 6195139df343f6..c4204e1661480f 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx @@ -151,7 +151,7 @@ export function SampleTableRow({ const spanIdToUse = oldSpanId || spanId; const strippedLocation = stripMetricParamsFromLocation(location); - if (timestamp && isPartialSpanOrTraceData(timestamp)) { + if (isPartialSpanOrTraceData(timestamp)) { return ( {getShortEventId(traceId)} diff --git a/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx b/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx index 0f135a1d0fec93..6cef7c22ba34ff 100644 --- a/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx +++ b/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx @@ -26,7 +26,7 @@ export function useNodeDetailsLink({ const targetId = node?.transactionId; const timestamp = node?.startTimestamp; - const isDisabled = timestamp ? isPartialSpanOrTraceData(timestamp) : false; + const isDisabled = isPartialSpanOrTraceData(timestamp); const url = getTraceDetailsUrl({ source, diff --git a/static/app/views/issueDetails/traceTimeline/traceLink.tsx b/static/app/views/issueDetails/traceTimeline/traceLink.tsx index a6ee290e756162..4547c9003ab91f 100644 --- a/static/app/views/issueDetails/traceTimeline/traceLink.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceLink.tsx @@ -57,8 +57,7 @@ export function TraceLink({event}: TraceLinkProps) { ); } - const eventTimestamp = getEventTimestampInSeconds(event); - if (eventTimestamp && isPartialSpanOrTraceData(eventTimestamp)) { + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { return ( diff --git a/static/app/views/issueDetails/traceTimeline/traceTimelineTooltip.tsx b/static/app/views/issueDetails/traceTimeline/traceTimelineTooltip.tsx index b6517380436574..02bad4affa2866 100644 --- a/static/app/views/issueDetails/traceTimeline/traceTimelineTooltip.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceTimelineTooltip.tsx @@ -129,9 +129,8 @@ function MoreEventsLink({ const organization = useOrganization(); const location = useLocation(); const area = useAnalyticsArea(); - const eventTimestamp = getEventTimestampInSeconds(event); - if (eventTimestamp && isPartialSpanOrTraceData(eventTimestamp)) { + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { return ( {tn('%s more event', '%s more events', filteredTimelineEvents.length - 3)} diff --git a/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx b/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx index 6424892ff539b4..c90fc9189e5509 100644 --- a/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx +++ b/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx @@ -133,9 +133,7 @@ export function IssueTraceWaterfallOverlay({ : [`txn-${event.eventID}`]; const baseLink = getTraceLinkForIssue(traceTarget, baseNodePath); - const eventTimestamp = getEventTimestampInSeconds(event); - const isOld = eventTimestamp ? isPartialSpanOrTraceData(eventTimestamp) : false; - if (isOld) { + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { return null; } diff --git a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx index 51d665e6b91e9f..bd5b36a5519a6f 100644 --- a/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx +++ b/static/app/views/performance/newTraceDetails/traceDrawer/details/span/eapSections/attributes.tsx @@ -140,9 +140,8 @@ export function Attributes({ }, [FieldKey.TRACE]: (props: CustomRenderersProps) => { const traceSlug = String(props.item.value); - const isOld = isPartialSpanOrTraceData(node.value.start_timestamp); - if (isOld) { + if (isPartialSpanOrTraceData(node.value.start_timestamp)) { return {props.item.value}; } diff --git a/static/app/views/performance/newTraceDetails/traceSummary.tsx b/static/app/views/performance/newTraceDetails/traceSummary.tsx index cc9dc519f1ae9f..10d5c27f47e567 100644 --- a/static/app/views/performance/newTraceDetails/traceSummary.tsx +++ b/static/app/views/performance/newTraceDetails/traceSummary.tsx @@ -83,7 +83,6 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { const timestamp = Array.isArray(location.query.timestamp) ? location.query.timestamp[0] : location.query.timestamp; - const isOld = !!timestamp && isPartialSpanOrTraceData(timestamp); if (traceContent.isPending) { return ; @@ -146,7 +145,7 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { {investigations.map((span, idx) => ( - {isOld ? ( + {isPartialSpanOrTraceData(timestamp) ? ( {span.spanOp} ) : ( {rendered}; } diff --git a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 3e54333289e8f4..b2e89437660e23 100644 --- a/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx +++ b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx @@ -235,6 +235,7 @@ export function EventsTable({ const isIssue = !!issueId; const isOld = isPartialSpanOrTraceData(dataRow.timestamp); let target: LocationDescriptor | null = null; + if (isIssue && !isRegressionIssue && field === 'id') { target = { pathname: `/organizations/${organization.slug}/issues/${issueId}/events/${dataRow.id}/`, diff --git a/static/app/views/projectEventRedirect.tsx b/static/app/views/projectEventRedirect.tsx index e2ccaf3e292d3c..e586732cba7bc7 100644 --- a/static/app/views/projectEventRedirect.tsx +++ b/static/app/views/projectEventRedirect.tsx @@ -59,8 +59,8 @@ export function ProjectEventRedirect() { {staleTime: 2 * 60 * 1000} // 2 minutes in milliseconds ); - const traceTimestamp = event ? getEventTimestampInSeconds(event) : undefined; - const isOldTrace = traceTimestamp ? isPartialSpanOrTraceData(traceTimestamp) : false; + const traceTimestamp = getEventTimestampInSeconds(event); + const isOldTrace = isPartialSpanOrTraceData(traceTimestamp); const willRedirectToIssueEvent = !!event?.groupID && !!event?.eventID; useEffect(() => { From fe77b0a7f7ad4a1f2df76e18cad86f169708ad17 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 13:12:42 -0300 Subject: [PATCH 37/39] Create tooltip wrapper component --- .../components/explore/disabledTraceLink.tsx | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/static/app/components/explore/disabledTraceLink.tsx b/static/app/components/explore/disabledTraceLink.tsx index 98b864d8535144..59177dfb9b6ecf 100644 --- a/static/app/components/explore/disabledTraceLink.tsx +++ b/static/app/components/explore/disabledTraceLink.tsx @@ -2,7 +2,7 @@ import type {LocationDescriptorObject} from 'history'; import {Link} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; -import {Tooltip} from '@sentry/scraps/tooltip'; +import {Tooltip, type TooltipProps} from '@sentry/scraps/tooltip'; import {t, tct} from 'sentry/locale'; @@ -12,31 +12,29 @@ interface DisabledTraceLinkProps { similarEventsUrl?: LocationDescriptorObject | string; } -/** - * Renders a non-clickable, muted trace/span link with a tooltip - * explaining that the data is older than 30 days. - * - * Optionally includes a "View similar traces/spans" link in the tooltip. - */ -export function DisabledTraceLink({ +interface DisabledTraceLinkTooltipProps extends Omit { + type: DisabledTraceLinkProps['type']; + similarEventsUrl?: DisabledTraceLinkProps['similarEventsUrl']; +} + +export function DisabledTraceLinkTooltip({ children, type, similarEventsUrl, -}: DisabledTraceLinkProps) { - let tooltipContent: React.ReactNode; - - if (type === 'trace') { - tooltipContent = similarEventsUrl ? ( - - {tct('Trace is older than 30 days. [similarLink] in the past 24 hours.', { - similarLink: {t('View similar traces')}, - })} - - ) : ( - {t('Trace is older than 30 days')} - ); - } else { - tooltipContent = similarEventsUrl ? ( + ...tooltipProps +}: DisabledTraceLinkTooltipProps) { + const title = + type === 'trace' ? ( + similarEventsUrl ? ( + + {tct('Trace is older than 30 days. [similarLink] in the past 24 hours.', { + similarLink: {t('View similar traces')}, + })} + + ) : ( + {t('Trace is older than 30 days')} + ) + ) : similarEventsUrl ? ( {tct('Span is older than 30 days. [similarLink] in the past 24 hours.', { similarLink: {t('View similar spans')}, @@ -45,13 +43,30 @@ export function DisabledTraceLink({ ) : ( {t('Span is older than 30 days')} ); - } return ( - + + {children} + + ); +} + +/** + * Renders a non-clickable, muted trace/span link with a tooltip + * explaining that the data is older than 30 days. + * + * Optionally includes a "View similar traces/spans" link in the tooltip. + */ +export function DisabledTraceLink({ + children, + type, + similarEventsUrl, +}: DisabledTraceLinkProps) { + return ( + {children} - + ); } From 354cc683b98b8c738b7896c110c4c12b696a2a53 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 13:20:12 -0300 Subject: [PATCH 38/39] Use new tooltip helper component --- .../interfaces/performance/eventTraceView.tsx | 9 +++------ .../performance/spanEvidenceKeyValueList.tsx | 15 +++++---------- .../components/events/profileEventEvidence.tsx | 9 +++------ .../profiling/continuousProfileHeader.tsx | 9 +++------ static/app/components/profiling/profileHeader.tsx | 9 +++------ .../issueDetails/traceTimeline/traceLink.tsx | 6 +++--- .../transactionTags/tagsHeatMap.tsx | 9 +++------ 7 files changed, 23 insertions(+), 43 deletions(-) diff --git a/static/app/components/events/interfaces/performance/eventTraceView.tsx b/static/app/components/events/interfaces/performance/eventTraceView.tsx index 569df846c5805e..1008e1a7d5c7c8 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.tsx @@ -3,13 +3,13 @@ import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; import {Grid} from '@sentry/scraps/layout'; -import {Tooltip} from '@sentry/scraps/tooltip'; import { isWebVitalsEvent, TRACE_WATERFALL_PREFERENCES_KEY, } from 'sentry/components/events/interfaces/performance/utils'; import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import {t} from 'sentry/locale'; import {type Event} from 'sentry/types/event'; @@ -185,10 +185,7 @@ export function EventTraceView({group, event, organization}: EventTraceViewProps title={t('Trace Preview')} actions={ - + {t('View Full Trace')} - + } > diff --git a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx index 9de3810368dc3d..cf12c3aa28882e 100644 --- a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx +++ b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx @@ -26,6 +26,7 @@ import { } from 'sentry/components/events/interfaces/spans/utils'; import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; import {AnnotatedText} from 'sentry/components/events/meta/annotatedText'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {t} from 'sentry/locale'; import type {Entry, EntryRequest, Event, EventTransaction} from 'sentry/types/event'; import {EntryType} from 'sentry/types/event'; @@ -378,14 +379,11 @@ function AIDetectedSpanEvidence({ const isOld = isPartialSpanOrTraceData(getEventTimestampInSeconds(event)); const actionButton = projectSlug ? ( - + {t('View Full Trace')} - + ) : undefined; const transactionRow = makeRow( @@ -626,14 +624,11 @@ const makeTransactionNameRow = ( const isOld = isPartialSpanOrTraceData(getEventTimestampInSeconds(event)); const actionButton = projectSlug ? ( - + {t('View Full Trace')} - + ) : undefined; return makeRow( diff --git a/static/app/components/events/profileEventEvidence.tsx b/static/app/components/events/profileEventEvidence.tsx index 5e55601ddb66e3..aeb8fab35db7a9 100644 --- a/static/app/components/events/profileEventEvidence.tsx +++ b/static/app/components/events/profileEventEvidence.tsx @@ -1,8 +1,8 @@ import {LinkButton} from '@sentry/scraps/button'; -import {Tooltip} from '@sentry/scraps/tooltip'; import {KeyValueList} from 'sentry/components/events/interfaces/keyValueList'; import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {IconProfiling} from 'sentry/icons'; import {t} from 'sentry/locale'; import type {Event} from 'sentry/types/event'; @@ -33,10 +33,7 @@ export function ProfileEventEvidence({event, projectSlug}: ProfileEvidenceProps) key: 'Transaction Name', value: evidenceData.transactionName, actionButton: traceSlug ? ( - + {t('View Transaction')} - + ) : null, }, ] diff --git a/static/app/components/profiling/continuousProfileHeader.tsx b/static/app/components/profiling/continuousProfileHeader.tsx index 4159c0ff64aca7..6cc0c1d993dd85 100644 --- a/static/app/components/profiling/continuousProfileHeader.tsx +++ b/static/app/components/profiling/continuousProfileHeader.tsx @@ -2,8 +2,8 @@ import {useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; -import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import * as Layout from 'sentry/components/layouts/thirds'; import type {ProfilingBreadcrumbsProps} from 'sentry/components/profiling/profilingBreadcrumbs'; @@ -57,10 +57,7 @@ export function ContinuousProfileHeader({transaction}: ContinuousProfileHeader) {transactionTarget && ( - + {t('Go to Trace')} - + )} diff --git a/static/app/components/profiling/profileHeader.tsx b/static/app/components/profiling/profileHeader.tsx index 558aec730c16a6..d95dfe745633ca 100644 --- a/static/app/components/profiling/profileHeader.tsx +++ b/static/app/components/profiling/profileHeader.tsx @@ -2,8 +2,8 @@ import {useCallback, useMemo} from 'react'; import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; -import {Tooltip} from '@sentry/scraps/tooltip'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import * as Layout from 'sentry/components/layouts/thirds'; import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs'; @@ -95,10 +95,7 @@ function ProfileHeader({transaction, projectId, eventId}: ProfileHeaderProps) { {transactionTarget && ( - + {t('Go to Trace')} - + )} diff --git a/static/app/views/issueDetails/traceTimeline/traceLink.tsx b/static/app/views/issueDetails/traceTimeline/traceLink.tsx index 4547c9003ab91f..e7e9c515db036c 100644 --- a/static/app/views/issueDetails/traceTimeline/traceLink.tsx +++ b/static/app/views/issueDetails/traceTimeline/traceLink.tsx @@ -2,10 +2,10 @@ import styled from '@emotion/styled'; import {Link} from '@sentry/scraps/link'; import {Text} from '@sentry/scraps/text'; -import {Tooltip} from '@sentry/scraps/tooltip'; import {useAnalyticsArea} from 'sentry/components/analyticsArea'; import {getEventTimestampInSeconds} from 'sentry/components/events/interfaces/utils'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {QuestionTooltip} from 'sentry/components/questionTooltip'; import {generateTraceTarget} from 'sentry/components/quickTrace/utils'; import {IconChevron} from 'sentry/icons'; @@ -59,12 +59,12 @@ export function TraceLink({event}: TraceLinkProps) { if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { return ( - + {t('View Full Trace')} - + ); } diff --git a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx index 1fced81343c494..c47171d2081a7b 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx @@ -11,12 +11,12 @@ import type {Location, LocationDescriptor} from 'history'; import memoize from 'lodash/memoize'; import {Flex} from '@sentry/scraps/layout'; -import {Tooltip} from '@sentry/scraps/tooltip'; import {HeatMapChart} from 'sentry/components/charts/heatMapChart'; import {HeaderTitleLegend} from 'sentry/components/charts/styles'; import {TransitionChart} from 'sentry/components/charts/transitionChart'; import {TransparentLoadingMask} from 'sentry/components/charts/transparentLoadingMask'; +import {DisabledTraceLinkTooltip} from 'sentry/components/explore/disabledTraceLink'; import {LoadingIndicator} from 'sentry/components/loadingIndicator'; import {MenuItem} from 'sentry/components/menuItem'; import {Overlay, PositionWrapper} from 'sentry/components/overlay'; @@ -397,12 +397,9 @@ export function TagsHeatMap( if (isOld) { return ( - + {dropdownItem} - + ); } From fc109903c63442c88b203d34da2a0dde0133afc4 Mon Sep 17 00:00:00 2001 From: nsdeschenes Date: Wed, 8 Apr 2026 13:47:52 -0300 Subject: [PATCH 39/39] test(performance): Fix spanEvidenceKeyValueList tests after trace link disable feature TransactionEventBuilder defaulted startTimestamp to 0 (epoch), which isPartialSpanOrTraceData correctly identifies as old data (>30 days), disabling the 'View Full Trace' button. Add dateCreated with a current timestamp so the isOld check sees a recent event, leaving startTimestamp and endTimestamp unchanged for duration calculations. Co-Authored-By: Claude Sonnet 4.6 --- tests/js/sentry-test/performance/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/js/sentry-test/performance/utils.ts b/tests/js/sentry-test/performance/utils.ts index 80568d0270218d..c5642a71782c01 100644 --- a/tests/js/sentry-test/performance/utils.ts +++ b/tests/js/sentry-test/performance/utils.ts @@ -51,6 +51,7 @@ export class TransactionEventBuilder { type: EventOrGroupType.TRANSACTION, startTimestamp: 0, endTimestamp: transactionSettings?.duration ?? 0, + dateCreated: new Date().toISOString(), contexts: { trace: { trace_id: this.TRACE_ID,