mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 04:51:13 -05:00
Add comprehensive edge-function error handling
Enhance error handling and logging across all edge functions: - Introduce a shared edgeFunctionWrapper with standardized error handling, request/response logging, tracing, and validation hooks. - Add runtime type validation utilities (ValidationError, validators, and parse/validate helpers) and integrate into edge flow. - Implement robust validation for incoming requests and known type mismatches, with detailed logs and structured responses. - Add post-RPC and post-database error logging to surface type/mismatch issues early. - Update approval/rejection entry points to leverage new validators and centralized error handling.
This commit is contained in:
347
supabase/functions/_shared/README.md
Normal file
347
supabase/functions/_shared/README.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# Edge Function Shared Utilities
|
||||
|
||||
Comprehensive error handling, logging, and type validation utilities for Supabase Edge Functions.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Using the Edge Function Wrapper (Recommended)
|
||||
|
||||
The easiest way to create a new edge function with full error handling:
|
||||
|
||||
```typescript
|
||||
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
||||
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { corsHeaders } from '../_shared/cors.ts';
|
||||
import { validateUUID, validateString } from '../_shared/typeValidation.ts';
|
||||
|
||||
serve(createEdgeFunction({
|
||||
name: 'my-function',
|
||||
requireAuth: true,
|
||||
corsHeaders,
|
||||
}, async (req, { requestId, span, userId }) => {
|
||||
// Parse and validate request
|
||||
const body = await req.json();
|
||||
validateUUID(body.id, 'id', { requestId });
|
||||
validateString(body.name, 'name', { requestId });
|
||||
|
||||
// Your business logic here
|
||||
const result = await processRequest(body);
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true, data: result }),
|
||||
{
|
||||
status: 200,
|
||||
headers: { ...corsHeaders, 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}));
|
||||
```
|
||||
|
||||
This automatically provides:
|
||||
- ✅ CORS handling
|
||||
- ✅ Authentication validation
|
||||
- ✅ Request/response logging
|
||||
- ✅ Distributed tracing
|
||||
- ✅ Comprehensive error handling
|
||||
- ✅ Performance monitoring
|
||||
|
||||
## Type Validation
|
||||
|
||||
### Basic Validation Functions
|
||||
|
||||
```typescript
|
||||
import {
|
||||
validateRequired,
|
||||
validateString,
|
||||
validateUUID,
|
||||
validateArray,
|
||||
validateUUIDArray,
|
||||
validateEntityType,
|
||||
validateActionType,
|
||||
validateObject,
|
||||
} from '../_shared/typeValidation.ts';
|
||||
|
||||
// Validate required field
|
||||
validateRequired(value, 'fieldName', { requestId });
|
||||
|
||||
// Validate string
|
||||
validateString(value, 'name', { requestId });
|
||||
|
||||
// Validate UUID
|
||||
validateUUID(value, 'userId', { requestId });
|
||||
|
||||
// Validate array with minimum length
|
||||
validateArray(value, 'itemIds', 1, { requestId });
|
||||
|
||||
// Validate array of UUIDs
|
||||
validateUUIDArray(value, 'submissionIds', 1, { requestId });
|
||||
|
||||
// Validate entity type (park, ride, company, etc.)
|
||||
validateEntityType(value, 'item_type', { requestId });
|
||||
|
||||
// Validate action type (create, edit, delete)
|
||||
validateActionType(value, 'action_type', { requestId });
|
||||
```
|
||||
|
||||
### Submission Item Validation
|
||||
|
||||
```typescript
|
||||
import { validateSubmissionItem } from '../_shared/typeValidation.ts';
|
||||
|
||||
const item = validateSubmissionItem(rawData, { requestId });
|
||||
// Returns: { id: string, item_type: ValidEntityType, action_type: ValidActionType }
|
||||
```
|
||||
|
||||
### Valid Entity Types
|
||||
|
||||
The following entity types are recognized by the system:
|
||||
|
||||
- `park`
|
||||
- `ride`
|
||||
- `manufacturer`
|
||||
- `operator`
|
||||
- `property_owner`
|
||||
- `designer`
|
||||
- `company` (consolidated type)
|
||||
- `ride_model`
|
||||
- `photo`
|
||||
- `milestone`
|
||||
- `timeline_event`
|
||||
|
||||
### Type Guards
|
||||
|
||||
```typescript
|
||||
import {
|
||||
isString,
|
||||
isUUID,
|
||||
isArray,
|
||||
isObject,
|
||||
isValidEntityType,
|
||||
isValidActionType,
|
||||
} from '../_shared/typeValidation.ts';
|
||||
|
||||
if (isString(value)) {
|
||||
// TypeScript now knows value is a string
|
||||
}
|
||||
|
||||
if (isValidEntityType(type)) {
|
||||
// TypeScript knows type is ValidEntityType
|
||||
}
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Automatic Error Categorization
|
||||
|
||||
The edge function wrapper automatically handles:
|
||||
|
||||
#### Validation Errors (400 Bad Request)
|
||||
```json
|
||||
{
|
||||
"error": "Invalid entity type: operator_invalid",
|
||||
"field": "item_type",
|
||||
"expected": "one of: park, ride, manufacturer, ...",
|
||||
"received": "operator_invalid",
|
||||
"requestId": "abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
#### Database Errors
|
||||
- **23505** → 409 Conflict (unique constraint violation)
|
||||
- **23503** → 400 Bad Request (foreign key violation)
|
||||
- **23514** → 400 Bad Request (check constraint violation)
|
||||
- **P0001** → 400 Bad Request (raised exception)
|
||||
- **42501** → 403 Forbidden (insufficient privilege)
|
||||
|
||||
#### Authentication Errors (401 Unauthorized)
|
||||
```json
|
||||
{
|
||||
"error": "Missing Authorization header",
|
||||
"requestId": "abc-123"
|
||||
}
|
||||
```
|
||||
|
||||
### Manual Error Formatting
|
||||
|
||||
```typescript
|
||||
import { formatEdgeError, toError } from '../_shared/errorFormatter.ts';
|
||||
|
||||
try {
|
||||
// Some operation
|
||||
} catch (error) {
|
||||
// Get human-readable error message
|
||||
const message = formatEdgeError(error);
|
||||
|
||||
// Convert to Error instance
|
||||
const err = toError(error);
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## Logging
|
||||
|
||||
### Structured Logging
|
||||
|
||||
```typescript
|
||||
import { edgeLogger } from '../_shared/logger.ts';
|
||||
|
||||
edgeLogger.info('Processing submission', {
|
||||
requestId,
|
||||
submissionId,
|
||||
itemCount,
|
||||
});
|
||||
|
||||
edgeLogger.warn('Slow query detected', {
|
||||
requestId,
|
||||
queryTime: 1500,
|
||||
query: 'fetch_submissions',
|
||||
});
|
||||
|
||||
edgeLogger.error('Failed to process item', {
|
||||
requestId,
|
||||
error: formatEdgeError(error),
|
||||
itemId,
|
||||
});
|
||||
```
|
||||
|
||||
### Sensitive Data Protection
|
||||
|
||||
The logger automatically redacts sensitive fields:
|
||||
- `password`
|
||||
- `token`
|
||||
- `secret`
|
||||
- `api_key`
|
||||
- `authorization`
|
||||
- `email`
|
||||
- `phone`
|
||||
- `ssn`
|
||||
- `credit_card`
|
||||
- `ip_address`
|
||||
- `session_id`
|
||||
|
||||
## Distributed Tracing
|
||||
|
||||
### Using Spans
|
||||
|
||||
```typescript
|
||||
import {
|
||||
startSpan,
|
||||
endSpan,
|
||||
addSpanEvent,
|
||||
setSpanAttributes,
|
||||
logSpan,
|
||||
} from '../_shared/logger.ts';
|
||||
|
||||
// Create a child span for a database operation
|
||||
const dbSpan = startSpan(
|
||||
'fetch_submissions',
|
||||
'DATABASE',
|
||||
getSpanContext(parentSpan),
|
||||
{
|
||||
'db.operation': 'select',
|
||||
'db.table': 'content_submissions',
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
addSpanEvent(dbSpan, 'query_start');
|
||||
|
||||
const result = await supabase
|
||||
.from('content_submissions')
|
||||
.select('*');
|
||||
|
||||
addSpanEvent(dbSpan, 'query_complete', {
|
||||
rowCount: result.data?.length,
|
||||
});
|
||||
|
||||
endSpan(dbSpan, 'ok');
|
||||
} catch (error) {
|
||||
addSpanEvent(dbSpan, 'query_failed', {
|
||||
error: formatEdgeError(error),
|
||||
});
|
||||
endSpan(dbSpan, 'error', error);
|
||||
throw error;
|
||||
} finally {
|
||||
logSpan(dbSpan);
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### Before (Manual Error Handling)
|
||||
|
||||
```typescript
|
||||
Deno.serve(async (req) => {
|
||||
if (req.method === 'OPTIONS') {
|
||||
return new Response(null, { status: 204, headers: corsHeaders });
|
||||
}
|
||||
|
||||
try {
|
||||
const authHeader = req.headers.get('Authorization');
|
||||
if (!authHeader) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Unauthorized' }),
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await req.json();
|
||||
|
||||
// Business logic...
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
return new Response(
|
||||
JSON.stringify({ error: String(error) }),
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### After (Using Wrapper)
|
||||
|
||||
```typescript
|
||||
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
|
||||
import { validateString } from '../_shared/typeValidation.ts';
|
||||
|
||||
serve(createEdgeFunction({
|
||||
name: 'my-function',
|
||||
requireAuth: true,
|
||||
corsHeaders,
|
||||
}, async (req, { requestId, userId }) => {
|
||||
const body = await req.json();
|
||||
validateString(body.name, 'name', { requestId });
|
||||
|
||||
// Business logic...
|
||||
|
||||
return new Response(
|
||||
JSON.stringify({ success: true }),
|
||||
{ status: 200, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}));
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Consistent Error Handling**: All errors are formatted consistently
|
||||
2. **Better Debugging**: Request IDs and trace IDs link errors across services
|
||||
3. **Type Safety**: Catch type mismatches early with clear error messages
|
||||
4. **Security**: Automatic redaction of sensitive data in logs
|
||||
5. **Performance Monitoring**: Built-in timing and span tracking
|
||||
6. **Maintainability**: Less boilerplate code in each function
|
||||
|
||||
## Example: Moderation Approval Function
|
||||
|
||||
See `process-selective-approval/index.ts` for a complete example of using these utilities in a production edge function.
|
||||
|
||||
Key features demonstrated:
|
||||
- Type validation for submission items
|
||||
- Entity type checking
|
||||
- Database error handling
|
||||
- Distributed tracing across RPC calls
|
||||
- Performance monitoring
|
||||
Reference in New Issue
Block a user