diff --git a/supabase/functions/_shared/edgeFunctionWrapper.ts b/supabase/functions/_shared/edgeFunctionWrapper.ts index b4f2982a..94558add 100644 --- a/supabase/functions/_shared/edgeFunctionWrapper.ts +++ b/supabase/functions/_shared/edgeFunctionWrapper.ts @@ -15,8 +15,9 @@ import { endSpan, addSpanEvent, logSpan, + logSpanToDatabase, extractSpanContextFromHeaders, - type Span + type Span } from './logger.ts'; import { formatEdgeError, toError } from './errorFormatter.ts'; import { ValidationError, logValidationError } from './typeValidation.ts'; @@ -188,6 +189,7 @@ export function wrapEdgeFunction( endSpan(span, 'ok'); logSpan(span); + logSpanToDatabase(span, requestId); // Clone response to add tracking headers const responseBody = await response.text(); @@ -221,6 +223,7 @@ export function wrapEdgeFunction( logValidationError(error, requestId, name); endSpan(span, 'error', error); logSpan(span); + logSpanToDatabase(span, requestId); const duration = span.endTime ? span.duration : Date.now() - span.startTime; @@ -291,6 +294,7 @@ export function wrapEdgeFunction( endSpan(span, 'error', error); logSpan(span); + logSpanToDatabase(span, requestId); const duration = span.endTime ? span.duration : Date.now() - span.startTime; @@ -333,6 +337,7 @@ export function wrapEdgeFunction( endSpan(span, 'error', error); logSpan(span); + logSpanToDatabase(span, requestId); const duration = span.endTime ? span.duration : Date.now() - span.startTime; diff --git a/supabase/functions/_shared/logger.ts b/supabase/functions/_shared/logger.ts index 1a6219f5..d8fbe1dd 100644 --- a/supabase/functions/_shared/logger.ts +++ b/supabase/functions/_shared/logger.ts @@ -3,6 +3,7 @@ * Prevents sensitive data exposure and provides consistent log format */ +import { createClient } from 'jsr:@supabase/supabase-js@2'; import { formatEdgeError } from './errorFormatter.ts'; type LogLevel = 'info' | 'warn' | 'error' | 'debug'; @@ -178,7 +179,7 @@ export function injectSpanContextIntoHeaders(spanContext: SpanContext): Record { + edgeLogger.error('Failed to persist span to database', { + spanId: span.spanId, + traceId: span.traceId, + error: formatEdgeError(error), + }); + }); +} + +/** + * Internal function to persist span to database + */ +async function persistSpanToDatabase(span: Span, requestId?: string): Promise { + const supabaseUrl = Deno.env.get('SUPABASE_URL'); + const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'); + + if (!supabaseUrl || !supabaseServiceKey) { + edgeLogger.warn('Skipping span persistence - Supabase credentials not configured'); + return; + } + + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + // Insert span + const { error: spanError } = await supabase + .from('request_spans') + .insert({ + span_id: span.spanId, + trace_id: span.traceId, + parent_span_id: span.parentSpanId, + request_id: requestId, + name: span.name, + kind: span.kind, + start_time: new Date(span.startTime).toISOString(), + end_time: span.endTime ? new Date(span.endTime).toISOString() : null, + duration_ms: span.duration, + status: span.status, + error_type: span.error?.type, + error_message: span.error?.message, + error_stack: span.error?.stack, + }); + + if (spanError) { + throw new Error(`Failed to insert span: ${spanError.message}`); + } + + // Insert attributes + const attributeInserts = Object.entries(span.attributes).map(([key, value]) => { + let valueType: 'string' | 'number' | 'boolean' = 'string'; + let valueStr: string; + + if (typeof value === 'number') { + valueType = 'number'; + valueStr = String(value); + } else if (typeof value === 'boolean') { + valueType = 'boolean'; + valueStr = String(value); + } else { + valueStr = String(value); + } + + return { + span_id: span.spanId, + key, + value: valueStr, + value_type: valueType, + }; + }); + + if (attributeInserts.length > 0) { + const { error: attrError } = await supabase + .from('span_attributes') + .insert(attributeInserts); + + if (attrError) { + edgeLogger.warn('Failed to insert span attributes', { + spanId: span.spanId, + error: attrError.message, + }); + } + } + + // Insert events + for (let i = 0; i < span.events.length; i++) { + const event = span.events[i]; + + const { data: eventData, error: eventError } = await supabase + .from('span_events') + .insert({ + span_id: span.spanId, + timestamp: new Date(event.timestamp).toISOString(), + name: event.name, + sequence_order: i, + }) + .select('id') + .single(); + + if (eventError || !eventData) { + edgeLogger.warn('Failed to insert span event', { + spanId: span.spanId, + eventName: event.name, + error: eventError?.message, + }); + continue; + } + + // Insert event attributes + if (event.attributes) { + const eventAttrInserts = Object.entries(event.attributes).map(([key, value]) => { + let valueType: 'string' | 'number' | 'boolean' = 'string'; + let valueStr: string; + + if (typeof value === 'number') { + valueType = 'number'; + valueStr = String(value); + } else if (typeof value === 'boolean') { + valueType = 'boolean'; + valueStr = String(value); + } else { + valueStr = String(value); + } + + return { + span_event_id: eventData.id, + key, + value: valueStr, + value_type: valueType, + }; + }); + + if (eventAttrInserts.length > 0) { + const { error: eventAttrError } = await supabase + .from('span_event_attributes') + .insert(eventAttrInserts); + + if (eventAttrError) { + edgeLogger.warn('Failed to insert event attributes', { + spanId: span.spanId, + eventName: event.name, + error: eventAttrError.message, + }); + } + } + } + } +} + // Fields that should never be logged const SENSITIVE_FIELDS = [ 'password', diff --git a/supabase/migrations/20251111042659_2691087d-ffdb-4d70-a35b-17cfc8ecc279.sql b/supabase/migrations/20251111042659_2691087d-ffdb-4d70-a35b-17cfc8ecc279.sql new file mode 100644 index 00000000..53f6bc0d --- /dev/null +++ b/supabase/migrations/20251111042659_2691087d-ffdb-4d70-a35b-17cfc8ecc279.sql @@ -0,0 +1,80 @@ +-- Fix security warnings from span storage schema + +-- Drop and recreate views with SECURITY INVOKER +DROP VIEW IF EXISTS public.trace_summary; +DROP VIEW IF EXISTS public.span_hierarchy; + +-- Recreate trace_summary with SECURITY INVOKER +CREATE VIEW public.trace_summary +WITH (security_invoker = true) +AS +SELECT + trace_id, + COUNT(*) as span_count, + MIN(start_time) as trace_start, + MAX(end_time) as trace_end, + SUM(duration_ms) as total_duration_ms, + COUNT(*) FILTER (WHERE status = 'error') as error_count, + ARRAY_AGG(DISTINCT name) as span_names, + ARRAY_AGG(span_id ORDER BY start_time) as span_ids +FROM public.request_spans +GROUP BY trace_id; + +-- Recreate span_hierarchy with SECURITY INVOKER +CREATE VIEW public.span_hierarchy +WITH (security_invoker = true) +AS +WITH RECURSIVE span_tree AS ( + -- Root spans (no parent) + SELECT + span_id, + parent_span_id, + trace_id, + name, + kind, + start_time, + duration_ms, + status, + 1 as depth, + ARRAY[span_id] as path + FROM public.request_spans + WHERE parent_span_id IS NULL + + UNION ALL + + -- Child spans + SELECT + rs.span_id, + rs.parent_span_id, + rs.trace_id, + rs.name, + rs.kind, + rs.start_time, + rs.duration_ms, + rs.status, + st.depth + 1, + st.path || rs.span_id + FROM public.request_spans rs + INNER JOIN span_tree st ON rs.parent_span_id = st.span_id +) +SELECT * FROM span_tree; + +-- Recreate function with explicit search_path +CREATE OR REPLACE FUNCTION public.cleanup_old_spans() +RETURNS integer +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public +AS $$ +DECLARE + deleted_count integer; +BEGIN + -- Delete spans older than 30 days + DELETE FROM public.request_spans + WHERE created_at < now() - interval '30 days'; + + GET DIAGNOSTICS deleted_count = ROW_COUNT; + + RETURN deleted_count; +END; +$$; \ No newline at end of file