@@ -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,