Skip to content

Commit 9713e5c

Browse files
authored
feat(profiling) add function metrics table (#76110)
First pass at adding function metrics table. This just renders a table with the function metrics and is missing time series and better linking to examples
1 parent 3aa9054 commit 9713e5c

File tree

5 files changed

+534
-4
lines changed

5 files changed

+534
-4
lines changed

Diff for: static/app/types/profiling.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,20 @@ declare namespace Profiling {
217217
scriptId?: number;
218218
};
219219

220+
type FunctionMetric = {
221+
avg: number;
222+
count: number;
223+
examples: ProfileReference[];
224+
fingerprint: number;
225+
in_app: boolean;
226+
name: string;
227+
p75: number;
228+
p95: number;
229+
p99: number;
230+
package: string;
231+
sum: number;
232+
};
233+
220234
type ProfileInput =
221235
| Profiling.Schema
222236
| JSSelfProfiling.Trace
@@ -286,5 +300,6 @@ declare namespace Profiling {
286300
profiles?: ReadonlyArray<ProfileReference>;
287301
};
288302
activeProfileIndex?: number;
303+
metrics?: FunctionMetric[];
289304
}
290305
}

Diff for: static/app/utils/profiling/hooks/useAggregateFlamegraphQuery.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ interface BaseAggregateFlamegraphQueryParameters {
1212
datetime?: PageFilters['datetime'];
1313
enabled?: boolean;
1414
environments?: PageFilters['environments'];
15+
metrics?: true;
1516
projects?: PageFilters['projects'];
1617
}
1718

@@ -30,6 +31,7 @@ interface TransactionsAggregateFlamegraphQueryParameters
3031

3132
interface ProfilesAggregateFlamegraphQueryParameters
3233
extends BaseAggregateFlamegraphQueryParameters {
34+
// query is not supported when querying from profiles
3335
dataSource: 'profiles';
3436
}
3537

@@ -46,7 +48,7 @@ export type UseAggregateFlamegraphQueryResult = UseApiQueryResult<
4648
export function useAggregateFlamegraphQuery(
4749
props: AggregateFlamegraphQueryParameters
4850
): UseAggregateFlamegraphQueryResult {
49-
const {dataSource, datetime, enabled, environments, projects} = props;
51+
const {dataSource, metrics, datetime, enabled, environments, projects} = props;
5052

5153
let fingerprint: string | undefined = undefined;
5254
let query: string | undefined = undefined;
@@ -62,7 +64,9 @@ export function useAggregateFlamegraphQuery(
6264
const {selection} = usePageFilters();
6365

6466
const endpointOptions = useMemo(() => {
65-
const params = {
67+
const params: {
68+
query: Record<string, any>;
69+
} = {
6670
query: {
6771
project: projects ?? selection.projects,
6872
environment: environments ?? selection.environments,
@@ -73,8 +77,21 @@ export function useAggregateFlamegraphQuery(
7377
},
7478
};
7579

80+
if (metrics) {
81+
params.query.expand = 'metrics';
82+
}
83+
7684
return params;
77-
}, [dataSource, datetime, environments, projects, fingerprint, query, selection]);
85+
}, [
86+
dataSource,
87+
datetime,
88+
environments,
89+
projects,
90+
fingerprint,
91+
query,
92+
metrics,
93+
selection,
94+
]);
7895

7996
return useApiQuery<Profiling.Schema>(
8097
[`/organizations/${organization.slug}/profiling/flamegraph/`, endpointOptions],

Diff for: static/app/views/profiling/content.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {LandingWidgetSelector} from './landing/landingWidgetSelector';
4646
import {ProfilesChart} from './landing/profileCharts';
4747
import {ProfilesChartWidget} from './landing/profilesChartWidget';
4848
import {ProfilingSlowestTransactionsPanel} from './landing/profilingSlowestTransactionsPanel';
49+
import {SlowestFunctionsTable} from './landing/slowestFunctionsTable';
4950
import {ProfilingOnboardingPanel} from './profilingOnboardingPanel';
5051

5152
const LEFT_WIDGET_CURSOR = 'leftCursor';
@@ -478,7 +479,18 @@ function ProfilingTransactionsContent(props: ProfilingTabContentProps) {
478479
<ProfilingOnboardingCTA />
479480
) : (
480481
<Fragment>
481-
{organization.features.includes('profiling-global-suspect-functions') ? (
482+
{organization.features.includes('continuous-profiling-compat') ? (
483+
<Fragment>
484+
<ProfilesChartWidget
485+
chartHeight={150}
486+
referrer="api.profiling.landing-chart"
487+
userQuery={query}
488+
selection={selection}
489+
continuousProfilingCompat={continuousProfilingCompat}
490+
/>
491+
<SlowestFunctionsTable />
492+
</Fragment>
493+
) : organization.features.includes('profiling-global-suspect-functions') ? (
482494
<Fragment>
483495
<ProfilesChartWidget
484496
chartHeight={150}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
2+
3+
import {SlowestFunctionsTable} from 'sentry/views/profiling/landing/slowestFunctionsTable';
4+
5+
describe('SlowestFunctionsTable', () => {
6+
it('shows loading state', () => {
7+
MockApiClient.addMockResponse({
8+
url: '/organizations/org-slug/profiling/flamegraph/',
9+
body: [],
10+
});
11+
12+
render(<SlowestFunctionsTable />);
13+
expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
14+
});
15+
16+
it('shows error state', async () => {
17+
MockApiClient.addMockResponse({
18+
url: '/organizations/org-slug/profiling/flamegraph/',
19+
body: [],
20+
statusCode: 500,
21+
});
22+
23+
render(<SlowestFunctionsTable />);
24+
expect(await screen.findByTestId('error-indicator')).toBeInTheDocument();
25+
});
26+
27+
it('shows no functions state', async () => {
28+
// @ts-expect-error partial schema mock
29+
const schema: Profiling.Schema = {
30+
metrics: [],
31+
};
32+
33+
MockApiClient.addMockResponse({
34+
url: '/organizations/org-slug/profiling/flamegraph/',
35+
match: [
36+
MockApiClient.matchQuery({
37+
expand: 'metrics',
38+
}),
39+
],
40+
body: schema,
41+
});
42+
43+
render(<SlowestFunctionsTable />);
44+
expect(await screen.findByText('No functions found')).toBeInTheDocument();
45+
});
46+
it('renders function fields', async () => {
47+
// @ts-expect-error partial schema mock
48+
const schema: Profiling.Schema = {
49+
metrics: [
50+
{
51+
name: 'slow-function',
52+
package: 'slow-package',
53+
p75: 1500 * 1e6,
54+
p95: 2000 * 1e6,
55+
p99: 3000 * 1e6,
56+
sum: 60_000 * 1e6,
57+
count: 5000,
58+
avg: 0.5 * 1e6,
59+
in_app: true,
60+
fingerprint: 12345,
61+
examples: [
62+
{
63+
project_id: 1,
64+
profile_id: 'profile-id',
65+
},
66+
],
67+
},
68+
],
69+
};
70+
71+
MockApiClient.addMockResponse({
72+
url: '/organizations/org-slug/profiling/flamegraph/',
73+
match: [
74+
MockApiClient.matchQuery({
75+
expand: 'metrics',
76+
}),
77+
],
78+
body: schema,
79+
});
80+
81+
render(<SlowestFunctionsTable />);
82+
for (const value of [
83+
'slow-function',
84+
'slow-package',
85+
'5k',
86+
'1.50s',
87+
'2.00s',
88+
'3.00s',
89+
'1.00min',
90+
]) {
91+
expect(await screen.findByText(value)).toBeInTheDocument();
92+
}
93+
});
94+
it('paginates response', async () => {
95+
// @ts-expect-error partial schema mock
96+
const schema: Profiling.Schema = {
97+
metrics: [],
98+
};
99+
100+
for (let i = 0; i < 10; i++) {
101+
schema.metrics?.push({
102+
name: 'slow-function',
103+
package: 'slow-package',
104+
p75: 1500 * 1e6,
105+
p95: 2000 * 1e6,
106+
p99: 3000 * 1e6,
107+
sum: 60_000 * 1e6,
108+
count: 5000,
109+
avg: 0.5 * 1e6,
110+
in_app: true,
111+
fingerprint: 12345,
112+
examples: [
113+
{
114+
project_id: 1,
115+
profile_id: 'profile-id',
116+
},
117+
],
118+
});
119+
}
120+
121+
MockApiClient.addMockResponse({
122+
url: '/organizations/org-slug/profiling/flamegraph/',
123+
match: [
124+
MockApiClient.matchQuery({
125+
expand: 'metrics',
126+
}),
127+
],
128+
body: schema,
129+
});
130+
131+
render(<SlowestFunctionsTable />);
132+
expect(await screen.findAllByText('slow-function')).toHaveLength(5);
133+
});
134+
135+
it('paginates results', async () => {
136+
// @ts-expect-error partial schema mock
137+
const schema: Profiling.Schema = {
138+
metrics: [],
139+
};
140+
141+
for (let i = 0; i < 10; i++) {
142+
schema.metrics?.push({
143+
name: 'slow-function-' + i,
144+
package: 'slow-package',
145+
p75: 1500 * 1e6,
146+
p95: 2000 * 1e6,
147+
p99: 3000 * 1e6,
148+
sum: 60_000 * 1e6,
149+
count: 5000,
150+
avg: 0.5 * 1e6,
151+
in_app: true,
152+
fingerprint: 12345,
153+
examples: [
154+
{
155+
project_id: 1,
156+
profile_id: 'profile-id',
157+
},
158+
],
159+
});
160+
}
161+
162+
MockApiClient.addMockResponse({
163+
url: '/organizations/org-slug/profiling/flamegraph/',
164+
match: [
165+
MockApiClient.matchQuery({
166+
expand: 'metrics',
167+
}),
168+
],
169+
body: schema,
170+
});
171+
172+
render(<SlowestFunctionsTable />);
173+
expect(await screen.findAllByText('slow-package')).toHaveLength(5);
174+
175+
userEvent.click(screen.getByLabelText('Next'));
176+
for (let i = 6; i < 10; i++) {
177+
expect(await screen.findByText('slow-function-' + i)).toBeInTheDocument();
178+
}
179+
expect(screen.getByLabelText('Next')).toBeDisabled();
180+
181+
userEvent.click(screen.getByLabelText('Previous'));
182+
for (let i = 0; i < 5; i++) {
183+
expect(await screen.findByText('slow-function-' + i)).toBeInTheDocument();
184+
}
185+
expect(screen.getByLabelText('Previous')).toBeDisabled();
186+
});
187+
});

0 commit comments

Comments
 (0)