Files
thrilltrack-explorer/supabase/functions/_shared/edgeFunctionWrapper.ts
gpt-engineer-app[bot] 0dfc5ff724 Persist spans to DB via logger
Implement fire-and-forget span persistence:
- Add logSpanToDatabase and persistSpanToDatabase to logger
- Persist spans, attributes, events, and event attributes to new tables
- Wire edgeFunctionWrapper to call DB persistence after each span
- Create required tables, views, and security policies
- Ensure non-blocking and guard for missing Supabase creds
2025-11-11 04:28:17 +00:00

390 lines
11 KiB
TypeScript

/**
* Edge Function Wrapper with Comprehensive Error Handling
*
* Provides standardized:
* - Request/response logging
* - Error handling and formatting
* - Distributed tracing
* - Type validation
* - Performance monitoring
*/
import {
edgeLogger,
startSpan,
endSpan,
addSpanEvent,
logSpan,
logSpanToDatabase,
extractSpanContextFromHeaders,
type Span
} from './logger.ts';
import { formatEdgeError, toError } from './errorFormatter.ts';
import { ValidationError, logValidationError } from './typeValidation.ts';
export interface EdgeFunctionConfig {
name: string;
requireAuth?: boolean;
corsHeaders?: HeadersInit;
logRequests?: boolean;
logResponses?: boolean;
}
export interface EdgeFunctionContext {
requestId: string;
span: Span;
userId?: string;
}
export type EdgeFunctionHandler = (
req: Request,
context: EdgeFunctionContext
) => Promise<Response>;
/**
* Wrap an edge function with comprehensive error handling
*/
export function wrapEdgeFunction(
config: EdgeFunctionConfig,
handler: EdgeFunctionHandler
): (req: Request) => Promise<Response> {
const {
name,
requireAuth = true,
corsHeaders = {},
logRequests = true,
logResponses = true,
} = config;
return async (req: Request): Promise<Response> => {
// ========================================================================
// STEP 1: Handle CORS preflight
// ========================================================================
if (req.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: corsHeaders
});
}
// ========================================================================
// STEP 2: Initialize tracing
// ========================================================================
const parentSpanContext = extractSpanContextFromHeaders(req.headers);
const span = startSpan(
name,
'SERVER',
parentSpanContext,
{
'http.method': req.method,
'http.url': req.url,
'function.name': name,
}
);
const requestId = span.spanId;
// ========================================================================
// STEP 3: Log incoming request
// ========================================================================
if (logRequests) {
edgeLogger.info('Request received', {
requestId,
action: name,
method: req.method,
url: req.url,
hasAuth: req.headers.has('Authorization'),
contentType: req.headers.get('Content-Type'),
userAgent: req.headers.get('User-Agent'),
});
}
try {
// ====================================================================
// STEP 4: Authentication (if required)
// ====================================================================
let userId: string | undefined;
if (requireAuth) {
addSpanEvent(span, 'authentication_start');
const authHeader = req.headers.get('Authorization');
if (!authHeader) {
addSpanEvent(span, 'authentication_failed', { reason: 'missing_header' });
endSpan(span, 'error');
logSpan(span);
return new Response(
JSON.stringify({
error: 'Missing Authorization header',
requestId
}),
{
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
// Extract user ID from JWT (simplified - extend as needed)
try {
// Note: In production, validate the JWT properly
const token = authHeader.replace('Bearer ', '');
const payload = JSON.parse(atob(token.split('.')[1]));
userId = payload.sub;
addSpanEvent(span, 'authentication_success', { userId });
span.attributes['user.id'] = userId;
} catch (error) {
addSpanEvent(span, 'authentication_failed', {
reason: 'invalid_token',
error: formatEdgeError(error)
});
endSpan(span, 'error', error);
logSpan(span);
return new Response(
JSON.stringify({
error: 'Invalid authentication token',
requestId
}),
{
status: 401,
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
}
);
}
}
// ====================================================================
// STEP 5: Execute handler
// ====================================================================
addSpanEvent(span, 'handler_start');
const context: EdgeFunctionContext = {
requestId,
span,
userId,
};
const response = await handler(req, context);
// ====================================================================
// STEP 6: Log success response
// ====================================================================
addSpanEvent(span, 'handler_complete', {
status: response.status,
statusText: response.statusText
});
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
if (logResponses) {
edgeLogger.info('Request completed', {
requestId,
action: name,
status: response.status,
duration,
});
}
endSpan(span, 'ok');
logSpan(span);
logSpanToDatabase(span, requestId);
// Clone response to add tracking headers
const responseBody = await response.text();
const enhancedResponse = new Response(responseBody, {
status: response.status,
statusText: response.statusText,
headers: {
...Object.fromEntries(response.headers.entries()),
'X-Request-Id': requestId,
'X-Span-Id': span.spanId,
'X-Trace-Id': span.traceId,
'X-Duration-Ms': duration.toString(),
},
});
return enhancedResponse;
} catch (error) {
// ====================================================================
// STEP 7: Handle errors
// ====================================================================
// Validation errors (client error)
if (error instanceof ValidationError) {
addSpanEvent(span, 'validation_error', {
field: error.field,
expected: error.expected,
received: error.received,
});
logValidationError(error, requestId, name);
endSpan(span, 'error', error);
logSpan(span);
logSpanToDatabase(span, requestId);
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
return new Response(
JSON.stringify({
error: error.message,
field: error.field,
expected: error.expected,
received: error.received,
requestId,
}),
{
status: 400,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-Id': requestId,
'X-Span-Id': span.spanId,
'X-Trace-Id': span.traceId,
'X-Duration-Ms': duration.toString(),
},
}
);
}
// Database errors (check for specific codes)
const errorObj = error as any;
if (errorObj?.code) {
addSpanEvent(span, 'database_error', {
code: errorObj.code,
message: errorObj.message,
});
// Handle specific error codes
let status = 500;
let message = formatEdgeError(error);
if (errorObj.code === '23505') {
// Unique constraint violation
status = 409;
message = 'A record with this information already exists';
} else if (errorObj.code === '23503') {
// Foreign key violation
status = 400;
message = 'Referenced record does not exist';
} else if (errorObj.code === '23514') {
// Check constraint violation
status = 400;
message = 'Data violates database constraints';
} else if (errorObj.code === 'P0001') {
// Raised exception
status = 400;
message = errorObj.message || 'Database validation failed';
} else if (errorObj.code === '42501') {
// Insufficient privilege
status = 403;
message = 'Permission denied';
}
edgeLogger.error('Database error', {
requestId,
action: name,
errorCode: errorObj.code,
errorMessage: errorObj.message,
errorDetails: errorObj.details,
errorHint: errorObj.hint,
});
endSpan(span, 'error', error);
logSpan(span);
logSpanToDatabase(span, requestId);
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
return new Response(
JSON.stringify({
error: message,
code: errorObj.code,
details: errorObj.details,
requestId,
}),
{
status,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-Id': requestId,
'X-Span-Id': span.spanId,
'X-Trace-Id': span.traceId,
'X-Duration-Ms': duration.toString(),
},
}
);
}
// Generic errors
const errorMessage = formatEdgeError(error);
addSpanEvent(span, 'unhandled_error', {
error: errorMessage,
errorType: error instanceof Error ? error.name : typeof error,
});
edgeLogger.error('Unhandled error', {
requestId,
action: name,
error: errorMessage,
errorType: error instanceof Error ? error.name : typeof error,
stack: error instanceof Error ? error.stack : undefined,
});
endSpan(span, 'error', error);
logSpan(span);
logSpanToDatabase(span, requestId);
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
return new Response(
JSON.stringify({
error: 'Internal server error',
message: errorMessage,
requestId,
}),
{
status: 500,
headers: {
...corsHeaders,
'Content-Type': 'application/json',
'X-Request-Id': requestId,
'X-Span-Id': span.spanId,
'X-Trace-Id': span.traceId,
'X-Duration-Ms': duration.toString(),
},
}
);
}
};
}
/**
* Create a simple edge function with standard error handling
*
* Example usage:
* ```ts
* serve(createEdgeFunction({
* name: 'my-function',
* requireAuth: true,
* corsHeaders: myCorsHeaders,
* }, async (req, { requestId, span, userId }) => {
* // Your handler logic here
* return new Response(JSON.stringify({ success: true }), {
* status: 200,
* headers: { 'Content-Type': 'application/json' }
* });
* }));
* ```
*/
export function createEdgeFunction(
config: EdgeFunctionConfig,
handler: EdgeFunctionHandler
): (req: Request) => Promise<Response> {
return wrapEdgeFunction(config, handler);
}