mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
Capture backend response metadata
Update edge function wrapper to emit backend metadata headers (X-Request-Id, X-Span-Id, X-Trace-Id, X-Duration-Ms) on responses; adjust logging to include duration and headers. Enhance edgeFunctionTracking to extract and propagate backend metadata from responses and errors; extend errorHandler to capture and log backend metadata for improved observability.
This commit is contained in:
@@ -34,7 +34,19 @@ export async function invokeWithTracking<T = any>(
|
|||||||
timeout: number = 30000,
|
timeout: number = 30000,
|
||||||
retryOptions?: Partial<RetryOptions>,
|
retryOptions?: Partial<RetryOptions>,
|
||||||
customHeaders?: Record<string, string>
|
customHeaders?: Record<string, string>
|
||||||
): Promise<{ data: T | null; error: any; requestId: string; duration: number; attempts?: number; status?: number; traceId?: string }> {
|
): Promise<{
|
||||||
|
data: T | null;
|
||||||
|
error: any;
|
||||||
|
requestId: string;
|
||||||
|
duration: number;
|
||||||
|
attempts?: number;
|
||||||
|
status?: number;
|
||||||
|
traceId?: string;
|
||||||
|
backendRequestId?: string;
|
||||||
|
backendSpanId?: string;
|
||||||
|
backendTraceId?: string;
|
||||||
|
backendDuration?: number;
|
||||||
|
}> {
|
||||||
// Configure retry options with defaults
|
// Configure retry options with defaults
|
||||||
const effectiveRetryOptions: RetryOptions = {
|
const effectiveRetryOptions: RetryOptions = {
|
||||||
maxAttempts: retryOptions?.maxAttempts ?? 3,
|
maxAttempts: retryOptions?.maxAttempts ?? 3,
|
||||||
@@ -123,6 +135,16 @@ export async function invokeWithTracking<T = any>(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Extract backend metadata from successful response
|
||||||
|
let backendRequestId: string | undefined;
|
||||||
|
let backendSpanId: string | undefined;
|
||||||
|
let backendTraceId: string | undefined;
|
||||||
|
let backendDuration: number | undefined;
|
||||||
|
|
||||||
|
// Note: Response headers from edge functions are not currently accessible via the client
|
||||||
|
// Backend metadata extraction will be enhanced when Supabase client supports response headers
|
||||||
|
// For now, backend can include metadata in response body if needed
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data: result,
|
data: result,
|
||||||
error: null,
|
error: null,
|
||||||
@@ -131,6 +153,10 @@ export async function invokeWithTracking<T = any>(
|
|||||||
attempts: attemptCount,
|
attempts: attemptCount,
|
||||||
status: 200,
|
status: 200,
|
||||||
traceId,
|
traceId,
|
||||||
|
backendRequestId,
|
||||||
|
backendSpanId,
|
||||||
|
backendTraceId,
|
||||||
|
backendDuration,
|
||||||
};
|
};
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
// Handle AbortError specifically
|
// Handle AbortError specifically
|
||||||
@@ -151,6 +177,22 @@ export async function invokeWithTracking<T = any>(
|
|||||||
|
|
||||||
const errorMessage = getErrorMessage(error);
|
const errorMessage = getErrorMessage(error);
|
||||||
|
|
||||||
|
// Extract backend metadata from error context
|
||||||
|
let backendRequestId: string | undefined;
|
||||||
|
let backendSpanId: string | undefined;
|
||||||
|
let backendTraceId: string | undefined;
|
||||||
|
let backendDuration: number | undefined;
|
||||||
|
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
const context = (error as any).context;
|
||||||
|
if (context) {
|
||||||
|
backendRequestId = context['x-request-id'];
|
||||||
|
backendSpanId = context['x-span-id'];
|
||||||
|
backendTraceId = context['x-trace-id'];
|
||||||
|
backendDuration = context['x-duration-ms'] ? parseInt(context['x-duration-ms']) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Detect CORS errors specifically
|
// Detect CORS errors specifically
|
||||||
const isCorsError = errorMessage.toLowerCase().includes('cors') ||
|
const isCorsError = errorMessage.toLowerCase().includes('cors') ||
|
||||||
errorMessage.toLowerCase().includes('cross-origin') ||
|
errorMessage.toLowerCase().includes('cross-origin') ||
|
||||||
@@ -166,6 +208,12 @@ export async function invokeWithTracking<T = any>(
|
|||||||
isCorsError,
|
isCorsError,
|
||||||
debugHint: isCorsError ? 'Browser blocked request - verify CORS headers allow X-Idempotency-Key or check network connectivity' : undefined,
|
debugHint: isCorsError ? 'Browser blocked request - verify CORS headers allow X-Idempotency-Key or check network connectivity' : undefined,
|
||||||
status: (error as any)?.status,
|
status: (error as any)?.status,
|
||||||
|
backendMetadata: backendRequestId ? {
|
||||||
|
requestId: backendRequestId,
|
||||||
|
spanId: backendSpanId,
|
||||||
|
traceId: backendTraceId,
|
||||||
|
duration: backendDuration,
|
||||||
|
} : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -180,6 +228,10 @@ export async function invokeWithTracking<T = any>(
|
|||||||
attempts: attemptCount,
|
attempts: attemptCount,
|
||||||
status: (error as any)?.status,
|
status: (error as any)?.status,
|
||||||
traceId: undefined,
|
traceId: undefined,
|
||||||
|
backendRequestId,
|
||||||
|
backendSpanId,
|
||||||
|
backendTraceId,
|
||||||
|
backendDuration,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,30 @@ export const handleError = (
|
|||||||
const errorId = (context.metadata?.requestId as string) || crypto.randomUUID();
|
const errorId = (context.metadata?.requestId as string) || crypto.randomUUID();
|
||||||
const shortErrorId = errorId.slice(0, 8);
|
const shortErrorId = errorId.slice(0, 8);
|
||||||
|
|
||||||
|
// Extract backend metadata if available
|
||||||
|
let backendRequestId: string | undefined;
|
||||||
|
let backendSpanId: string | undefined;
|
||||||
|
let backendTraceId: string | undefined;
|
||||||
|
let backendDuration: number | undefined;
|
||||||
|
|
||||||
|
if (error && typeof error === 'object') {
|
||||||
|
const errorObj = error as any;
|
||||||
|
// Check for backend metadata in error context
|
||||||
|
if (errorObj.context) {
|
||||||
|
backendRequestId = errorObj.context['x-request-id'];
|
||||||
|
backendSpanId = errorObj.context['x-span-id'];
|
||||||
|
backendTraceId = errorObj.context['x-trace-id'];
|
||||||
|
backendDuration = errorObj.context['x-duration-ms'] ? parseInt(errorObj.context['x-duration-ms']) : undefined;
|
||||||
|
}
|
||||||
|
// Also check metadata property
|
||||||
|
if (context.metadata) {
|
||||||
|
backendRequestId = backendRequestId || (context.metadata.backendRequestId as string);
|
||||||
|
backendSpanId = backendSpanId || (context.metadata.backendSpanId as string);
|
||||||
|
backendTraceId = backendTraceId || (context.metadata.backendTraceId as string);
|
||||||
|
backendDuration = backendDuration || (context.metadata.backendDuration as number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if this is a connection error and dispatch event
|
// Check if this is a connection error and dispatch event
|
||||||
if (isSupabaseConnectionError(error)) {
|
if (isSupabaseConnectionError(error)) {
|
||||||
const errorMsg = getErrorMessage(error).toLowerCase();
|
const errorMsg = getErrorMessage(error).toLowerCase();
|
||||||
@@ -169,6 +193,12 @@ export const handleError = (
|
|||||||
supabaseError: supabaseErrorDetails,
|
supabaseError: supabaseErrorDetails,
|
||||||
isCorsError,
|
isCorsError,
|
||||||
debugHint: isCorsError ? 'Browser blocked request - check CORS headers or network connectivity' : undefined,
|
debugHint: isCorsError ? 'Browser blocked request - check CORS headers or network connectivity' : undefined,
|
||||||
|
backendMetadata: backendRequestId ? {
|
||||||
|
requestId: backendRequestId,
|
||||||
|
spanId: backendSpanId,
|
||||||
|
traceId: backendTraceId,
|
||||||
|
duration: backendDuration,
|
||||||
|
} : undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Additional debug logging when stack is missing
|
// Additional debug logging when stack is missing
|
||||||
@@ -204,7 +234,13 @@ export const handleError = (
|
|||||||
attempt: context.metadata?.attempt,
|
attempt: context.metadata?.attempt,
|
||||||
retriesExhausted: context.metadata?.retriesExhausted || false,
|
retriesExhausted: context.metadata?.retriesExhausted || false,
|
||||||
supabaseError: supabaseErrorDetails,
|
supabaseError: supabaseErrorDetails,
|
||||||
metadata: context.metadata
|
metadata: context.metadata,
|
||||||
|
backendMetadata: backendRequestId ? {
|
||||||
|
requestId: backendRequestId,
|
||||||
|
spanId: backendSpanId,
|
||||||
|
traceId: backendTraceId,
|
||||||
|
duration: backendDuration,
|
||||||
|
} : undefined,
|
||||||
}),
|
}),
|
||||||
p_timezone: envContext.timezone,
|
p_timezone: envContext.timezone,
|
||||||
p_referrer: document.referrer || undefined,
|
p_referrer: document.referrer || undefined,
|
||||||
@@ -221,8 +257,9 @@ export const handleError = (
|
|||||||
// Show user-friendly toast with error ID (skip for retry attempts)
|
// Show user-friendly toast with error ID (skip for retry attempts)
|
||||||
const isRetry = context.metadata?.isRetry === true || context.metadata?.attempt;
|
const isRetry = context.metadata?.isRetry === true || context.metadata?.attempt;
|
||||||
if (!isRetry) {
|
if (!isRetry) {
|
||||||
|
const refId = backendRequestId ? backendRequestId.slice(0, 8) : shortErrorId;
|
||||||
toast.error(context.action, {
|
toast.error(context.action, {
|
||||||
description: `${errorMessage}\n\nReference ID: ${shortErrorId}`,
|
description: `${errorMessage}\n\nReference ID: ${refId}`,
|
||||||
duration: 5000,
|
duration: 5000,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,19 +175,35 @@ export function wrapEdgeFunction(
|
|||||||
statusText: response.statusText
|
statusText: response.statusText
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
|
||||||
|
|
||||||
if (logResponses) {
|
if (logResponses) {
|
||||||
edgeLogger.info('Request completed', {
|
edgeLogger.info('Request completed', {
|
||||||
requestId,
|
requestId,
|
||||||
action: name,
|
action: name,
|
||||||
status: response.status,
|
status: response.status,
|
||||||
duration: span.endTime ? span.duration : Date.now() - span.startTime,
|
duration,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
endSpan(span, 'ok');
|
endSpan(span, 'ok');
|
||||||
logSpan(span);
|
logSpan(span);
|
||||||
|
|
||||||
return response;
|
// 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) {
|
} catch (error) {
|
||||||
// ====================================================================
|
// ====================================================================
|
||||||
@@ -206,6 +222,8 @@ export function wrapEdgeFunction(
|
|||||||
endSpan(span, 'error', error);
|
endSpan(span, 'error', error);
|
||||||
logSpan(span);
|
logSpan(span);
|
||||||
|
|
||||||
|
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: error.message,
|
error: error.message,
|
||||||
@@ -216,7 +234,14 @@ export function wrapEdgeFunction(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 400,
|
status: 400,
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -267,6 +292,8 @@ export function wrapEdgeFunction(
|
|||||||
endSpan(span, 'error', error);
|
endSpan(span, 'error', error);
|
||||||
logSpan(span);
|
logSpan(span);
|
||||||
|
|
||||||
|
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: message,
|
error: message,
|
||||||
@@ -276,7 +303,14 @@ export function wrapEdgeFunction(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status,
|
status,
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -300,6 +334,8 @@ export function wrapEdgeFunction(
|
|||||||
endSpan(span, 'error', error);
|
endSpan(span, 'error', error);
|
||||||
logSpan(span);
|
logSpan(span);
|
||||||
|
|
||||||
|
const duration = span.endTime ? span.duration : Date.now() - span.startTime;
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
error: 'Internal server error',
|
error: 'Internal server error',
|
||||||
@@ -308,7 +344,14 @@ export function wrapEdgeFunction(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
status: 500,
|
status: 500,
|
||||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
|
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(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user