From 03d06b64bac40aeaed966ace2747989371c14e3d Mon Sep 17 00:00:00 2001 From: giancarlo Date: Thu, 25 Apr 2024 13:19:46 +0700 Subject: [PATCH] Refactor billing code and add error monitoring Refactored the code that retrieves the billing customer id by renaming the function getBillingCustomerId to getCustomerId. Also, bolstered error handling: implemented exception capture in particular scenarios across multiple files. If an error occurs, it's now captured and reported to the configured provider. --- .../personal-account-billing-page.loader.ts | 2 +- .../_lib/server/user-billing.service.ts | 4 ++-- .../team-account-billing-page.loader.ts | 2 +- packages/features/accounts/src/server/api.ts | 4 ++-- packages/next/README.md | 4 +++- packages/next/package.json | 1 + packages/next/src/actions/index.ts | 22 +++++++++++++++---- packages/next/src/routes/index.ts | 20 ++++++++++++++--- packages/next/src/utils/index.ts | 11 ++++++++++ pnpm-lock.yaml | 4 ++++ 10 files changed, 60 insertions(+), 14 deletions(-) diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts index 2750ef78b..6632a11fa 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/personal-account-billing-page.loader.ts @@ -39,7 +39,7 @@ export const loadPersonalAccountBillingPageData = cache((userId: string) => { ? api.getSubscription(userId) : api.getOrder(userId); - const customerId = api.getBillingCustomerId(userId); + const customerId = api.getCustomerId(userId); return Promise.all([data, customerId]); }); diff --git a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts index 527f43d22..0fba68f44 100644 --- a/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts +++ b/apps/web/app/(dashboard)/home/(user)/billing/_lib/server/user-billing.service.ts @@ -59,7 +59,7 @@ class UserBillingService { // find the customer ID for the account if it exists // (eg. if the account has been billed before) const api = createAccountsApi(this.client); - const customerId = await api.getBillingCustomerId(accountId); + const customerId = await api.getCustomerId(accountId); const product = billingConfig.products.find( (item) => item.id === productId, @@ -139,7 +139,7 @@ class UserBillingService { const accountId = data.id; const api = createAccountsApi(this.client); - const customerId = await api.getBillingCustomerId(accountId); + const customerId = await api.getCustomerId(accountId); const returnUrl = getBillingPortalReturnUrl(); if (!customerId) { diff --git a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts index 6d4462d00..b685ef0c2 100644 --- a/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts +++ b/apps/web/app/(dashboard)/home/[account]/_lib/server/team-account-billing-page.loader.ts @@ -31,7 +31,7 @@ export const loadTeamAccountBillingPage = cache((accountId: string) => { ? api.getSubscription(accountId) : api.getOrder(accountId); - const customerId = api.getBillingCustomerId(accountId); + const customerId = api.getCustomerId(accountId); return Promise.all([data, customerId]); }); diff --git a/packages/features/accounts/src/server/api.ts b/packages/features/accounts/src/server/api.ts index c5e9a2044..480e2a4f1 100644 --- a/packages/features/accounts/src/server/api.ts +++ b/packages/features/accounts/src/server/api.ts @@ -87,12 +87,12 @@ class AccountsApi { } /** - * @name getBillingCustomerId + * @name getCustomerId * Get the billing customer ID for the given user. * If the user does not have a billing customer ID, it will return null. * @param accountId */ - async getBillingCustomerId(accountId: string) { + async getCustomerId(accountId: string) { const response = await this.client .from('billing_customers') .select('customer_id') diff --git a/packages/next/README.md b/packages/next/README.md index 32919ca1d..fe29c0a46 100644 --- a/packages/next/README.md +++ b/packages/next/README.md @@ -32,6 +32,7 @@ The `enhanceAction` function takes two arguments: The options object can contain the following properties: - `captcha` - If true, the action will require a captcha to be passed to the body as `captchaToken` - `schema` - A zod schema that the data will be validated against +- `captureException` - If true, the action will capture exceptions and report them to the configured provider. It is `true` by default. When successfully called, the action will return the result of the action function. @@ -70,10 +71,11 @@ export const POST = enhanceRouteHandler(({ request, body, user }) => { // "user" is the user object from the session // "request" is the raw request object passed by POST - // if "captcha" is true, the action will require a captcha + // if "captureException" is true, the action will capture exceptions and report them to the configured provider }, { captcha: true, + captureException: true, schema: z.object({ id: z.number() }), diff --git a/packages/next/package.json b/packages/next/package.json index e45be6586..896721423 100644 --- a/packages/next/package.json +++ b/packages/next/package.json @@ -16,6 +16,7 @@ "devDependencies": { "@kit/auth": "workspace:^", "@kit/eslint-config": "workspace:*", + "@kit/monitoring": "workspace:*", "@kit/prettier-config": "workspace:*", "@kit/supabase": "workspace:^", "@kit/tailwind-config": "workspace:*", diff --git a/packages/next/src/actions/index.ts b/packages/next/src/actions/index.ts index a3cd6b5db..2637f2560 100644 --- a/packages/next/src/actions/index.ts +++ b/packages/next/src/actions/index.ts @@ -10,7 +10,7 @@ import { verifyCaptchaToken } from '@kit/auth/captcha/server'; import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client'; -import { zodParseFactory } from '../utils'; +import { captureException, zodParseFactory } from '../utils'; /** * @@ -22,9 +22,10 @@ export function enhanceAction< Schema extends z.ZodType, z.ZodTypeDef>, Response, >( - fn: (params: z.infer, user: User) => Response, + fn: (params: z.infer, user: User) => Response | Promise, config: { captcha?: boolean; + captureException?: boolean; schema: Schema; }, ) { @@ -52,7 +53,20 @@ export function enhanceAction< const parsed = zodParseFactory(config.schema); const data = parsed(params); - // pass the data to the action - return fn(data, auth.data); + // capture exceptions if required + const shouldCaptureException = config.captureException ?? true; + + if (shouldCaptureException) { + try { + return await fn(data, auth.data); + } catch (error) { + await captureException(error); + + throw error; + } + } else { + // pass the data to the action + return fn(data, auth.data); + } }; } diff --git a/packages/next/src/routes/index.ts b/packages/next/src/routes/index.ts index e4c1567e6..56b56e7c9 100644 --- a/packages/next/src/routes/index.ts +++ b/packages/next/src/routes/index.ts @@ -11,7 +11,7 @@ import { verifyCaptchaToken } from '@kit/auth/captcha/server'; import { requireUser } from '@kit/supabase/require-user'; import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client'; -import { zodParseFactory } from '../utils'; +import { captureException, zodParseFactory } from '../utils'; interface HandlerParams { request: NextRequest; @@ -53,6 +53,7 @@ export const enhanceRouteHandler = < // Parameters object params?: { captcha?: boolean; + captureException?: boolean; schema?: Schema; }, ) => { @@ -92,8 +93,21 @@ export const enhanceRouteHandler = < body = zodParseFactory(params.schema)(body); } - // all good, call the handler with the request, body and user - return handler({ request, body, user }); + const shouldCaptureException = params?.captureException ?? true; + + if (shouldCaptureException) { + try { + return await handler({ request, body, user }); + } catch (error) { + // capture the exception + await captureException(error); + + throw error; + } + } else { + // all good, call the handler with the request, body and user + return handler({ request, body, user }); + } }; }; diff --git a/packages/next/src/utils/index.ts b/packages/next/src/utils/index.ts index 6eb64adea..61b457840 100644 --- a/packages/next/src/utils/index.ts +++ b/packages/next/src/utils/index.ts @@ -12,3 +12,14 @@ export const zodParseFactory = throw new Error(`Invalid data: ${err as string}`); } }; + +export async function captureException(exception: unknown) { + const { getServerMonitoringService } = await import('@kit/monitoring/server'); + + const service = await getServerMonitoringService(); + + const error = + exception instanceof Error ? exception : new Error(exception as string); + + return service.captureException(error); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09a0a4efb..817e1c4cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1029,6 +1029,10 @@ importers: version: 18.2.0 packages/next: + dependencies: + '@kit/monitoring': + specifier: workspace:* + version: link:../monitoring/api devDependencies: '@kit/auth': specifier: workspace:^