From a1280ddd054d6834d888ef592cee912f73a17d68 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 11 Nov 2025 03:55:02 +0000 Subject: [PATCH] Migrate Novu functions to wrapEdgeFunction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor Phase 3 Batch 2–4 Novu-related functions to use the createEdgeFunction wrapper, replacing explicit HTTP servers with edge wrapper, adding standardized logging, tracing, and error handling across subscriber management, topic/notification, and migration/sync functions. --- .../functions/create-novu-subscriber/index.ts | 199 +++--------------- .../functions/manage-moderator-topic/index.ts | 57 ++--- .../functions/migrate-novu-users/index.ts | 57 ++--- .../notify-system-announcement/index.ts | 87 ++------ .../functions/remove-novu-subscriber/index.ts | 74 ++----- .../sync-all-moderators-to-topic/index.ts | 66 ++---- .../functions/trigger-notification/index.ts | 55 ++--- .../update-novu-preferences/index.ts | 88 ++------ .../functions/update-novu-subscriber/index.ts | 52 ++--- 9 files changed, 166 insertions(+), 569 deletions(-) diff --git a/supabase/functions/create-novu-subscriber/index.ts b/supabase/functions/create-novu-subscriber/index.ts index 1c55731f..35a4ff35 100644 --- a/supabase/functions/create-novu-subscriber/index.ts +++ b/supabase/functions/create-novu-subscriber/index.ts @@ -1,21 +1,17 @@ -import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeaders } from '../_shared/cors.ts'; import { edgeLogger } from '../_shared/logger.ts'; import { formatEdgeError } from '../_shared/errorFormatter.ts'; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; +import { validateString } from '../_shared/typeValidation.ts'; -// Simple request tracking -const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() }); -const endRequest = (tracking: { start: number }) => Date.now() - tracking.start; - -serve(async (req) => { - const tracking = startRequest(); - - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - try { +export default createEdgeFunction( + { + name: 'create-novu-subscriber', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { @@ -26,168 +22,53 @@ serve(async (req) => { secretKey: novuApiKey }); - // Parse and validate request body - let requestBody; - try { - requestBody = await req.json(); - } catch (parseError) { - return new Response( - JSON.stringify({ - success: false, - error: 'Invalid JSON in request body', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); - } - - const { subscriberId, email, firstName, lastName, phone, avatar, data } = requestBody; + const { subscriberId, email, firstName, lastName, phone, avatar, data } = await req.json(); // Validate required fields - if (!subscriberId || typeof subscriberId !== 'string' || subscriberId.trim() === '') { - return new Response( - JSON.stringify({ - success: false, - error: 'subscriberId is required and must be a non-empty string', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); - } + validateString(subscriberId, 'subscriberId', { requestId: context.requestId }); + validateString(email, 'email', { requestId: context.requestId }); - if (!email || typeof email !== 'string' || email.trim() === '') { - return new Response( - JSON.stringify({ - success: false, - error: 'email is required and must be a non-empty string', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); - } - - // Validate email format using regex + // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(email)) { - return new Response( - JSON.stringify({ - success: false, - error: 'Invalid email format. Please provide a valid email address', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('Invalid email format. Please provide a valid email address'); } // Validate optional fields if provided if (firstName !== undefined && firstName !== null && (typeof firstName !== 'string' || firstName.length > 100)) { - return new Response( - JSON.stringify({ - success: false, - error: 'firstName must be a string with maximum 100 characters', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('firstName must be a string with maximum 100 characters'); } if (lastName !== undefined && lastName !== null && (typeof lastName !== 'string' || lastName.length > 100)) { - return new Response( - JSON.stringify({ - success: false, - error: 'lastName must be a string with maximum 100 characters', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('lastName must be a string with maximum 100 characters'); } if (phone !== undefined && phone !== null) { if (typeof phone !== 'string') { - return new Response( - JSON.stringify({ - success: false, - error: 'phone must be a string', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('phone must be a string'); } - // Validate phone format (basic validation for international numbers) const phoneRegex = /^\+?[1-9]\d{1,14}$/; if (!phoneRegex.test(phone.replace(/[\s\-\(\)]/g, ''))) { - return new Response( - JSON.stringify({ - success: false, - error: 'Invalid phone format. Please provide a valid international phone number', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('Invalid phone format. Please provide a valid international phone number'); } } if (avatar !== undefined && avatar !== null && (typeof avatar !== 'string' || !avatar.startsWith('http'))) { - return new Response( - JSON.stringify({ - success: false, - error: 'avatar must be a valid URL', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('avatar must be a valid URL'); } - // Validate data field if provided if (data !== undefined && data !== null) { if (typeof data !== 'object' || Array.isArray(data)) { - return new Response( - JSON.stringify({ - success: false, - error: 'data must be a valid object', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('data must be a valid object'); } - - // Check data size (limit to 10KB serialized) const dataSize = JSON.stringify(data).length; if (dataSize > 10240) { - return new Response( - JSON.stringify({ - success: false, - error: 'data field is too large (maximum 10KB)', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('data field is too large (maximum 10KB)'); } } - edgeLogger.info('Creating Novu subscriber', { subscriberId, email: '***', firstName, requestId: tracking.requestId }); + context.span.setAttribute('action', 'create_novu_subscriber'); + edgeLogger.info('Creating Novu subscriber', { subscriberId, email: '***', firstName, requestId: context.requestId }); const subscriber = await novu.subscribers.identify(subscriberId, { email, @@ -198,26 +79,24 @@ serve(async (req) => { data, }); - const duration = endRequest(tracking); edgeLogger.info('Subscriber created successfully', { subscriberId: subscriber.data._id, - requestId: tracking.requestId, - duration + requestId: context.requestId }); // Add subscriber to "users" topic for global announcements try { - edgeLogger.info('Adding subscriber to users topic', { subscriberId, requestId: tracking.requestId }); + edgeLogger.info('Adding subscriber to users topic', { subscriberId, requestId: context.requestId }); await novu.topics.addSubscribers('users', { subscribers: [subscriberId], }); - edgeLogger.info('Successfully added subscriber to users topic', { subscriberId, requestId: tracking.requestId }); + edgeLogger.info('Successfully added subscriber to users topic', { subscriberId, requestId: context.requestId }); } catch (topicError: unknown) { // Non-blocking - log error but don't fail the request edgeLogger.error('Failed to add subscriber to users topic', { error: formatEdgeError(topicError), subscriberId, - requestId: tracking.requestId + requestId: context.requestId }); } @@ -225,31 +104,11 @@ serve(async (req) => { JSON.stringify({ success: true, subscriberId: subscriber.data._id, - requestId: tracking.requestId }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, + headers: { 'Content-Type': 'application/json' }, status: 200, } ); - } catch (error: unknown) { - const duration = endRequest(tracking); - edgeLogger.error('Error creating Novu subscriber', { - error: formatEdgeError(error), - requestId: tracking.requestId, - duration - }); - - return new Response( - JSON.stringify({ - success: false, - error: error.message, - requestId: tracking.requestId - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json', 'X-Request-ID': tracking.requestId }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/manage-moderator-topic/index.ts b/supabase/functions/manage-moderator-topic/index.ts index 2dcb880f..9471b8b1 100644 --- a/supabase/functions/manage-moderator-topic/index.ts +++ b/supabase/functions/manage-moderator-topic/index.ts @@ -1,23 +1,22 @@ -import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; import { withEdgeRetry } from '../_shared/retryHelper.ts'; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; const TOPICS = { MODERATION_SUBMISSIONS: 'moderation-submissions', MODERATION_REPORTS: 'moderation-reports', } as const; -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - const tracking = startRequest('manage-moderator-topic'); - - try { +export default createEdgeFunction( + { + name: 'manage-moderator-topic', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { throw new Error('NOVU_API_KEY is not configured'); @@ -35,7 +34,8 @@ serve(async (req) => { throw new Error('Action must be either "add" or "remove"'); } - edgeLogger.info(`${action === 'add' ? 'Adding' : 'Removing'} user ${userId} ${action === 'add' ? 'to' : 'from'} moderator topics`, { action: 'manage_moderator_topic', requestId: tracking.requestId, userId, operation: action }); + context.span.setAttribute('action', 'manage_moderator_topic'); + edgeLogger.info(`${action === 'add' ? 'Adding' : 'Removing'} user ${userId} ${action === 'add' ? 'to' : 'from'} moderator topics`, { action: 'manage_moderator_topic', requestId: context.requestId, userId, operation: action }); const topics = [TOPICS.MODERATION_SUBMISSIONS, TOPICS.MODERATION_REPORTS]; const results = []; @@ -49,17 +49,17 @@ serve(async (req) => { await novu.topics.addSubscribers(topicKey, { subscribers: [userId], }); - edgeLogger.info('Added user to topic', { action: 'manage_moderator_topic', requestId: tracking.requestId, userId, topicKey }); + edgeLogger.info('Added user to topic', { action: 'manage_moderator_topic', requestId: context.requestId, userId, topicKey }); } else { // Remove subscriber from topic await novu.topics.removeSubscribers(topicKey, { subscribers: [userId], }); - edgeLogger.info('Removed user from topic', { action: 'manage_moderator_topic', requestId: tracking.requestId, userId, topicKey }); + edgeLogger.info('Removed user from topic', { action: 'manage_moderator_topic', requestId: context.requestId, userId, topicKey }); } }, { maxAttempts: 3, baseDelay: 1000 }, - tracking.requestId, + context.requestId, `${action}-topic-${topicKey}` ); @@ -67,7 +67,7 @@ serve(async (req) => { } catch (error: any) { edgeLogger.error(`Error ${action}ing user ${userId} ${action === 'add' ? 'to' : 'from'} topic ${topicKey}`, { action: 'manage_moderator_topic', - requestId: tracking.requestId, + requestId: context.requestId, userId, topicKey, error: error.message @@ -83,44 +83,19 @@ serve(async (req) => { const allSuccess = results.every(r => r.success); - endRequest(tracking, allSuccess ? 200 : 207); - return new Response( JSON.stringify({ success: allSuccess, userId, action, results, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: allSuccess ? 200 : 207, // 207 = Multi-Status (partial success) } ); - } catch (error: any) { - edgeLogger.error('Error managing moderator topic', { action: 'manage_moderator_topic', requestId: tracking.requestId, error: error.message }); - - endRequest(tracking, 500, error.message); - - return new Response( - JSON.stringify({ - success: false, - error: error.message, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/migrate-novu-users/index.ts b/supabase/functions/migrate-novu-users/index.ts index 7cad78ba..47805bb9 100644 --- a/supabase/functions/migrate-novu-users/index.ts +++ b/supabase/functions/migrate-novu-users/index.ts @@ -1,17 +1,16 @@ -import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - const tracking = startRequest('migrate-novu-users'); - - try { +export default createEdgeFunction( + { + name: 'migrate-novu-users', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const novuApiKey = Deno.env.get('NOVU_API_KEY'); @@ -20,6 +19,8 @@ serve(async (req) => { throw new Error('NOVU_API_KEY is not configured'); } + context.span.setAttribute('action', 'migrate_novu_users'); + // Create Supabase client with service role for admin access const supabase = createClient(supabaseUrl, supabaseServiceKey); @@ -52,20 +53,15 @@ serve(async (req) => { if (profilesError) throw profilesError; if (!profiles || profiles.length === 0) { - endRequest(tracking, 200); - return new Response( JSON.stringify({ success: true, message: 'No users to migrate', results: [], - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } @@ -133,43 +129,24 @@ serve(async (req) => { await new Promise(resolve => setTimeout(resolve, 100)); } - endRequest(tracking, 200); + edgeLogger.info('Migration complete', { + requestId: context.requestId, + total: profiles.length, + successful: results.filter(r => r.success).length + }); return new Response( JSON.stringify({ success: true, total: profiles.length, results, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } ); - } catch (error: any) { - edgeLogger.error('Error migrating Novu users', { action: 'migrate_novu_users', requestId: tracking.requestId, error: error.message }); - - endRequest(tracking, 500, error.message); - - return new Response( - JSON.stringify({ - success: false, - error: error.message, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/notify-system-announcement/index.ts b/supabase/functions/notify-system-announcement/index.ts index 54e4259e..7ea85d59 100644 --- a/supabase/functions/notify-system-announcement/index.ts +++ b/supabase/functions/notify-system-announcement/index.ts @@ -1,7 +1,7 @@ -import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { corsHeadersWithTracing as corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; interface AnnouncementPayload { title: string; @@ -10,38 +10,25 @@ interface AnnouncementPayload { actionUrl?: string; } -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - const tracking = startRequest('notify-system-announcement'); - - try { +export default createEdgeFunction( + { + name: 'notify-system-announcement', + requireAuth: true, + corsHeaders: corsHeaders + }, + async (req, context) => { const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; const supabase = createClient(supabaseUrl, supabaseServiceKey); - // Get authorization header - const authHeader = req.headers.get('authorization'); - if (!authHeader) { - throw new Error('Authorization header required'); - } - - // Verify user is admin or superuser - const token = authHeader.replace('Bearer ', ''); - const { data: { user }, error: authError } = await supabase.auth.getUser(token); - - if (authError || !user) { - throw new Error('Unauthorized: Invalid token'); - } + context.span.setAttribute('action', 'notify_system_announcement'); // Check user role const { data: roles, error: roleError } = await supabase .from('user_roles') .select('role') - .eq('user_id', user.id) + .eq('user_id', context.userId) .in('role', ['admin', 'superuser']); if (roleError || !roles || roles.length === 0) { @@ -52,7 +39,7 @@ serve(async (req) => { const { data: profile } = await supabase .from('profiles') .select('username, display_name') - .eq('user_id', user.id) + .eq('user_id', context.userId) .single(); const payload: AnnouncementPayload = await req.json(); @@ -71,7 +58,7 @@ serve(async (req) => { title: payload.title, severity: payload.severity, publishedBy: profile?.username || 'unknown', - requestId: tracking.requestId + requestId: context.requestId }); // Fetch the workflow ID for system announcements @@ -83,22 +70,13 @@ serve(async (req) => { .maybeSingle(); if (templateError) { - edgeLogger.error('Error fetching workflow', { action: 'notify_system_announcement', requestId: tracking.requestId, error: templateError }); + edgeLogger.error('Error fetching workflow', { action: 'notify_system_announcement', requestId: context.requestId, error: templateError }); throw new Error(`Failed to fetch workflow: ${templateError.message}`); } if (!template) { - edgeLogger.warn('No active system-announcement workflow found', { action: 'notify_system_announcement', requestId: tracking.requestId }); - return new Response( - JSON.stringify({ - success: false, - error: 'No active system-announcement workflow configured', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + edgeLogger.warn('No active system-announcement workflow found', { action: 'notify_system_announcement', requestId: context.requestId }); + throw new Error('No active system-announcement workflow configured'); } const announcementId = crypto.randomUUID(); @@ -117,7 +95,7 @@ serve(async (req) => { publishedBy, }; - edgeLogger.info('Triggering announcement to all users via "users" topic', { action: 'notify_system_announcement', requestId: tracking.requestId }); + edgeLogger.info('Triggering announcement to all users via "users" topic', { action: 'notify_system_announcement', requestId: context.requestId }); // Invoke the trigger-notification function with users topic const { data: result, error: notifyError } = await supabase.functions.invoke( @@ -132,13 +110,11 @@ serve(async (req) => { ); if (notifyError) { - edgeLogger.error('Error triggering notification', { action: 'notify_system_announcement', requestId: tracking.requestId, error: notifyError }); + edgeLogger.error('Error triggering notification', { action: 'notify_system_announcement', requestId: context.requestId, error: notifyError }); throw notifyError; } - edgeLogger.info('System announcement triggered successfully', { action: 'notify_system_announcement', requestId: tracking.requestId, result }); - - endRequest(tracking, 200); + edgeLogger.info('System announcement triggered successfully', { action: 'notify_system_announcement', requestId: context.requestId, result }); return new Response( JSON.stringify({ @@ -146,36 +122,13 @@ serve(async (req) => { transactionId: result?.transactionId, announcementId, payload: notificationPayload, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } ); - } catch (error: any) { - edgeLogger.error('Error in notify-system-announcement', { action: 'notify_system_announcement', requestId: tracking.requestId, error: error.message }); - - endRequest(tracking, error.message.includes('Unauthorized') ? 403 : 500, error.message); - - return new Response( - JSON.stringify({ - success: false, - error: error.message, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: error.message.includes('Unauthorized') ? 403 : 500, - } - ); } -}); +); diff --git a/supabase/functions/remove-novu-subscriber/index.ts b/supabase/functions/remove-novu-subscriber/index.ts index cdcc9e0f..40b45a75 100644 --- a/supabase/functions/remove-novu-subscriber/index.ts +++ b/supabase/functions/remove-novu-subscriber/index.ts @@ -1,16 +1,16 @@ -import { serve } from "https://deno.land/std@0.190.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; +import { validateString } from '../_shared/typeValidation.ts'; -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - const tracking = startRequest('remove-novu-subscriber'); - - try { +export default createEdgeFunction( + { + name: 'remove-novu-subscriber', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { @@ -26,83 +26,47 @@ serve(async (req) => { deleteSubscriber?: boolean; }; - if (!subscriberId || typeof subscriberId !== 'string' || subscriberId.trim() === '') { - return new Response( - JSON.stringify({ - success: false, - error: 'subscriberId is required and must be a non-empty string', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); - } + validateString(subscriberId, 'subscriberId', { requestId: context.requestId }); - edgeLogger.info('Removing subscriber from users topic', { action: 'remove_novu_subscriber', subscriberId, requestId: tracking.requestId }); + context.span.setAttribute('action', 'remove_novu_subscriber'); + edgeLogger.info('Removing subscriber from users topic', { action: 'remove_novu_subscriber', subscriberId, requestId: context.requestId }); // Remove subscriber from "users" topic try { await novu.topics.removeSubscribers('users', { subscribers: [subscriberId], }); - edgeLogger.info('Successfully removed subscriber from users topic', { action: 'remove_novu_subscriber', subscriberId }); + edgeLogger.info('Successfully removed subscriber from users topic', { action: 'remove_novu_subscriber', subscriberId, requestId: context.requestId }); } catch (topicError: any) { - edgeLogger.error('Failed to remove subscriber from users topic', { action: 'remove_novu_subscriber', subscriberId, error: topicError.message }); + edgeLogger.error('Failed to remove subscriber from users topic', { action: 'remove_novu_subscriber', subscriberId, error: topicError.message, requestId: context.requestId }); // Continue - we still want to delete the subscriber if requested } // Optionally delete the subscriber entirely from Novu if (deleteSubscriber) { try { - edgeLogger.info('Deleting subscriber from Novu', { action: 'remove_novu_subscriber', subscriberId }); + edgeLogger.info('Deleting subscriber from Novu', { action: 'remove_novu_subscriber', subscriberId, requestId: context.requestId }); await novu.subscribers.delete(subscriberId); - edgeLogger.info('Successfully deleted subscriber from Novu', { action: 'remove_novu_subscriber', subscriberId }); + edgeLogger.info('Successfully deleted subscriber from Novu', { action: 'remove_novu_subscriber', subscriberId, requestId: context.requestId }); } catch (deleteError: any) { - edgeLogger.error('Failed to delete subscriber from Novu', { action: 'remove_novu_subscriber', subscriberId, error: deleteError.message }); + edgeLogger.error('Failed to delete subscriber from Novu', { action: 'remove_novu_subscriber', subscriberId, error: deleteError.message, requestId: context.requestId }); throw deleteError; } } - endRequest(tracking, 200); - return new Response( JSON.stringify({ success: true, subscriberId, removedFromTopic: true, deleted: deleteSubscriber, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } ); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - edgeLogger.error('Error removing Novu subscriber', { action: 'remove_novu_subscriber', error: errorMessage, requestId: tracking.requestId }); - - endRequest(tracking, 500, errorMessage); - - return new Response( - JSON.stringify({ - success: false, - error: errorMessage, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/sync-all-moderators-to-topic/index.ts b/supabase/functions/sync-all-moderators-to-topic/index.ts index 8c3a9823..0cf29f49 100644 --- a/supabase/functions/sync-all-moderators-to-topic/index.ts +++ b/supabase/functions/sync-all-moderators-to-topic/index.ts @@ -1,23 +1,22 @@ -import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from '../_shared/logger.ts'; +import { edgeLogger } from '../_shared/logger.ts'; import { withEdgeRetry } from '../_shared/retryHelper.ts'; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; const TOPICS = { MODERATION_SUBMISSIONS: 'moderation-submissions', MODERATION_REPORTS: 'moderation-reports', } as const; -serve(async (req) => { - const tracking = startRequest(); - - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - try { +export default createEdgeFunction( + { + name: 'sync-all-moderators-to-topic', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); const supabaseUrl = Deno.env.get('SUPABASE_URL'); const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY'); @@ -29,8 +28,9 @@ serve(async (req) => { const novu = new Novu({ secretKey: novuApiKey }); const supabase = createClient(supabaseUrl, supabaseServiceKey); + context.span.setAttribute('action', 'sync_moderators'); edgeLogger.info('Starting moderator sync to Novu topics', { - requestId: tracking.requestId, + requestId: context.requestId, action: 'sync_moderators' }); @@ -48,7 +48,7 @@ serve(async (req) => { const uniqueUserIds = [...new Set(moderatorRoles.map(r => r.user_id))]; edgeLogger.info('Found unique moderators to sync', { - requestId: tracking.requestId, + requestId: context.requestId, count: uniqueUserIds.length }); @@ -62,12 +62,12 @@ serve(async (req) => { try { // Ensure topic exists (Novu will create it if it doesn't) await novu.topics.create({ key: topicKey, name: topicKey }); - edgeLogger.info('Topic ready', { requestId: tracking.requestId, topicKey }); + edgeLogger.info('Topic ready', { requestId: context.requestId, topicKey }); } catch (error: any) { // Topic might already exist, which is fine if (!error.message?.includes('already exists')) { edgeLogger.warn('Note about topic', { - requestId: tracking.requestId, + requestId: context.requestId, topicKey, error: error.message }); @@ -90,20 +90,20 @@ serve(async (req) => { }); }, { maxAttempts: 3, baseDelay: 2000 }, - tracking.requestId, + context.requestId, `sync-batch-${topicKey}-${i}` ); successCount += batch.length; edgeLogger.info('Added batch of users to topic', { - requestId: tracking.requestId, + requestId: context.requestId, topicKey, batchSize: batch.length }); } catch (error: any) { errorCount += batch.length; edgeLogger.error('Error adding batch to topic', { - requestId: tracking.requestId, + requestId: context.requestId, topicKey, batchSize: batch.length, error: error.message @@ -118,10 +118,8 @@ serve(async (req) => { }); } - const duration = endRequest(tracking); edgeLogger.info('Sync completed', { - requestId: tracking.requestId, - duration, + requestId: context.requestId, results }); @@ -130,39 +128,13 @@ serve(async (req) => { success: true, message: 'Moderator sync completed', results, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } ); - } catch (error: any) { - const duration = endRequest(tracking); - edgeLogger.error('Error syncing moderators to topics', { - requestId: tracking.requestId, - duration, - error: error.message - }); - - return new Response( - JSON.stringify({ - success: false, - error: error.message, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/trigger-notification/index.ts b/supabase/functions/trigger-notification/index.ts index 18f41948..05c004f5 100644 --- a/supabase/functions/trigger-notification/index.ts +++ b/supabase/functions/trigger-notification/index.ts @@ -1,16 +1,15 @@ -import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - const tracking = startRequest('trigger-notification'); - - try { +export default createEdgeFunction( + { + name: 'trigger-notification', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { @@ -38,10 +37,11 @@ serve(async (req) => { ? { subscriberId } : { topicKey: topicKey! }; + context.span.setAttribute('action', 'trigger_notification'); edgeLogger.info('Triggering notification', { workflowId, recipient, - requestId: tracking.requestId, + requestId: context.requestId, action: 'trigger_notification' }); @@ -53,50 +53,21 @@ serve(async (req) => { }); edgeLogger.info('Notification triggered successfully', { - requestId: tracking.requestId, + requestId: context.requestId, transactionId: result.data.transactionId }); - endRequest(tracking, 200); - return new Response( JSON.stringify({ success: true, transactionId: result.data.transactionId, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } ); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - edgeLogger.error('Error triggering notification', { - requestId: tracking.requestId, - error: errorMessage - }); - - endRequest(tracking, 500, errorMessage); - - return new Response( - JSON.stringify({ - success: false, - error: errorMessage, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/update-novu-preferences/index.ts b/supabase/functions/update-novu-preferences/index.ts index 561ecb33..7c8325b6 100644 --- a/supabase/functions/update-novu-preferences/index.ts +++ b/supabase/functions/update-novu-preferences/index.ts @@ -1,17 +1,16 @@ -import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; -serve(async (req) => { - const tracking = startRequest('update-novu-preferences'); - - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - try { +export default createEdgeFunction( + { + name: 'update-novu-preferences', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); const supabaseUrl = Deno.env.get('SUPABASE_URL')!; const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; @@ -28,33 +27,16 @@ serve(async (req) => { const { userId, preferences } = await req.json(); - edgeLogger.info('Updating preferences for user', { userId, requestId: tracking.requestId }); + context.span.setAttribute('action', 'update_novu_preferences'); + edgeLogger.info('Updating preferences for user', { userId, requestId: context.requestId }); // Validate input if (!userId) { - return new Response( - JSON.stringify({ - success: false, - error: 'userId is required', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('userId is required'); } if (!preferences?.channelPreferences) { - return new Response( - JSON.stringify({ - success: false, - error: 'channelPreferences is required in preferences object', - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 400, - } - ); + throw new Error('channelPreferences is required in preferences object'); } // Get Novu subscriber ID from database @@ -96,7 +78,7 @@ serve(async (req) => { edgeLogger.error('Failed to update channel preference', { channel: channelType, error: channelError.message, - requestId: tracking.requestId + requestId: context.requestId }); results.push({ channel: channelType, @@ -114,54 +96,24 @@ serve(async (req) => { if (!allSucceeded) { edgeLogger.warn('Some channel preferences failed to update', { failedChannels: failedChannels.map(c => c.channel), - requestId: tracking.requestId + requestId: context.requestId }); - return new Response( - JSON.stringify({ - success: false, - error: 'Some channel preferences failed to update', - results, - failedChannels: failedChannels.map(c => c.channel), - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 502, // Bad Gateway - external service failure - } - ); + throw new Error('Some channel preferences failed to update: ' + failedChannels.map(c => c.channel).join(', ')); } - const duration = endRequest(tracking); edgeLogger.info('All preferences updated successfully', { - requestId: tracking.requestId, - duration + requestId: context.requestId }); + return new Response( JSON.stringify({ success: true, results, }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json' }, status: 200, } ); - } catch (error: any) { - const duration = endRequest(tracking); - edgeLogger.error('Error updating Novu preferences', { - error: error.message, - requestId: tracking.requestId, - duration - }); - - return new Response( - JSON.stringify({ - success: false, - error: error.message, - }), - { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, - status: 500, - } - ); } -}); +); diff --git a/supabase/functions/update-novu-subscriber/index.ts b/supabase/functions/update-novu-subscriber/index.ts index 2471f277..5d0d7a66 100644 --- a/supabase/functions/update-novu-subscriber/index.ts +++ b/supabase/functions/update-novu-subscriber/index.ts @@ -1,16 +1,15 @@ -import { serve } from "https://deno.land/std@0.168.0/http/server.ts"; import { Novu } from "npm:@novu/api@1.6.0"; import { corsHeaders } from '../_shared/cors.ts'; -import { edgeLogger, startRequest, endRequest } from "../_shared/logger.ts"; +import { edgeLogger } from "../_shared/logger.ts"; +import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts'; -serve(async (req) => { - if (req.method === 'OPTIONS') { - return new Response(null, { headers: corsHeaders }); - } - - const tracking = startRequest('update-novu-subscriber'); - - try { +export default createEdgeFunction( + { + name: 'update-novu-subscriber', + requireAuth: false, + corsHeaders: corsHeaders + }, + async (req, context) => { const novuApiKey = Deno.env.get('NOVU_API_KEY'); if (!novuApiKey) { @@ -31,7 +30,8 @@ serve(async (req) => { data?: Record; }; - edgeLogger.info('Updating Novu subscriber', { action: 'update_novu_subscriber', subscriberId, email, firstName, requestId: tracking.requestId }); + context.span.setAttribute('action', 'update_novu_subscriber'); + edgeLogger.info('Updating Novu subscriber', { action: 'update_novu_subscriber', subscriberId, email, firstName, requestId: context.requestId }); const subscriber = await novu.subscribers.update(subscriberId, { email, @@ -42,45 +42,19 @@ serve(async (req) => { data, }); - edgeLogger.info('Subscriber updated successfully', { action: 'update_novu_subscriber', subscriberId: subscriber.data._id }); - - endRequest(tracking, 200); + edgeLogger.info('Subscriber updated successfully', { action: 'update_novu_subscriber', subscriberId: subscriber.data._id, requestId: context.requestId }); return new Response( JSON.stringify({ success: true, subscriberId: subscriber.data._id, - requestId: tracking.requestId }), { headers: { - ...corsHeaders, 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId }, status: 200, } ); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - edgeLogger.error('Error updating Novu subscriber', { action: 'update_novu_subscriber', error: errorMessage, requestId: tracking.requestId }); - - endRequest(tracking, 500, errorMessage); - - return new Response( - JSON.stringify({ - success: false, - error: errorMessage, - requestId: tracking.requestId - }), - { - headers: { - ...corsHeaders, - 'Content-Type': 'application/json', - 'X-Request-ID': tracking.requestId - }, - status: 500, - } - ); } -}); +);