diff --git a/static/app/views/explore/utils.tsx b/static/app/views/explore/utils.tsx index 1f97455b80f293..1e74bb8a849fa7 100644 --- a/static/app/views/explore/utils.tsx +++ b/static/app/views/explore/utils.tsx @@ -65,6 +65,7 @@ export function getExploreUrl({ sort, field, id, + table, title, referrer, }: { @@ -79,6 +80,7 @@ export function getExploreUrl({ referrer?: string; selection?: PageFilters; sort?: string; + table?: string; title?: string; visualize?: BaseVisualize[]; }) { @@ -100,6 +102,7 @@ export function getExploreUrl({ field, utc, id, + table, title, referrer, }; diff --git a/static/app/views/performance/newTraceDetails/index.tsx b/static/app/views/performance/newTraceDetails/index.tsx index 4bf58637ebd592..5e6307c1444465 100644 --- a/static/app/views/performance/newTraceDetails/index.tsx +++ b/static/app/views/performance/newTraceDetails/index.tsx @@ -22,6 +22,7 @@ import { } from 'sentry/views/performance/newTraceDetails/traceOurlogs'; import {TraceSummarySection} from 'sentry/views/performance/newTraceDetails/traceSummary'; import {TraceTabsAndVitals} from 'sentry/views/performance/newTraceDetails/traceTabsAndVitals'; +import {PartialTraceDataWarning} from 'sentry/views/performance/newTraceDetails/traceTypeWarnings/partialTraceDataWarning'; import {TraceWaterfall} from 'sentry/views/performance/newTraceDetails/traceWaterfall'; import { TraceLayoutTabKeys, @@ -165,6 +166,11 @@ function TraceViewImpl({traceSlug}: {traceSlug: string}) { traceSlug={traceSlug} organization={organization} /> + { + describe('when the trace is older than 30 days', () => { + beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date(2025, 0, 31)); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should render warning', () => { + const organization = OrganizationFixture(); + const start = new Date('2024-01-01T00:00:00Z').getTime() / 1e3; + + const eapTrace = makeEAPTrace([ + makeEAPSpan({ + op: 'http.server', + start_timestamp: start, + end_timestamp: start + 2, + children: [makeEAPSpan({start_timestamp: start + 1, end_timestamp: start + 4})], + }), + ]); + + render( + , + {organization} + ); + + expect(screen.getByText('Partial Trace Data:')).toBeInTheDocument(); + + expect( + screen.getByText( + 'Trace may be missing spans since the age of the trace is older than 30 days' + ) + ).toBeInTheDocument(); + + expect( + screen.getByRole('link', {name: 'Search similar traces in the past 24 hours'}) + ).toBeInTheDocument(); + + const queryString = encodeURIComponent('is_transaction:true span.op:http.server'); + expect( + screen.getByRole('link', {name: 'Search similar traces in the past 24 hours'}) + ).toHaveAttribute( + 'href', + `/organizations/${organization.slug}/explore/traces/?mode=samples&project=1&query=${queryString}&statsPeriod=24h&table=trace` + ); + }); + }); + + describe('when the trace is younger than 30 days', () => { + beforeAll(() => { + jest.useFakeTimers().setSystemTime(new Date(2025, 0, 1)); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('should not render the warning', () => { + const organization = OrganizationFixture(); + const start = new Date('2025-01-01T00:00:00Z').getTime() / 1e3; + + const eapTrace = makeEAPTrace([ + makeEAPSpan({ + op: 'http.server', + start_timestamp: start, + end_timestamp: start + 2, + children: [makeEAPSpan({start_timestamp: start + 1, end_timestamp: start + 4})], + }), + ]); + + render( + , + {organization} + ); + + expect(screen.queryByText('Partial Trace Data:')).not.toBeInTheDocument(); + + expect( + screen.queryByText( + 'Trace may be missing spans since the age of the trace is older than 30 days' + ) + ).not.toBeInTheDocument(); + + expect( + screen.queryByRole('link', {name: 'Search similar traces in the past 24 hours'}) + ).not.toBeInTheDocument(); + }); + }); +}); diff --git a/static/app/views/performance/newTraceDetails/traceTypeWarnings/partialTraceDataWarning.tsx b/static/app/views/performance/newTraceDetails/traceTypeWarnings/partialTraceDataWarning.tsx new file mode 100644 index 00000000000000..72fdf03e1a86f3 --- /dev/null +++ b/static/app/views/performance/newTraceDetails/traceTypeWarnings/partialTraceDataWarning.tsx @@ -0,0 +1,103 @@ +import {useMemo} from 'react'; +import moment from 'moment-timezone'; + +import {Alert} from '@sentry/scraps/alert'; +import {Link} from '@sentry/scraps/link'; +import {Text} from '@sentry/scraps/text'; + +import {t, tct} from 'sentry/locale'; +import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import useOrganization from 'sentry/utils/useOrganization'; +import usePageFilters from 'sentry/utils/usePageFilters'; +import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode'; +import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types'; +import {getExploreUrl} from 'sentry/views/explore/utils'; +import {getRepresentativeTraceEvent} from 'sentry/views/performance/newTraceDetails/traceApi/utils'; +import type {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree'; + +interface PartialTraceDataWarningProps { + logs: OurLogsResponseItem[] | undefined; + timestamp: number | undefined; + tree: TraceTree; +} + +export function PartialTraceDataWarning({ + logs, + timestamp, + tree, +}: PartialTraceDataWarningProps) { + const organization = useOrganization(); + const {selection} = usePageFilters(); + + const rep = getRepresentativeTraceEvent(tree, logs); + + let op = ''; + if (rep?.event) { + op = + 'transaction.op' in rep.event + ? `${rep.event?.['transaction.op']}` + : 'op' in rep.event + ? `${rep.event.op}` + : ''; + } + + const queryString = useMemo(() => { + const search = new MutableSearch(''); + search.addFilterValue('is_transaction', 'true'); + + if (op) { + search.addFilterValue('span.op', op); + } + + return search.formatString(); + }, [op]); + + if (!timestamp) { + return null; + } + + const now = moment(); + const isTraceTooYoung = moment(timestamp * 1000).isAfter(now.subtract(30, 'days')); + + if (isTraceTooYoung) { + return null; + } + + const projects = + typeof rep.event?.project_id === 'number' ? [rep.event?.project_id] : []; + + const exploreUrl = getExploreUrl({ + organization, + mode: Mode.SAMPLES, + query: queryString, + table: 'trace', + selection: { + ...selection, + projects, + datetime: { + start: null, + end: null, + utc: null, + period: '24h', + }, + }, + }); + + return ( + {t('Search similar traces in the past 24 hours')} + } + > + + {tct( + '[dataCategory] Trace may be missing spans since the age of the trace is older than 30 days', + { + dataCategory: {t('Partial Trace Data:')}, + } + )} + + + ); +}