Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions static/app/utils/performance/quickTrace/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -143,3 +143,12 @@ export type TraceMeta = {
transaction_child_count_map: Record<string, number>;
transactions: number;
};

export type EAPTraceMeta = {
errors: number;
logs: number;
performance_issues: number;
span_count: number;
span_count_map: Record<string, number>;
transaction_child_count_map: Record<string, number>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import {OrganizationFixture} from 'sentry-fixture/organization';
import {makeTestQueryClient} from 'sentry-test/queryClient';
import {renderHook, waitFor} from 'sentry-test/reactTestingLibrary';

import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
import {OrganizationContext} from 'sentry/views/organizationContext';
import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';

import {useTraceMeta} from './useTraceMeta';

jest.mock('sentry/utils/useSyncedLocalStorageState', () => ({
useSyncedLocalStorageState: jest.fn(),
}));

const organization = OrganizationFixture();
const queryClient = makeTestQueryClient();

Expand All @@ -29,6 +34,7 @@ const mockedReplayTraces: ReplayTrace[] = [

describe('useTraceMeta', () => {
beforeEach(function () {
jest.mocked(useSyncedLocalStorageState).mockReturnValue(['non-eap', jest.fn()]);
queryClient.clear();
jest.clearAllMocks();
});
Expand Down Expand Up @@ -119,6 +125,95 @@ describe('useTraceMeta', () => {
});
});

it('EAP - Returns merged meta results', async () => {
const org = OrganizationFixture({
features: ['trace-spans-format'],
});

jest.mocked(useSyncedLocalStorageState).mockReturnValue(['eap', jest.fn()]);

MockApiClient.addMockResponse({
method: 'GET',
url: '/organizations/org-slug/trace-meta/slug1/',
body: {
errors: 1,
logs: 1,
performance_issues: 1,
span_count: 1,
span_count_map: {
op1: 1,
},
transaction_child_count_map: [{'transaction.id': '1', count: 1}],
},
});
MockApiClient.addMockResponse({
method: 'GET',
url: '/organizations/org-slug/trace-meta/slug2/',
body: {
errors: 1,
logs: 1,
performance_issues: 1,
span_count: 1,
span_count_map: {
op1: 1,
op2: 1,
},
transaction_child_count_map: [{'transaction.id': '2', count: 2}],
},
});
MockApiClient.addMockResponse({
method: 'GET',
url: '/organizations/org-slug/trace-meta/slug3/',
body: {
errors: 1,
logs: 1,
performance_issues: 1,
span_count: 1,
span_count_map: {
op3: 1,
},
transaction_child_count_map: [{'transaction.id': '3', count: 1}],
},
});

const wrapper = ({children}: {children: React.ReactNode}) => (
<QueryClientProvider client={queryClient}>
<OrganizationContext value={org}>{children}</OrganizationContext>
</QueryClientProvider>
);

const {result} = renderHook(() => useTraceMeta(mockedReplayTraces), {wrapper});

expect(result.current).toEqual({
data: undefined,
errors: [],
status: 'pending',
});

await waitFor(() => expect(result.current.status === 'success').toBe(true));

expect(result.current).toEqual({
data: {
errors: 3,
logs: 3,
performance_issues: 3,
span_count: 3,
span_count_map: {
op1: 2,
op2: 1,
op3: 1,
},
transaction_child_count_map: {
'1': 1,
'2': 2,
'3': 1,
},
},
errors: [],
status: 'success',
});
});

it('Collects errors from rejected api calls', async () => {
const mockRequest1 = MockApiClient.addMockResponse({
method: 'GET',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilte
import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
import type {PageFilters} from 'sentry/types/core';
import type {Organization} from 'sentry/types/organization';
import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
import type {EAPTraceMeta, TraceMeta} from 'sentry/utils/performance/quickTrace/types';
import type {QueryStatus} from 'sentry/utils/queryClient';
import {decodeScalar} from 'sentry/utils/queryString';
import useApi from 'sentry/utils/useApi';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import {useSyncedLocalStorageState} from 'sentry/utils/useSyncedLocalStorageState';
import {TRACE_FORMAT_PREFERENCE_KEY} from 'sentry/views/performance/newTraceDetails/traceHeader/styles';
import type {ReplayTrace} from 'sentry/views/replays/detail/trace/useReplayTraces';

type TraceMetaQueryParams =
Expand Down Expand Up @@ -42,64 +44,88 @@ function getMetaQueryParams(
}

async function fetchSingleTraceMetaNew(
type: 'non-eap' | 'eap',
api: Client,
organization: Organization,
replayTrace: ReplayTrace,
queryParams: any
) {
const data = await api.requestPromise(
`/organizations/${organization.slug}/events-trace-meta/${replayTrace.traceSlug}/`,
{
method: 'GET',
data: queryParams,
}
);
const url: string =
type === 'eap'
? `/organizations/${organization.slug}/trace-meta/${replayTrace.traceSlug}/`
: `/organizations/${organization.slug}/events-trace-meta/${replayTrace.traceSlug}/`;

const data = await api.requestPromise(url, {
method: 'GET',
data: queryParams,
});
return data;
}

async function fetchTraceMetaInBatches(
type: 'non-eap' | 'eap',
api: Client,
organization: Organization,
replayTraces: ReplayTrace[],
normalizedParams: any,
filters: Partial<PageFilters> = {}
) {
const clonedTraceIds = [...replayTraces];
const meta: TraceMeta = {
errors: 0,
performance_issues: 0,
projects: 0,
transactions: 0,
transaction_child_count_map: {},
span_count: 0,
span_count_map: {},
};
const meta: TraceMeta | EAPTraceMeta =
type === 'eap'
? {
errors: 0,
logs: 0,
performance_issues: 0,
span_count: 0,
span_count_map: {},
transaction_child_count_map: {},
}
: {
errors: 0,
performance_issues: 0,
projects: 0,
transactions: 0,
transaction_child_count_map: {},
span_count: 0,
span_count_map: {},
};

const apiErrors: Error[] = [];

while (clonedTraceIds.length > 0) {
const batch = clonedTraceIds.splice(0, 3);
const results = await Promise.allSettled(
const results = await Promise.allSettled<TraceMeta | EAPTraceMeta>(
batch.map(replayTrace => {
const queryParams = getMetaQueryParams(replayTrace, normalizedParams, filters);
return fetchSingleTraceMetaNew(api, organization, replayTrace, queryParams);
return fetchSingleTraceMetaNew(type, api, organization, replayTrace, queryParams);
})
);

results.reduce((acc, result) => {
if (result.status === 'fulfilled') {
acc.errors += result.value.errors;
acc.performance_issues += result.value.performance_issues;
acc.projects = Math.max(acc.projects, result.value.projects);
acc.transactions += result.value.transactions;

if ('projects' in acc && 'projects' in result.value) {
acc.projects = Math.max(acc.projects, result.value.projects);
}
if ('transactions' in acc && 'transactions' in result.value) {
acc.transactions += result.value.transactions;
}
if ('logs' in acc && 'logs' in result.value) {
acc.logs += result.value.logs;
}

// Turn the transaction_child_count_map array into a map of transaction id to child count
// for more efficient lookups.
result.value.transaction_child_count_map.forEach(
({'transaction.id': id, count}: any) => {
acc.transaction_child_count_map[id] = count;
}
);
if (Array.isArray(result.value.transaction_child_count_map)) {
result.value.transaction_child_count_map.forEach(
({'transaction.id': id, count}: any) => {
acc.transaction_child_count_map[id] = count;
}
);
}

acc.span_count += result.value.span_count;
Object.entries(result.value.span_count_map).forEach(([span_op, count]: any) => {
Expand All @@ -116,7 +142,7 @@ async function fetchTraceMetaInBatches(
}

export type TraceMetaQueryResults = {
data: TraceMeta | undefined;
data: TraceMeta | EAPTraceMeta | undefined;
errors: Error[];
status: QueryStatus;
};
Expand All @@ -125,6 +151,13 @@ export function useTraceMeta(replayTraces: ReplayTrace[]): TraceMetaQueryResults
const api = useApi();
const filters = usePageFilters();
const organization = useOrganization();
const [storedTraceFormat] = useSyncedLocalStorageState(
TRACE_FORMAT_PREFERENCE_KEY,
'non-eap'
);

const isEAP: boolean =
storedTraceFormat === 'eap' && organization.features.includes('trace-spans-format');

const normalizedParams = useMemo(() => {
const query = qs.parse(location.search);
Expand All @@ -140,14 +173,15 @@ export function useTraceMeta(replayTraces: ReplayTrace[]): TraceMetaQueryResults
const query = useQuery<
{
apiErrors: Error[];
meta: TraceMeta;
meta: TraceMeta | EAPTraceMeta;
},
Error
>({
// eslint-disable-next-line @tanstack/query/exhaustive-deps
queryKey: ['traceData', replayTraces.map(trace => trace.traceSlug)],
queryFn: () =>
fetchTraceMetaInBatches(
isEAP ? 'eap' : 'non-eap',
api,
organization,
replayTraces,
Expand Down Expand Up @@ -175,19 +209,28 @@ export function useTraceMeta(replayTraces: ReplayTrace[]): TraceMetaQueryResults
// The trace meta query has to reflect this by returning a single transaction and project.
const demoResults = useMemo(() => {
return {
data: {
errors: 0,
performance_issues: 0,
projects: 1,
transactions: 1,
transaction_child_count_map: {},
span_count: 0,
span_count_map: {},
},
data: isEAP
? {
errors: 0,
logs: 0,
performance_issues: 0,
span_count: 0,
span_count_map: {},
transaction_child_count_map: {},
}
: {
errors: 0,
performance_issues: 0,
projects: 1,
transactions: 1,
transaction_child_count_map: {},
span_count: 0,
span_count_map: {},
},
errors: [],
status: 'success' as QueryStatus,
};
}, []);
}, [isEAP]);

return mode === 'demo' ? demoResults : results;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Organization} from 'sentry/types/organization';
import getDuration from 'sentry/utils/duration/getDuration';
import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types';
import type {TraceMetaQueryResults} from 'sentry/views/performance/newTraceDetails/traceApi/useTraceMeta';
import type {RepresentativeTraceEvent} from 'sentry/views/performance/newTraceDetails/traceApi/utils';
import {TraceDrawerComponents} from 'sentry/views/performance/newTraceDetails/traceDrawer/details/styles';
import {
Expand Down Expand Up @@ -51,7 +51,7 @@ const SectionBody = styled('div')<{rightAlign?: boolean}>`

interface MetaProps {
logs: OurLogsResponseItem[] | undefined;
meta: TraceMeta | undefined;
meta: TraceMetaQueryResults['data'];
organization: Organization;
representativeEvent: RepresentativeTraceEvent;
tree: TraceTree;
Expand Down Expand Up @@ -166,7 +166,11 @@ export function Meta(props: MetaProps) {
<MetaSection
rightAlignBody
headingText={t('Logs')}
bodyText={props.logs?.length ?? 0}
bodyText={
props.meta && 'logs' in props.meta
? props.meta.logs
: (props.logs?.length ?? 0)
}
/>
) : null}
</MetaWrapper>
Expand Down
Loading