mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
982d860123
## What Migrates the `edge-functions-last-hour-stats` query (requests + server error counts shown on the Edge Functions list) from the BigQuery-style `logs.all` endpoint to the OTEL/ClickHouse `logs.all.otel` endpoint. <img width="2378" height="958" alt="CleanShot 2026-07-07 at 16 24 43@2x" src="https://github.com/user-attachments/assets/5bf3f04c-43e1-44a3-af28-d53feee27f68" /> ## How - Adds an OTEL SQL builder that reads from the single `logs` table (`source = 'function_edge_logs'`), using `log_attributes['function_id']` and `toInt32OrZero(log_attributes['response.status_code'])` instead of `cross join unnest(metadata)`. - Gated by the `otelLegacyLogs` flag, matching the rest of the logs code. The BigQuery path is preserved when the flag is off, and the two paths cache under separate query keys. ## Testing - Unit tests cover both endpoints and assert the generated SQL for each path. - Go to edge fns list - stats load correctly <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for using the OTEL logs backend for edge function last-hour stats when enabled. * Queries and caching now automatically distinguish between the standard and OTEL-backed data sources. * **Bug Fixes** * Ensured stats results are fetched from the correct endpoint based on the selected logging backend. * Added coverage to verify OTEL-specific SQL and response behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
141 lines
4.1 KiB
TypeScript
141 lines
4.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { getEdgeFunctionsLastHourStats } from './edge-functions-last-hour-stats-query'
|
|
import { post } from '@/data/fetchers'
|
|
|
|
vi.mock('@/data/fetchers', () => ({
|
|
post: vi.fn(),
|
|
handleError: vi.fn(),
|
|
}))
|
|
|
|
type PostResponse = Awaited<ReturnType<typeof post>>
|
|
|
|
describe('getEdgeFunctionsLastHourStats', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers()
|
|
vi.setSystemTime(new Date('2024-01-15T12:00:00.000Z'))
|
|
vi.mocked(post).mockReset()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('requests last-hour function stats from logs.all by default', async () => {
|
|
vi.mocked(post).mockResolvedValue({ data: { result: [] }, error: null } as PostResponse)
|
|
|
|
await getEdgeFunctionsLastHourStats({
|
|
projectRef: 'project-ref',
|
|
functionIds: ['fn_1', 'fn_2'],
|
|
})
|
|
|
|
expect(post).toHaveBeenCalledWith('/platform/projects/{ref}/analytics/endpoints/logs.all', {
|
|
params: {
|
|
path: { ref: 'project-ref' },
|
|
query: { key: 'last-hour-stats' },
|
|
},
|
|
body: expect.objectContaining({
|
|
sql: expect.stringContaining(`and function_id in ('fn_1', 'fn_2')`),
|
|
iso_timestamp_start: '2024-01-15T11:00:00.000Z',
|
|
iso_timestamp_end: '2024-01-15T12:00:00.000Z',
|
|
}),
|
|
signal: undefined,
|
|
})
|
|
|
|
const postCalls = vi.mocked(post).mock.calls as Array<
|
|
[string, { body?: { sql?: string } } | undefined]
|
|
>
|
|
|
|
expect(postCalls[0]?.[1]?.body?.sql).toContain('from\n function_edge_logs')
|
|
})
|
|
|
|
it('requests last-hour function stats from logs.all.otel when useOtel is set', async () => {
|
|
vi.mocked(post).mockResolvedValue({ data: { result: [] }, error: null } as PostResponse)
|
|
|
|
await getEdgeFunctionsLastHourStats({
|
|
projectRef: 'project-ref',
|
|
functionIds: ['fn_1', 'fn_2'],
|
|
useOtel: true,
|
|
})
|
|
|
|
expect(post).toHaveBeenCalledWith(
|
|
'/platform/projects/{ref}/analytics/endpoints/logs.all.otel',
|
|
{
|
|
params: {
|
|
path: { ref: 'project-ref' },
|
|
query: { key: 'last-hour-stats' },
|
|
},
|
|
body: expect.objectContaining({
|
|
sql: expect.stringContaining(`and log_attributes['function_id'] in ('fn_1', 'fn_2')`),
|
|
iso_timestamp_start: '2024-01-15T11:00:00.000Z',
|
|
iso_timestamp_end: '2024-01-15T12:00:00.000Z',
|
|
}),
|
|
signal: undefined,
|
|
}
|
|
)
|
|
|
|
const postCalls = vi.mocked(post).mock.calls as Array<
|
|
[string, { body?: { sql?: string } } | undefined]
|
|
>
|
|
const sql = postCalls[0]?.[1]?.body?.sql ?? ''
|
|
|
|
expect(sql).toContain('from logs')
|
|
expect(sql).toContain("source = 'function_edge_logs'")
|
|
expect(sql).toContain("case when toInt32OrZero(log_attributes['response.status_code']) >= 500")
|
|
expect(sql).not.toContain('cross join unnest')
|
|
})
|
|
|
|
it('coerces counts to numbers and computes error rates per function', async () => {
|
|
vi.mocked(post).mockResolvedValue({
|
|
data: {
|
|
result: [
|
|
{ function_id: 'fn_1', requests_count: '100', server_err_count: '5' },
|
|
{ function_id: 'fn_2', requests_count: 8, server_err_count: 0 },
|
|
],
|
|
},
|
|
error: null,
|
|
} as PostResponse)
|
|
|
|
const result = await getEdgeFunctionsLastHourStats({
|
|
projectRef: 'project-ref',
|
|
functionIds: ['fn_1', 'fn_2'],
|
|
})
|
|
|
|
expect(result).toEqual({
|
|
fn_1: {
|
|
functionId: 'fn_1',
|
|
requestsCount: 100,
|
|
serverErrorCount: 5,
|
|
errorRate: 5,
|
|
},
|
|
fn_2: {
|
|
functionId: 'fn_2',
|
|
requestsCount: 8,
|
|
serverErrorCount: 0,
|
|
errorRate: 0,
|
|
},
|
|
})
|
|
})
|
|
|
|
it('handles empty results', async () => {
|
|
vi.mocked(post).mockResolvedValue({ data: { result: [] }, error: null } as PostResponse)
|
|
|
|
const result = await getEdgeFunctionsLastHourStats({
|
|
projectRef: 'project-ref',
|
|
functionIds: ['fn_1'],
|
|
})
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
|
|
it('skips the logs query when there are no function ids', async () => {
|
|
const result = await getEdgeFunctionsLastHourStats({
|
|
projectRef: 'project-ref',
|
|
functionIds: [],
|
|
})
|
|
|
|
expect(result).toEqual({})
|
|
expect(post).not.toHaveBeenCalled()
|
|
})
|
|
})
|