Skip to content

Commit 1b04f1a

Browse files
authored
add shared api metrics to auth and realtime reports (supabase#36960)
* add shared api report to auth and realtime * prop to hide report metrics on shared report * rm commented out code * remove manual mapping, add type cast for type * rm realtime filters
1 parent 5a72ebf commit 1b04f1a

File tree

5 files changed

+386
-39
lines changed

5 files changed

+386
-39
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import { get } from 'data/fetchers'
2+
import { generateRegexpWhere } from './Reports.constants'
3+
import { ReportFilterItem } from './Reports.types'
4+
import { useQueries } from '@tanstack/react-query'
5+
import * as Sentry from '@sentry/nextjs'
6+
7+
export const SHARED_API_REPORT_SQL = {
8+
totalRequests: {
9+
queryType: 'logs',
10+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
11+
--reports-api-total-requests
12+
select
13+
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
14+
count(t.id) as count
15+
FROM ${src} t
16+
cross join unnest(metadata) as m
17+
cross join unnest(m.response) as response
18+
cross join unnest(m.request) as request
19+
cross join unnest(request.headers) as headers
20+
${generateRegexpWhere(filters)}
21+
GROUP BY
22+
timestamp
23+
ORDER BY
24+
timestamp ASC`,
25+
},
26+
topRoutes: {
27+
queryType: 'logs',
28+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
29+
-- reports-api-top-routes
30+
select
31+
request.path as path,
32+
request.method as method,
33+
request.search as search,
34+
response.status_code as status_code,
35+
count(t.id) as count
36+
from ${src} t
37+
cross join unnest(metadata) as m
38+
cross join unnest(m.response) as response
39+
cross join unnest(m.request) as request
40+
cross join unnest(request.headers) as headers
41+
${generateRegexpWhere(filters)}
42+
group by
43+
request.path, request.method, request.search, response.status_code
44+
order by
45+
count desc
46+
limit 10
47+
`,
48+
},
49+
errorCounts: {
50+
queryType: 'logs',
51+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
52+
-- reports-api-error-counts
53+
select
54+
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
55+
count(t.id) as count
56+
FROM ${src} t
57+
cross join unnest(metadata) as m
58+
cross join unnest(m.response) as response
59+
cross join unnest(m.request) as request
60+
cross join unnest(request.headers) as headers
61+
WHERE
62+
response.status_code >= 400
63+
${generateRegexpWhere(filters, false)}
64+
GROUP BY
65+
timestamp
66+
ORDER BY
67+
timestamp ASC
68+
`,
69+
},
70+
topErrorRoutes: {
71+
queryType: 'logs',
72+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
73+
-- reports-api-top-error-routes
74+
select
75+
request.path as path,
76+
request.method as method,
77+
request.search as search,
78+
response.status_code as status_code,
79+
count(t.id) as count
80+
from ${src} t
81+
cross join unnest(metadata) as m
82+
cross join unnest(m.response) as response
83+
cross join unnest(m.request) as request
84+
cross join unnest(request.headers) as headers
85+
where
86+
response.status_code >= 400
87+
${generateRegexpWhere(filters, false)}
88+
group by
89+
request.path, request.method, request.search, response.status_code
90+
order by
91+
count desc
92+
limit 10
93+
`,
94+
},
95+
responseSpeed: {
96+
queryType: 'logs',
97+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
98+
-- reports-api-response-speed
99+
select
100+
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
101+
avg(response.origin_time) as avg
102+
FROM
103+
${src} t
104+
cross join unnest(metadata) as m
105+
cross join unnest(m.response) as response
106+
cross join unnest(m.request) as request
107+
cross join unnest(request.headers) as headers
108+
${generateRegexpWhere(filters)}
109+
GROUP BY
110+
timestamp
111+
ORDER BY
112+
timestamp ASC
113+
`,
114+
},
115+
topSlowRoutes: {
116+
queryType: 'logs',
117+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
118+
-- reports-api-top-slow-routes
119+
select
120+
request.path as path,
121+
request.method as method,
122+
request.search as search,
123+
response.status_code as status_code,
124+
count(t.id) as count,
125+
avg(response.origin_time) as avg
126+
from ${src} t
127+
cross join unnest(metadata) as m
128+
cross join unnest(m.response) as response
129+
cross join unnest(m.request) as request
130+
cross join unnest(request.headers) as headers
131+
${generateRegexpWhere(filters)}
132+
group by
133+
request.path, request.method, request.search, response.status_code
134+
order by
135+
avg desc
136+
limit 10
137+
`,
138+
},
139+
networkTraffic: {
140+
queryType: 'logs',
141+
sql: (filters: ReportFilterItem[], src = 'edge_logs') => `
142+
-- reports-api-network-traffic
143+
select
144+
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
145+
coalesce(
146+
safe_divide(
147+
sum(
148+
cast(coalesce(headers.content_length, "0") as int64)
149+
),
150+
1000000
151+
),
152+
0
153+
) as ingress_mb,
154+
coalesce(
155+
safe_divide(
156+
sum(
157+
cast(coalesce(resp_headers.content_length, "0") as int64)
158+
),
159+
1000000
160+
),
161+
0
162+
) as egress_mb,
163+
FROM
164+
${src} t
165+
cross join unnest(metadata) as m
166+
cross join unnest(m.response) as response
167+
cross join unnest(m.request) as request
168+
cross join unnest(request.headers) as headers
169+
cross join unnest(response.headers) as resp_headers
170+
${generateRegexpWhere(filters)}
171+
GROUP BY
172+
timestamp
173+
ORDER BY
174+
timestamp ASC
175+
`,
176+
},
177+
}
178+
179+
export type SharedAPIReportKey = keyof typeof SHARED_API_REPORT_SQL
180+
181+
const fetchLogs = async ({
182+
projectRef,
183+
sql,
184+
start,
185+
end,
186+
}: {
187+
projectRef: string
188+
sql: string
189+
start: string
190+
end: string
191+
}) => {
192+
const { data, error } = await get(`/platform/projects/{ref}/analytics/endpoints/logs.all`, {
193+
params: {
194+
path: { ref: projectRef },
195+
query: {
196+
sql,
197+
iso_timestamp_start: start,
198+
iso_timestamp_end: end,
199+
},
200+
},
201+
})
202+
203+
if (error || data?.error) {
204+
Sentry.captureException({
205+
message: 'Shared API Report Error',
206+
data: {
207+
error,
208+
data,
209+
},
210+
})
211+
throw error || data?.error
212+
}
213+
214+
return data
215+
}
216+
217+
type SharedAPIReportParams = {
218+
src: string
219+
filters: ReportFilterItem[]
220+
start: string
221+
end: string
222+
projectRef: string
223+
enabled?: boolean
224+
}
225+
export const useSharedAPIReport = ({
226+
src = 'edge_logs',
227+
filters,
228+
start,
229+
end,
230+
projectRef,
231+
enabled = true,
232+
}: SharedAPIReportParams) => {
233+
const queries = useQueries({
234+
queries: Object.entries(SHARED_API_REPORT_SQL).map(([key, value]) => ({
235+
queryKey: ['shared-api-report', key, src, filters, start, end, projectRef],
236+
enabled,
237+
queryFn: () =>
238+
fetchLogs({
239+
projectRef,
240+
sql: value.sql(filters, src),
241+
start,
242+
end,
243+
}),
244+
})),
245+
})
246+
247+
const keys = Object.keys(SHARED_API_REPORT_SQL) as Array<keyof typeof SHARED_API_REPORT_SQL>
248+
249+
const data = keys.reduce(
250+
(acc, key, i) => {
251+
acc[key] = queries[i].data?.result || []
252+
return acc
253+
},
254+
{} as { [K in keyof typeof SHARED_API_REPORT_SQL]: unknown[] }
255+
)
256+
257+
const error = keys.reduce(
258+
(acc, key, i) => {
259+
acc[key] = queries[i].error as string
260+
return acc
261+
},
262+
{} as { [K in keyof typeof SHARED_API_REPORT_SQL]: string }
263+
)
264+
265+
const isLoading = keys.reduce(
266+
(acc, key, i) => {
267+
acc[key] = queries[i].isLoading
268+
return acc
269+
},
270+
{} as { [K in keyof typeof SHARED_API_REPORT_SQL]: boolean }
271+
)
272+
273+
return {
274+
data,
275+
error,
276+
isLoading,
277+
}
278+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import ReportWidget from './ReportWidget'
2+
import {
3+
ErrorCountsChartRenderer,
4+
NetworkTrafficRenderer,
5+
ResponseSpeedChartRenderer,
6+
TopApiRoutesRenderer,
7+
TotalRequestsChartRenderer,
8+
} from './renderers/ApiRenderers'
9+
import { SharedAPIReportKey, useSharedAPIReport } from './SharedAPIReport.constants'
10+
import { useParams } from 'common'
11+
12+
export function SharedAPIReport({
13+
filterBy,
14+
start,
15+
end,
16+
hiddenReports = [],
17+
}: {
18+
filterBy: 'auth' | 'realtime' | 'storage' | 'graphql' | 'functions'
19+
start: string
20+
end: string
21+
hiddenReports?: SharedAPIReportKey[]
22+
}) {
23+
const { ref } = useParams() as { ref: string }
24+
25+
const { data, error, isLoading } = useSharedAPIReport({
26+
src: filterBy === 'functions' ? 'function_edge_logs' : 'edge_logs',
27+
filters: [
28+
{
29+
key: 'request.path',
30+
value: `/${filterBy}`,
31+
compare: 'matches',
32+
},
33+
],
34+
start,
35+
end,
36+
projectRef: ref,
37+
enabled: !!ref && !!filterBy,
38+
})
39+
40+
return (
41+
<div className="grid grid-cols-1 gap-4">
42+
{!hiddenReports.includes('totalRequests') && (
43+
<ReportWidget
44+
isLoading={isLoading.totalRequests}
45+
title="Total Requests"
46+
data={data.totalRequests || []}
47+
error={error.totalRequests}
48+
renderer={TotalRequestsChartRenderer}
49+
append={TopApiRoutesRenderer}
50+
appendProps={{ data: data.topRoutes }}
51+
/>
52+
)}
53+
{!hiddenReports.includes('errorCounts') && (
54+
<ReportWidget
55+
isLoading={isLoading.errorCounts}
56+
title="Response Errors"
57+
tooltip="Error responses with 4XX or 5XX status codes"
58+
data={data.errorCounts || []}
59+
error={error.errorCounts}
60+
renderer={ErrorCountsChartRenderer}
61+
appendProps={{
62+
data: data.topErrorRoutes || [],
63+
}}
64+
append={TopApiRoutesRenderer}
65+
/>
66+
)}
67+
{!hiddenReports.includes('responseSpeed') && (
68+
<ReportWidget
69+
isLoading={isLoading.responseSpeed}
70+
title="Response Speed"
71+
tooltip="Average response speed of a request (in ms)"
72+
data={data.responseSpeed || []}
73+
error={error.responseSpeed}
74+
renderer={ResponseSpeedChartRenderer}
75+
appendProps={{ data: data.topSlowRoutes || [] }}
76+
append={TopApiRoutesRenderer}
77+
/>
78+
)}
79+
{!hiddenReports.includes('networkTraffic') && (
80+
<ReportWidget
81+
isLoading={isLoading.networkTraffic}
82+
error={error.networkTraffic}
83+
title="Network Traffic"
84+
tooltip="Ingress and egress of requests and responses respectively"
85+
data={data.networkTraffic || []}
86+
renderer={NetworkTrafficRenderer}
87+
/>
88+
)}
89+
</div>
90+
)
91+
}

apps/studio/pages/project/[ref]/reports/auth.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { REPORT_DATERANGE_HELPER_LABELS } from 'components/interfaces/Reports/Re
1919
import UpgradePrompt from 'components/interfaces/Settings/Logs/UpgradePrompt'
2020
import { DatePickerValue } from 'components/interfaces/Settings/Logs/Logs.DatePickers'
2121
import type { NextPageWithLayout } from 'types'
22+
import { SharedAPIReport } from 'components/interfaces/Reports/SharedAPIReport'
2223

2324
const AuthReport: NextPageWithLayout = () => {
2425
return (
@@ -133,6 +134,13 @@ const AuthUsage = () => {
133134
orgPlanId={orgPlan?.id}
134135
/>
135136
))}
137+
<div className="relative pt-8 mt-8 border-t">
138+
<SharedAPIReport
139+
filterBy="auth"
140+
start={selectedDateRange?.period_start?.date}
141+
end={selectedDateRange?.period_end?.date}
142+
/>
143+
</div>
136144
</ReportStickyNav>
137145
</>
138146
)

0 commit comments

Comments
 (0)