From d2b0a3b09db68ace9e207a28be89fba0afc5b512 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Sun, 22 Feb 2026 12:52:34 +0000 Subject: [PATCH 1/6] Enhance Supabase client handling with fallback logic and error management --- .../src/api/controllers/metrics.controller.ts | 114 ++++++++++++++++-- 1 file changed, 101 insertions(+), 13 deletions(-) diff --git a/apps/api/src/api/controllers/metrics.controller.ts b/apps/api/src/api/controllers/metrics.controller.ts index 76a7ce591..290e33e77 100644 --- a/apps/api/src/api/controllers/metrics.controller.ts +++ b/apps/api/src/api/controllers/metrics.controller.ts @@ -42,20 +42,115 @@ export interface WeeklyVolume { } let supabaseClient: SupabaseClient | null = null; +let supabaseAnonClient: SupabaseClient | null = null; -function getSupabaseClient() { +function getServiceSupabaseClient() { if (!supabaseClient) { if (!config.supabase.url) { throw new Error("Missing Supabase URL in configuration."); } - if (!config.supabase.anonKey) { - throw new Error("Missing Supabase Key in configuration."); + if (!config.supabase.serviceRoleKey) { + throw new Error("Missing Supabase service key in configuration."); } - supabaseClient = createClient(config.supabase.url, config.supabase.anonKey); + supabaseClient = createClient(config.supabase.url, config.supabase.serviceRoleKey, { + auth: { + autoRefreshToken: false, + persistSession: false + } + }); } return supabaseClient; } +function getAnonSupabaseClient() { + if (!supabaseAnonClient) { + if (!config.supabase.url) { + throw new Error("Missing Supabase URL in configuration."); + } + if (!config.supabase.anonKey) { + throw new Error("Missing Supabase anon key in configuration."); + } + supabaseAnonClient = createClient(config.supabase.url, config.supabase.anonKey, { + auth: { + autoRefreshToken: false, + persistSession: false + } + }); + } + return supabaseAnonClient; +} + +function isAuthOrPermissionError(error: any): boolean { + const errorText = `${error?.code ?? ""} ${error?.message ?? ""}`.toLowerCase(); + return ( + errorText.includes("permission") || + errorText.includes("denied") || + errorText.includes("not allowed") || + errorText.includes("invalid api key") || + errorText.includes("jwt") || + errorText.includes("401") || + errorText.includes("403") + ); +} + +async function rpcWithFallback(fn: string, params: Record): Promise { + const hasServiceKey = !!config.supabase.serviceRoleKey; + const hasAnonKey = !!config.supabase.anonKey; + + if (!config.supabase.url) { + throw new Error("Missing Supabase URL in configuration."); + } + if (!hasServiceKey && !hasAnonKey) { + throw new Error("Missing Supabase keys in configuration."); + } + + const primaryClient = hasServiceKey ? getServiceSupabaseClient() : getAnonSupabaseClient(); + const primaryAuthMode = hasServiceKey ? "service" : "anon"; + const fallbackAuthMode = hasServiceKey ? "anon" : "service"; + + const primaryResult = await primaryClient.rpc(fn, params); + if (!primaryResult.error) { + return (primaryResult.data as T) ?? ([] as unknown as T); + } + + logger.error("Supabase RPC failed", { + authMode: primaryAuthMode, + code: primaryResult.error.code, + details: primaryResult.error.details, + function: fn, + hint: primaryResult.error.hint, + message: primaryResult.error.message + }); + + const shouldFallback = hasServiceKey && hasAnonKey && isAuthOrPermissionError(primaryResult.error); + if (!shouldFallback) { + throw primaryResult.error; + } + + const fallbackClient = getAnonSupabaseClient(); + const fallbackResult = await fallbackClient.rpc(fn, params); + + if (!fallbackResult.error) { + logger.warn("Supabase RPC succeeded with fallback auth mode", { + fallbackAuthMode, + function: fn, + primaryAuthMode + }); + return (fallbackResult.data as T) ?? ([] as unknown as T); + } + + logger.error("Supabase RPC failed after fallback", { + authMode: fallbackAuthMode, + code: fallbackResult.error.code, + details: fallbackResult.error.details, + function: fn, + hint: fallbackResult.error.hint, + message: fallbackResult.error.message + }); + + throw fallbackResult.error; +} + const zeroVolume = (key: string, keyName: "day" | "month"): any => ({ [keyName]: key, chains: [] @@ -67,11 +162,8 @@ async function getMonthlyVolumes(): Promise { if (cached) return cached; try { - const supabase = getSupabaseClient(); - const { data, error } = await supabase.rpc("get_monthly_volumes_by_chain", { year_param: null }); - if (error) throw error; + const rawData = await rpcWithFallback("get_monthly_volumes_by_chain", { year_param: null }); - const rawData = (data as MonthlyVolume[]) || []; if (!rawData.length) return []; const dataMap = new Map(rawData.map(row => [row.month, row])); @@ -97,15 +189,11 @@ async function getMonthlyVolumes(): Promise { } async function getDailyVolumes(startDate: string, endDate: string): Promise { - const supabase = getSupabaseClient(); - const { data, error } = await supabase.rpc("get_daily_volumes_by_chain", { + const rawData = await rpcWithFallback("get_daily_volumes_by_chain", { end_date: endDate, start_date: startDate }); - if (error) throw error; - - const rawData = (data as DailyVolume[]) || []; const dataMap = new Map(rawData.map(row => [row.day, row])); const current = new Date(startDate); From abfe508b5ad9b6af21433e235d94917a4f34173e Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Sun, 22 Feb 2026 14:21:37 +0100 Subject: [PATCH 2/6] Update apps/api/src/api/controllers/metrics.controller.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/api/src/api/controllers/metrics.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/api/controllers/metrics.controller.ts b/apps/api/src/api/controllers/metrics.controller.ts index 290e33e77..6c6582b8d 100644 --- a/apps/api/src/api/controllers/metrics.controller.ts +++ b/apps/api/src/api/controllers/metrics.controller.ts @@ -164,7 +164,7 @@ async function getMonthlyVolumes(): Promise { try { const rawData = await rpcWithFallback("get_monthly_volumes_by_chain", { year_param: null }); - if (!rawData.length) return []; + if (!rawData || !rawData.length) return []; const dataMap = new Map(rawData.map(row => [row.month, row])); From 8cbef9b15d7bfe989ee2cab36587a5af53ea306b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:25:21 +0000 Subject: [PATCH 3/6] Initial plan From 5630834981fbab2e531f0b10bdafbda25484659d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:27:15 +0000 Subject: [PATCH 4/6] Address PR review feedback on type safety and error handling Co-authored-by: ebma <6690623+ebma@users.noreply.github.com> --- .../src/api/controllers/metrics.controller.ts | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/apps/api/src/api/controllers/metrics.controller.ts b/apps/api/src/api/controllers/metrics.controller.ts index 6c6582b8d..08afe9f14 100644 --- a/apps/api/src/api/controllers/metrics.controller.ts +++ b/apps/api/src/api/controllers/metrics.controller.ts @@ -81,19 +81,33 @@ function getAnonSupabaseClient() { } function isAuthOrPermissionError(error: any): boolean { - const errorText = `${error?.code ?? ""} ${error?.message ?? ""}`.toLowerCase(); - return ( - errorText.includes("permission") || - errorText.includes("denied") || - errorText.includes("not allowed") || - errorText.includes("invalid api key") || - errorText.includes("jwt") || - errorText.includes("401") || - errorText.includes("403") - ); + const rawCode = error?.code ?? ""; + const rawMessage = error?.message ?? ""; + const errorCode = String(rawCode).toLowerCase(); + const errorMessage = String(rawMessage).toLowerCase(); + + const hasAuthOrPermissionText = + errorMessage.includes("permission") || + errorMessage.includes("denied") || + errorMessage.includes("not allowed") || + errorMessage.includes("invalid api key") || + errorMessage.includes("jwt"); + + const has401 = + errorCode === "401" || + /\b401\b/.test(errorMessage); + + const has403 = + errorCode === "403" || + /\b403\b/.test(errorMessage); + + return hasAuthOrPermissionText || has401 || has403; } -async function rpcWithFallback(fn: string, params: Record): Promise { +async function rpcWithFallback>( + fn: string, + params: P +): Promise { const hasServiceKey = !!config.supabase.serviceRoleKey; const hasAnonKey = !!config.supabase.anonKey; @@ -110,7 +124,10 @@ async function rpcWithFallback(fn: string, params: Record): Prom const primaryResult = await primaryClient.rpc(fn, params); if (!primaryResult.error) { - return (primaryResult.data as T) ?? ([] as unknown as T); + if (primaryResult.data == null) { + return [] as T; + } + return primaryResult.data as T; } logger.error("Supabase RPC failed", { @@ -131,12 +148,15 @@ async function rpcWithFallback(fn: string, params: Record): Prom const fallbackResult = await fallbackClient.rpc(fn, params); if (!fallbackResult.error) { - logger.warn("Supabase RPC succeeded with fallback auth mode", { + logger.error("Supabase RPC succeeded with fallback auth mode - this may indicate a permission configuration issue", { fallbackAuthMode, function: fn, primaryAuthMode }); - return (fallbackResult.data as T) ?? ([] as unknown as T); + if (fallbackResult.data == null) { + return [] as T; + } + return fallbackResult.data as T; } logger.error("Supabase RPC failed after fallback", { @@ -162,7 +182,10 @@ async function getMonthlyVolumes(): Promise { if (cached) return cached; try { - const rawData = await rpcWithFallback("get_monthly_volumes_by_chain", { year_param: null }); + const rawData = await rpcWithFallback( + "get_monthly_volumes_by_chain", + { year_param: null } + ); if (!rawData || !rawData.length) return []; @@ -189,25 +212,32 @@ async function getMonthlyVolumes(): Promise { } async function getDailyVolumes(startDate: string, endDate: string): Promise { - const rawData = await rpcWithFallback("get_daily_volumes_by_chain", { - end_date: endDate, - start_date: startDate - }); + try { + const rawData = await rpcWithFallback( + "get_daily_volumes_by_chain", + { + end_date: endDate, + start_date: startDate + } + ); - const dataMap = new Map(rawData.map(row => [row.day, row])); + const dataMap = new Map(rawData.map(row => [row.day, row])); - const current = new Date(startDate); - const end = new Date(endDate); - const volumes: DailyVolume[] = []; + const current = new Date(startDate); + const end = new Date(endDate); + const volumes: DailyVolume[] = []; - while (current <= end) { - const dayStr = current.toISOString().slice(0, 10); - // If date is missing, return empty chains array instead of zeroed fields - volumes.push(dataMap.get(dayStr) || { chains: [], day: dayStr }); - current.setDate(current.getDate() + 1); - } + while (current <= end) { + const dayStr = current.toISOString().slice(0, 10); + // If date is missing, return empty chains array instead of zeroed fields + volumes.push(dataMap.get(dayStr) || { chains: [], day: dayStr }); + current.setDate(current.getDate() + 1); + } - return volumes; + return volumes; + } catch (error: any) { + throw new Error("Could not calculate daily volumes: " + error.message); + } } function aggregateWeekly(daily: DailyVolume[]): WeeklyVolume[] { From 1b05eca59864a7f2c7511c9e85dd77c6444c9980 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Feb 2026 13:27:55 +0000 Subject: [PATCH 5/6] Improve error handling with better logging and stack traces Co-authored-by: ebma <6690623+ebma@users.noreply.github.com> --- apps/api/src/api/controllers/metrics.controller.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/apps/api/src/api/controllers/metrics.controller.ts b/apps/api/src/api/controllers/metrics.controller.ts index 08afe9f14..adfe05b08 100644 --- a/apps/api/src/api/controllers/metrics.controller.ts +++ b/apps/api/src/api/controllers/metrics.controller.ts @@ -207,7 +207,9 @@ async function getMonthlyVolumes(): Promise { cache.set(cacheKey, volumes, CACHE_TTL_SECONDS); return volumes; } catch (error: any) { - throw new Error("Could not calculate monthly volumes: " + error.message); + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error("Could not calculate monthly volumes", { error: errorMessage, stack: error?.stack }); + throw new Error("Could not calculate monthly volumes: " + errorMessage); } } @@ -236,7 +238,9 @@ async function getDailyVolumes(startDate: string, endDate: string): Promise Date: Sun, 22 Feb 2026 14:43:05 +0000 Subject: [PATCH 6/6] Fix type issues --- .../src/api/controllers/metrics.controller.ts | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/apps/api/src/api/controllers/metrics.controller.ts b/apps/api/src/api/controllers/metrics.controller.ts index adfe05b08..51f670bf7 100644 --- a/apps/api/src/api/controllers/metrics.controller.ts +++ b/apps/api/src/api/controllers/metrics.controller.ts @@ -93,21 +93,14 @@ function isAuthOrPermissionError(error: any): boolean { errorMessage.includes("invalid api key") || errorMessage.includes("jwt"); - const has401 = - errorCode === "401" || - /\b401\b/.test(errorMessage); + const has401 = errorCode === "401" || /\b401\b/.test(errorMessage); - const has403 = - errorCode === "403" || - /\b403\b/.test(errorMessage); + const has403 = errorCode === "403" || /\b403\b/.test(errorMessage); return hasAuthOrPermissionText || has401 || has403; } -async function rpcWithFallback>( - fn: string, - params: P -): Promise { +async function rpcWithFallback>(fn: string, params: P): Promise { const hasServiceKey = !!config.supabase.serviceRoleKey; const hasAnonKey = !!config.supabase.anonKey; @@ -125,7 +118,7 @@ async function rpcWithFallback { if (cached) return cached; try { - const rawData = await rpcWithFallback( - "get_monthly_volumes_by_chain", - { year_param: null } - ); + const rawData = await rpcWithFallback("get_monthly_volumes_by_chain", { + year_param: null + }); if (!rawData || !rawData.length) return [];