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 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/components/events/contexts/knownContext/trace.tsx b/static/app/components/events/contexts/knownContext/trace.tsx index 62e6c5d34fcebf..6d3451581fbb6c 100644 --- a/static/app/components/events/contexts/knownContext/trace.tsx +++ b/static/app/components/events/contexts/knownContext/trace.tsx @@ -3,12 +3,15 @@ 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 {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'; 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 +69,14 @@ export function getTraceContextData({ const traceWasSampled = data?.sampled ?? true; if (traceWasSampled) { + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { + 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/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx b/static/app/components/events/eventStatisticalDetector/eventComparison/eventDisplay.tsx index c52241a8b18cb6..db184296f27922 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'; @@ -215,6 +216,34 @@ function EventDisplay({ location, organization, }); + + const minimapContent = ( + + + + + + ); + + const isOld = isPartialSpanOrTraceData(eventData.endTimestamp); + const minimap = isOld ? ( + minimapContent + ) : ( + {minimapContent} + ); + return (
@@ -245,13 +274,19 @@ function EventDisplay({ )} /> - } - /> + + } + /> +
@@ -279,25 +314,7 @@ function EventDisplay({
- - - - - - - + {minimap} 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..1008e1a7d5c7c8 100644 --- a/static/app/components/events/interfaces/performance/eventTraceView.tsx +++ b/static/app/components/events/interfaces/performance/eventTraceView.tsx @@ -9,6 +9,7 @@ import { 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'; @@ -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,7 @@ export function EventTraceView({group, event, organization}: EventTraceViewProps ); const hasTracePreviewFeature = organization.features.includes('profiling'); + const isOld = isPartialSpanOrTraceData(getEventTimestampInSeconds(event)); return ( - - {t('View Full Trace')} - + + + {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 670007744daf49..cf12c3aa28882e 100644 --- a/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx +++ b/static/app/components/events/interfaces/performance/spanEvidenceKeyValueList.tsx @@ -24,7 +24,9 @@ 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 {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'; @@ -40,6 +42,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 +377,13 @@ function AIDetectedSpanEvidence({ organization, }); + const isOld = isPartialSpanOrTraceData(getEventTimestampInSeconds(event)); const actionButton = projectSlug ? ( - - {t('View Full Trace')} - + + + {t('View Full Trace')} + + ) : undefined; const transactionRow = makeRow( @@ -616,10 +622,13 @@ const makeTransactionNameRow = ( organization, }); + const isOld = isPartialSpanOrTraceData(getEventTimestampInSeconds(event)); const actionButton = projectSlug ? ( - - {t('View Full Trace')} - + + + {t('View Full Trace')} + + ) : undefined; return makeRow( diff --git a/static/app/components/events/interfaces/utils.tsx b/static/app/components/events/interfaces/utils.tsx index 32182e6b382685..aaa88e88c7daa5 100644 --- a/static/app/components/events/interfaces/utils.tsx +++ b/static/app/components/events/interfaces/utils.tsx @@ -372,7 +372,9 @@ const timestampsFieldCandidates = [ 'endTimestamp', ]; -export function getEventTimestampInSeconds(event: Event): number | undefined { +export function getEventTimestampInSeconds(event?: Event): number | undefined { + if (!event) return undefined; + for (const key of timestampsFieldCandidates) { if (key in event) { const value = event[key as keyof Event]; 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 6acbfbb5bc1d3d..aeb8fab35db7a9 100644 --- a/static/app/components/events/profileEventEvidence.tsx +++ b/static/app/components/events/profileEventEvidence.tsx @@ -1,11 +1,14 @@ import {LinkButton} from '@sentry/scraps/button'; 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'; 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 +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 traceTimestamp = evidenceData.timestamp ?? getEventTimestampInSeconds(event); + const isOld = isPartialSpanOrTraceData(traceTimestamp); const keyValueListData = [ ...(evidenceData.transactionId && evidenceData.transactionName @@ -28,18 +33,24 @@ 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/explore/disabledTraceLink.tsx b/static/app/components/explore/disabledTraceLink.tsx new file mode 100644 index 00000000000000..59177dfb9b6ecf --- /dev/null +++ b/static/app/components/explore/disabledTraceLink.tsx @@ -0,0 +1,72 @@ +import type {LocationDescriptorObject} from 'history'; + +import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; +import {Tooltip, type TooltipProps} from '@sentry/scraps/tooltip'; + +import {t, tct} from 'sentry/locale'; + +interface DisabledTraceLinkProps { + children: React.ReactNode; + type: 'trace' | 'span'; + similarEventsUrl?: LocationDescriptorObject | string; +} + +interface DisabledTraceLinkTooltipProps extends Omit { + type: DisabledTraceLinkProps['type']; + similarEventsUrl?: DisabledTraceLinkProps['similarEventsUrl']; +} + +export function DisabledTraceLinkTooltip({ + children, + type, + 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')}, + })} + + ) : ( + {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} + + + ); +} diff --git a/static/app/components/profiling/continuousProfileHeader.tsx b/static/app/components/profiling/continuousProfileHeader.tsx index 0c3efebf108d21..6cc0c1d993dd85 100644 --- a/static/app/components/profiling/continuousProfileHeader.tsx +++ b/static/app/components/profiling/continuousProfileHeader.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import {LinkButton} from '@sentry/scraps/button'; +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'; @@ -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,16 @@ 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..e148525c22a824 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'; @@ -19,6 +20,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'; @@ -275,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; @@ -283,11 +295,7 @@ function TransactionEventDetails({ { key: 'transaction', label: t('Transaction'), - value: transactionTarget ? ( - {transaction.title} - ) : ( - transaction.title - ), + value: transactionValue, }, { key: 'timestamp', @@ -410,7 +418,7 @@ function ProfileEventDetails({ organization, }) : null; - if (transactionTarget) { + if (transactionTarget && !isPartialSpanOrTraceData(transaction?.endTimestamp)) { return ( {label}: @@ -418,6 +426,14 @@ function ProfileEventDetails({ ); } + if (isPartialSpanOrTraceData(transaction?.endTimestamp)) { + return ( + + {label}: + {value} + + ); + } } if (key === 'projectID') { if (project && organization) { 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..8ad4263d46d5e5 100644 --- a/static/app/components/profiling/profileEventsTable.tsx +++ b/static/app/components/profiling/profileEventsTable.tsx @@ -5,6 +5,7 @@ import {Link} from '@sentry/scraps/link'; 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 { @@ -28,6 +29,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 +177,12 @@ 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')} + + )} diff --git a/static/app/utils/trace/isOlderThan30Days.spec.tsx b/static/app/utils/trace/isOlderThan30Days.spec.tsx new file mode 100644 index 00000000000000..6b897e8b930f29 --- /dev/null +++ b/static/app/utils/trace/isOlderThan30Days.spec.tsx @@ -0,0 +1,60 @@ +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: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('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 new file mode 100644 index 00000000000000..2783aa1857fffd --- /dev/null +++ b/static/app/utils/trace/isOlderThan30Days.tsx @@ -0,0 +1,41 @@ +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; + } + + 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. + * + * Handles timestamps in seconds, milliseconds, or ISO string format. + */ +export function isPartialSpanOrTraceData( + timestamp: string | number | undefined | null +): boolean { + if (timestamp === undefined || timestamp === null) { + return false; + } + const now = moment(); + const timestampDate = moment(normalizeTimestamp(timestamp)); + if (!timestampDate.isValid()) { + return false; + } + return now.diff(timestampDate, 'days') >= TRACE_DATA_RETENTION_DAYS; +} diff --git a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx index e2ffcfd7ff1d2f..74458a693c4b8c 100644 --- a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx +++ b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx @@ -7,6 +7,7 @@ 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'; @@ -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,24 @@ export function renderTraceAsLinkable(widget?: Widget) { if (!eventView || typeof id !== 'string') { return {emptyStringValue}; } + const traceTimestamp = getTimeStampFromTableDateField( + data['max(timestamp)'] ?? data.timestamp + ); + + if (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..9c1a85d8818fca 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -2,6 +2,7 @@ import pickBy from 'lodash/pickBy'; import {Link} from '@sentry/scraps/link'; +import {DisabledTraceLink} from 'sentry/components/explore/disabledTraceLink'; import type {TagCollection} from 'sentry/types/group'; import type { EventsStats, @@ -30,6 +31,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 +430,14 @@ function renderEventInTraceView( return {getShortEventId(spanId)}; } + if (isPartialSpanOrTraceData(data.timestamp)) { + return ( + + {getShortEventId(spanId)} + + ); + } + const target = generateLinkToEventInTraceView({ traceSlug: data.trace, timestamp: data.timestamp, diff --git a/static/app/views/discover/eventDetails.tsx b/static/app/views/discover/eventDetails.tsx index c51e684696025f..7ce8bdb840dd42 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,10 +48,14 @@ export default function EventDetails() { {staleTime: 2 * 60 * 1000} // 2 minutes in milliseonds ); + const traceTimestamp = getEventTimestampInSeconds(event); + const isOldTrace = isPartialSpanOrTraceData(traceTimestamp); + 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}`, }); @@ -59,20 +64,28 @@ 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, + willRedirectToIssueEvent, + ]); if (error) { const notFound = error.status === 404; @@ -95,6 +108,12 @@ export default function EventDetails() { ); } + if (event && isOldTrace && !willRedirectToIssueEvent) { + return ( + + ); + } + if ( isPending || (!isPending && event) // Prevents flash of loading error below once event is loaded successfully 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 3c39f68ebdac7d..6e0da5edca4a1a 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -9,6 +9,7 @@ import {Link} from '@sentry/scraps/link'; 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'; @@ -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,14 @@ export function TableView(props: TableViewProps) { ); } + if (isPartialSpanOrTraceData(dataRow.timestamp)) { + return [ + + {value} + , + ]; + } + target = generateLinkToEventInTraceView({ traceSlug: dataRow.trace, eventId: dataRow.id, @@ -336,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 = ( {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..1711b637f43051 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/explore/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/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTableRow.tsx index 73c2fc349ecdd5..c4204e1661480f 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'; @@ -16,6 +17,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 +151,14 @@ export function SampleTableRow({ const spanIdToUse = oldSpanId || spanId; const strippedLocation = stripMetricParamsFromLocation(location); + if (isPartialSpanOrTraceData(timestamp)) { + return ( + + {getShortEventId(traceId)} + + ); + } + const hasSpans = (telemetry?.spansCount ?? 0) > 0; const shouldGoToSpans = spanIdToUse && hasSpans; 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` ); diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx index 67294bb6344c9a..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'; @@ -22,6 +21,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 +36,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'; @@ -170,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 { @@ -236,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 050f23c2788b01..d67b7860f38512 100644 --- a/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx +++ b/static/app/views/explore/tables/tracesTable/fieldRenderers.tsx @@ -6,21 +6,22 @@ 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'; 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')` /** @@ -439,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} + ); } @@ -529,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} + ); } diff --git a/static/app/views/explore/tables/tracesTable/utils.tsx b/static/app/views/explore/tables/tracesTable/utils.tsx index e0d110be469053..d43f9bdaf5340e 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,12 +33,6 @@ 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; -} - interface GetSimilarEventsUrlArgs { organization: Organization; projectIds: number[]; diff --git a/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx b/static/app/views/insights/browser/webVitals/components/pageOverviewWebVitalsDetailPanel.tsx index 23d6181383d7ae..f83ab9d4d3e32a 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, @@ -21,6 +22,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 +284,9 @@ export function PageOverviewWebVitalsDetailPanel({ return {NO_VALUE}; } if (key === 'id') { + const isOld = isPartialSpanOrTraceData(row.timestamp); const eventTarget = + !isOld && project?.slug && generateLinkToEventInTraceView({ eventId: row.id, @@ -293,6 +297,15 @@ export function PageOverviewWebVitalsDetailPanel({ view: domainViewFilters.view, source: TraceViewSources.WEB_VITALS_MODULE, }); + if (isOld) { + return ( + + + {getShortEventId(row.trace)} + + + ); + } return ( {eventTarget ? ( 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..af04aab6a38975 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 { @@ -31,6 +32,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,17 +480,22 @@ 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, - }); - if (key === 'id' && 'id' in row) { + const isOld = 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 ( diff --git a/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx b/static/app/views/insights/common/components/samplesTable/spanSamplesTable.tsx index cf293cb0f1f086..8ca95e78499ff5 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/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'; @@ -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 ( + {spanId.slice(0, SPAN_ID_DISPLAY_LENGTH)} + + ); + } + const url = normalizeUrl( generateLinkToEventInTraceView({ eventId: transactionId, diff --git a/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.tsx b/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.tsx index 4ca9f57c7bd81f..aa6292aad9a4f9 100644 --- a/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.tsx +++ b/static/app/views/insights/common/views/spanSummaryPage/sampleList/index.tsx @@ -11,6 +11,7 @@ import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {PageAlert, PageAlertProvider} from 'sentry/utils/performance/contexts/pageAlert'; import {decodeScalar} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {isPartialSpanOrTraceData} from 'sentry/utils/trace/isOlderThan30Days'; import {useLocationQuery} from 'sentry/utils/url/useLocationQuery'; import {useLocation} from 'sentry/utils/useLocation'; import {useNavigate} from 'sentry/utils/useNavigate'; @@ -149,6 +150,9 @@ export function SampleList({groupId, moduleName, transactionRoute, referrer}: Pr 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/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..20699af9139426 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 { @@ -26,6 +27,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 +77,13 @@ export function EventSamplesTable({ } if (column.key === eventIdKey) { + if (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,9 +80,19 @@ const TraceViewDrawer = memo(function TraceViewDrawer({ {t('Abbreviated Trace')} - - {t('View in Full Trace')} - + + + {t('View in Full Trace')} + + diff --git a/static/app/views/insights/pages/agents/components/tracesTable.tsx b/static/app/views/insights/pages/agents/components/tracesTable.tsx index 64af2928fa5998..29424000cfd36d 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/explore/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/insights/pages/agents/hooks/useNodeDetailsLink.tsx b/static/app/views/insights/pages/agents/hooks/useNodeDetailsLink.tsx index 872bc19054585c..6cef7c22ba34ff 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 = isPartialSpanOrTraceData(timestamp); + + 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/issueDetails/traceTimeline/traceLink.tsx b/static/app/views/issueDetails/traceTimeline/traceLink.tsx index 31fb27b3472505..e7e9c515db036c 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 {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'; 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,17 @@ export function TraceLink({event}: TraceLinkProps) { ); } + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { + 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,47 @@ function EventItem({timelineEvent, location}: EventItemProps) { ); } +function MoreEventsLink({ + event, + filteredTimelineEvents, +}: { + event: Event; + filteredTimelineEvents: TimelineEvent[]; +}) { + const organization = useOrganization(); + const location = useLocation(); + const area = useAnalyticsArea(); + + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { + 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/issuesTraceWaterfallOverlay.tsx b/static/app/views/performance/newTraceDetails/issuesTraceWaterfallOverlay.tsx index 9ce2708c090649..c90fc9189e5509 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,10 @@ export function IssueTraceWaterfallOverlay({ : [`txn-${event.eventID}`]; const baseLink = getTraceLinkForIssue(traceTarget, baseNodePath); + if (isPartialSpanOrTraceData(getEventTimestampInSeconds(event))) { + return null; + } + return ( { const traceSlug = String(props.item.value); + + if (isPartialSpanOrTraceData(node.value.start_timestamp)) { + 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..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 @@ -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, @@ -76,11 +78,20 @@ 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; customRenderers[`${prefix}.trace_id`] = () => { + const traceLabel = traceIdRenderer({trace: link.traceId}, renderBaggage); + + if (isOld && isCrossTraceLink) { + return {traceLabel}; + } + const traceTarget = generateLinkToEventInTraceView({ organization, location, @@ -94,18 +105,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 +136,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 +149,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..10d5c27f47e567 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,9 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { const {feedback} = useFeedbackSDKIntegration(); const organization = useOrganization(); const location = useLocation(); + const timestamp = Array.isArray(location.query.timestamp) + ? location.query.timestamp[0] + : location.query.timestamp; if (traceContent.isPending) { return ; @@ -140,17 +145,21 @@ export function TraceSummarySection({traceSlug}: {traceSlug: string}) { {investigations.map((span, idx) => ( - - {span.spanOp} - + {isPartialSpanOrTraceData(timestamp) ? ( + {span.spanOp} + ) : ( + + {span.spanOp} + + )} - {span.explanation} ))} 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..f891a4b3c9bf4f 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,22 @@ function renderBodyCell( if (column.key === 'trace') { const traceId = row.trace?.toString() ?? ''; if (traceId) { + 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 (isPartialSpanOrTraceData(row.timestamp)) { + return {rendered}; + } + const target = getTraceDetailsUrl({ organization, traceSlug: traceId, @@ -292,18 +310,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/transactionEvents/eventsTable.tsx b/static/app/views/performance/transactionSummary/transactionEvents/eventsTable.tsx index 6f5a7478590da6..b2e89437660e23 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'; @@ -37,6 +38,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,11 +233,16 @@ export function EventsTable({ if (field === 'id' || field === 'trace') { 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}/`, }; + } else if (field === 'id' && isOld) { + // Trace waterfall data older than 30 days is no longer available + target = null; } else if (field === 'id') { target = generateLinkToEventInTraceView({ traceSlug: dataRow.trace?.toString()!, @@ -246,6 +253,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, @@ -261,7 +270,13 @@ 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..c47171d2081a7b 100644 --- a/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx +++ b/static/app/views/performance/transactionSummary/transactionTags/tagsHeatMap.tsx @@ -16,6 +16,7 @@ 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'; @@ -39,6 +40,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 +357,32 @@ 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, - }); - - return ( - + const isOld = 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, + }); + + const dropdownItem = ( + @@ -384,6 +394,16 @@ export function TagsHeatMap( ); + + if (isOld) { + return ( + + {dropdownItem} + + ); + } + + return dropdownItem; })} {moreEventsTarget && transactionTableData && @@ -468,6 +488,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'; @@ -477,6 +498,7 @@ function DropdownItem({ children, onSelect, allowDefaultEvent, + disabled, to, width = 'large', }: DropdownItemProps) { @@ -486,6 +508,7 @@ function DropdownItem({ to={to} onSelect={onSelect} width={width} + disabled={disabled} allowDefaultEvent={allowDefaultEvent} > diff --git a/static/app/views/performance/transactionSummary/utils.tsx b/static/app/views/performance/transactionSummary/utils.tsx index 447834de0645da..17081cddebb3d4 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,7 @@ export function generateTraceLink(dateSelection: any, view?: DomainView) { location: Location ): LocationDescriptor => { const traceId = tableRow.trace ? `${tableRow.trace}` : ''; - if (!traceId) { + if (!traceId || isPartialSpanOrTraceData(tableRow.timestamp)) { return {}; } @@ -162,6 +163,10 @@ export function generateTransactionIdLink(view?: DomainView) { location: Location, spanId?: string ): LocationDescriptor => { + if (isPartialSpanOrTraceData(tableRow.timestamp)) { + return {}; + } + return generateLinkToEventInTraceView({ eventId: tableRow.id, timestamp: tableRow.timestamp!, 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..e586732cba7bc7 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,13 +59,17 @@ export function ProjectEventRedirect() { {staleTime: 2 * 60 * 1000} // 2 minutes in milliseconds ); + const traceTimestamp = getEventTimestampInSeconds(event); + const isOldTrace = isPartialSpanOrTraceData(traceTimestamp); + const willRedirectToIssueEvent = !!event?.groupID && !!event?.eventID; + useEffect(() => { if (!event) { return; } // 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( { @@ -98,21 +103,29 @@ 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, + willRedirectToIssueEvent, + ]); if (error) { const notFound = error.status === 404; @@ -137,6 +150,12 @@ export function ProjectEventRedirect() { ); } + if (event && isOldTrace && !willRedirectToIssueEvent) { + return ( + + ); + } + if ( isPending || (!isPending && event) // Prevents flash of loading error below once event is loaded successfully 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,