Files
supabase/apps/studio/data/logs/unified-logs.utils.test.ts
Saxon FletcherandClaude Opus 5.5 09c3167551 feat(studio): scope unified logs queries to one edge function
Adds an optional `scope` to the unified logs search that narrows every
query (rows, counts, chart and facets, on both the OTEL and BigQuery
builders) to one edge function's invocations and runtime output.

- New `edge function runtime` log type backed by `function_logs`, with
  its level read from the console level and details kept in metadata
- `getLogTypeVisibility` keeps that type out of the project-wide Logs
  page, which already shows runtime logs inside each invocation
- `toQuerySearchParams` attaches the scope

Nothing sets a scope yet, so the Logs page is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 15:59:51 +10:00

337 lines
9.4 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
extractLogMetadata,
mapUnifiedLogRow,
parseUnifiedLogsQueryRows,
} from './unified-logs.utils'
describe('extractLogMetadata', () => {
describe('non-auth logs', () => {
it('returns projected status, method, and pathname', () => {
const row = {
log_type: 'api',
status: 404,
method: 'GET',
pathname: '/rest/v1/users?select=id',
event_message: 'irrelevant',
}
expect(extractLogMetadata(row)).toEqual({
status: 404,
method: 'GET',
pathname: '/rest/v1/users?select=id',
})
})
it('returns the projected pathname', () => {
const row = {
log_type: 'api',
status: 500,
method: 'POST',
pathname: '/fallback',
event_message: '',
}
expect(extractLogMetadata(row).pathname).toBe('/fallback')
})
it('returns an empty string when pathname is missing', () => {
const row = {
log_type: 'api',
status: 200,
method: 'GET',
event_message: '',
}
expect(extractLogMetadata(row).pathname).toBe('')
})
it('defaults status to 200 when missing', () => {
const row = {
log_type: 'api',
method: 'GET',
event_message: '',
}
expect(extractLogMetadata(row).status).toBe(200)
})
})
describe('auth logs', () => {
it('extracts status, method, and pathname from event_message JSON', () => {
const row = {
log_type: 'auth',
status: 999,
method: 'IGNORED',
event_message: JSON.stringify({
status: 400,
method: 'POST',
path: '/token',
msg: 'request completed',
}),
}
expect(extractLogMetadata(row)).toEqual({
status: 400,
method: 'POST',
pathname: '/token',
})
})
it('falls back to leading 3-digit status in msg when event_message.status is missing', () => {
const row = {
log_type: 'auth',
event_message: JSON.stringify({
method: 'POST',
path: '/token',
msg: '400: Invalid login credentials',
}),
}
expect(extractLogMetadata(row).status).toBe(400)
})
it('falls back to leading 3-digit status in error when neither status nor msg has one', () => {
const row = {
log_type: 'auth',
event_message: JSON.stringify({
method: 'POST',
path: '/token',
msg: 'request completed',
error: '403: forbidden',
}),
}
expect(extractLogMetadata(row).status).toBe(403)
})
it('returns undefined status when no status can be found anywhere', () => {
const row = {
log_type: 'auth',
event_message: JSON.stringify({
method: 'POST',
path: '/token',
msg: 'request completed',
}),
}
expect(extractLogMetadata(row).status).toBeUndefined()
})
it('does not extract status from a non-leading 3-digit number in msg', () => {
const row = {
log_type: 'auth',
event_message: JSON.stringify({
msg: 'attempt 500 failed',
}),
}
expect(extractLogMetadata(row).status).toBeUndefined()
})
it('does not match 4+ digit numbers at the start of msg', () => {
const row = {
log_type: 'auth',
event_message: JSON.stringify({
msg: '4000 something',
}),
}
expect(extractLogMetadata(row).status).toBeUndefined()
})
it('returns undefined metadata when event_message is not valid JSON', () => {
const row = {
log_type: 'auth',
status: 200,
method: 'GET',
event_message: 'not json',
}
expect(extractLogMetadata(row)).toEqual({
status: undefined,
method: undefined,
pathname: undefined,
})
})
it('prefers explicit event_message.status over msg/error extraction', () => {
const row = {
log_type: 'auth',
event_message: JSON.stringify({
status: 200,
msg: '500: something went wrong',
error: '503: service unavailable',
}),
}
expect(extractLogMetadata(row).status).toBe(200)
})
})
describe('compute logs', () => {
it('does not synthesize unsupported request metadata', () => {
const row = {
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
log_type: 'compute',
metadata: {
cw_event_id: '39883203917805946105278943454814281535421893832620638214',
launch_id: '1788424715503435269',
log_group: '/aws/lambda-microvms/workers/cxkpapyhaaywrtudnqpl/api',
log_stream: 'launch-1788424715503435269',
source: 'worker_guest_logs',
worker: 'api',
},
project: 'cxkpapyhaaywrtudnqpl',
timestamp: 1788424716876000,
}
expect(extractLogMetadata(row)).toEqual({ status: null, method: null, pathname: null })
})
it('preserves only Compute metadata while keeping unsupported fields null', () => {
const metadata = {
cw_event_id: '39883203917805946105278943454814281535421893832620638214',
launch_id: '1788424715503435269',
log_group: '/aws/lambda-microvms/workers/cxkpapyhaaywrtudnqpl/api',
log_stream: 'launch-1788424715503435269',
source: 'worker_guest_logs',
worker: 'api',
}
const mapped = mapUnifiedLogRow({
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
log_type: 'compute',
metadata,
timestamp: 1788424716876000,
status: 200,
level: 'success',
method: 'GET',
pathname: '/invented',
auth_user: 'invented-user',
log_count: null,
logs: null,
})
expect(mapped).toMatchObject({
id: '51a29911-9293-4616-8984-743cc548b629',
timestamp: 1788424716876000,
event_message: 'Error: Dynamic require of "path" is not supported',
metadata,
status: null,
level: null,
method: null,
pathname: null,
auth_user: null,
})
expect(mapped).not.toHaveProperty('project')
})
it('maps edge function runtime rows without request fields but with their metadata and level', () => {
const metadata = { event_type: 'Log', level: 'error', execution_id: 'exec-1' }
const mapped = mapUnifiedLogRow({
id: 'runtime-log',
timestamp: 1788424716876000,
log_type: 'edge function runtime',
metadata,
status: '',
level: 'error',
method: '',
pathname: '',
event_message: 'TypeError: failed to fetch',
log_count: null,
logs: null,
})
expect(mapped).toMatchObject({
log_type: 'edge function runtime',
event_message: 'TypeError: failed to fetch',
metadata,
level: 'error',
status: null,
method: null,
pathname: null,
})
})
it('does not add metadata to non-Compute rows', () => {
const mapped = mapUnifiedLogRow({
id: 'edge-log',
timestamp: 1788424716876000,
log_type: 'edge',
metadata: { request: 'existing metadata' },
status: 200,
level: 'success',
method: 'GET',
pathname: '/rest/v1',
event_message: null,
log_count: 0,
logs: null,
})
expect(mapped).not.toHaveProperty('metadata')
expect(mapped.log_count).toBe(0)
})
})
})
describe('parseUnifiedLogsQueryRows', () => {
const computeRow = {
event_message: 'Error: Dynamic require of "path" is not supported',
id: '51a29911-9293-4616-8984-743cc548b629',
metadata: {
cw_event_id: '39883203917805946105278943454814281535421893832620638214',
launch_id: '1788424715503435269',
log_group: '/aws/lambda-microvms/workers/cxkpapyhaaywrtudnqpl/api',
log_stream: 'launch-1788424715503435269',
source: 'worker_guest_logs',
worker: 'api',
},
project: 'cxkpapyhaaywrtudnqpl',
timestamp: 1788424716876000,
log_type: 'compute',
status: null,
level: null,
pathname: null,
method: null,
log_count: null,
logs: null,
auth_user: null,
}
it('parses the Compute projection and strips project', () => {
const [parsed] = parseUnifiedLogsQueryRows([computeRow])
expect(parsed).toMatchObject({
id: computeRow.id,
timestamp: computeRow.timestamp,
event_message: computeRow.event_message,
metadata: computeRow.metadata,
log_type: 'compute',
status: null,
level: null,
pathname: null,
method: null,
})
expect(parsed).not.toHaveProperty('project')
})
it('returns an empty array for undefined results', () => {
expect(parseUnifiedLogsQueryRows(undefined)).toEqual([])
})
it('rejects invalid metadata', () => {
expect(() => parseUnifiedLogsQueryRows([{ ...computeRow, metadata: 'invalid' }])).toThrow()
})
it.each([{ id: 42 }, { timestamp: true }])('rejects invalid identity fields', (invalidFields) => {
expect(() => parseUnifiedLogsQueryRows([{ ...computeRow, ...invalidFields }])).toThrow()
})
it('rejects invalid projected field types', () => {
expect(() => parseUnifiedLogsQueryRows([{ ...computeRow, level: 'info' }])).toThrow()
})
})