Skip to content
Merged
3 changes: 3 additions & 0 deletions static/app/views/explore/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export function getExploreUrl({
sort,
field,
id,
table,
title,
referrer,
}: {
Expand All @@ -79,6 +80,7 @@ export function getExploreUrl({
referrer?: string;
selection?: PageFilters;
sort?: string;
table?: string;
title?: string;
visualize?: BaseVisualize[];
}) {
Expand All @@ -100,6 +102,7 @@ export function getExploreUrl({
field,
utc,
id,
table,
title,
referrer,
};
Expand Down
6 changes: 6 additions & 0 deletions static/app/views/performance/newTraceDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -165,6 +166,11 @@ function TraceViewImpl({traceSlug}: {traceSlug: string}) {
traceSlug={traceSlug}
organization={organization}
/>
<PartialTraceDataWarning
timestamp={queryParams.timestamp}
logs={logsData}
tree={tree}
/>
<TraceTabsAndVitals
tabsConfig={{
tabOptions,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import {OrganizationFixture} from 'sentry-fixture/organization';

import {render, screen} from 'sentry-test/reactTestingLibrary';

import {TraceTree} from 'sentry/views/performance/newTraceDetails/traceModels/traceTree';
import {
makeEAPSpan,
makeEAPTrace,
} from 'sentry/views/performance/newTraceDetails/traceModels/traceTreeTestUtils';

import {PartialTraceDataWarning} from './partialTraceDataWarning';

describe('PartialTraceDataWarning', () => {
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(
<PartialTraceDataWarning
logs={[]}
timestamp={start}
tree={TraceTree.FromTrace(eapTrace, {replay: null, meta: null, organization})}
/>,
{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(
<PartialTraceDataWarning
logs={[]}
timestamp={start}
tree={TraceTree.FromTrace(eapTrace, {replay: null, meta: null, organization})}
/>,
{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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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 (
<Alert
type="warning"
trailingItems={
<Link to={exploreUrl}>{t('Search similar traces in the past 24 hours')}</Link>
}
>
<Text as="p">
{tct(
'[dataCategory] Trace may be missing spans since the age of the trace is older than 30 days',
{
dataCategory: <Text bold>{t('Partial Trace Data:')}</Text>,
}
)}
</Text>
</Alert>
);
}
Loading