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.
This commit is contained in:
@@ -39,7 +39,7 @@ export const loadPersonalAccountBillingPageData = cache((userId: string) => {
|
|||||||
? api.getSubscription(userId)
|
? api.getSubscription(userId)
|
||||||
: api.getOrder(userId);
|
: api.getOrder(userId);
|
||||||
|
|
||||||
const customerId = api.getBillingCustomerId(userId);
|
const customerId = api.getCustomerId(userId);
|
||||||
|
|
||||||
return Promise.all([data, customerId]);
|
return Promise.all([data, customerId]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class UserBillingService {
|
|||||||
// find the customer ID for the account if it exists
|
// find the customer ID for the account if it exists
|
||||||
// (eg. if the account has been billed before)
|
// (eg. if the account has been billed before)
|
||||||
const api = createAccountsApi(this.client);
|
const api = createAccountsApi(this.client);
|
||||||
const customerId = await api.getBillingCustomerId(accountId);
|
const customerId = await api.getCustomerId(accountId);
|
||||||
|
|
||||||
const product = billingConfig.products.find(
|
const product = billingConfig.products.find(
|
||||||
(item) => item.id === productId,
|
(item) => item.id === productId,
|
||||||
@@ -139,7 +139,7 @@ class UserBillingService {
|
|||||||
|
|
||||||
const accountId = data.id;
|
const accountId = data.id;
|
||||||
const api = createAccountsApi(this.client);
|
const api = createAccountsApi(this.client);
|
||||||
const customerId = await api.getBillingCustomerId(accountId);
|
const customerId = await api.getCustomerId(accountId);
|
||||||
const returnUrl = getBillingPortalReturnUrl();
|
const returnUrl = getBillingPortalReturnUrl();
|
||||||
|
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const loadTeamAccountBillingPage = cache((accountId: string) => {
|
|||||||
? api.getSubscription(accountId)
|
? api.getSubscription(accountId)
|
||||||
: api.getOrder(accountId);
|
: api.getOrder(accountId);
|
||||||
|
|
||||||
const customerId = api.getBillingCustomerId(accountId);
|
const customerId = api.getCustomerId(accountId);
|
||||||
|
|
||||||
return Promise.all([data, customerId]);
|
return Promise.all([data, customerId]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -87,12 +87,12 @@ class AccountsApi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name getBillingCustomerId
|
* @name getCustomerId
|
||||||
* Get the billing customer ID for the given user.
|
* Get the billing customer ID for the given user.
|
||||||
* If the user does not have a billing customer ID, it will return null.
|
* If the user does not have a billing customer ID, it will return null.
|
||||||
* @param accountId
|
* @param accountId
|
||||||
*/
|
*/
|
||||||
async getBillingCustomerId(accountId: string) {
|
async getCustomerId(accountId: string) {
|
||||||
const response = await this.client
|
const response = await this.client
|
||||||
.from('billing_customers')
|
.from('billing_customers')
|
||||||
.select('customer_id')
|
.select('customer_id')
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ The `enhanceAction` function takes two arguments:
|
|||||||
The options object can contain the following properties:
|
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`
|
- `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
|
- `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.
|
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
|
// "user" is the user object from the session
|
||||||
|
|
||||||
// "request" is the raw request object passed by POST
|
// "request" is the raw request object passed by POST
|
||||||
|
|
||||||
// if "captcha" is true, the action will require a captcha
|
// 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,
|
captcha: true,
|
||||||
|
captureException: true,
|
||||||
schema: z.object({
|
schema: z.object({
|
||||||
id: z.number()
|
id: z.number()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@kit/auth": "workspace:^",
|
"@kit/auth": "workspace:^",
|
||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
|
"@kit/monitoring": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/supabase": "workspace:^",
|
"@kit/supabase": "workspace:^",
|
||||||
"@kit/tailwind-config": "workspace:*",
|
"@kit/tailwind-config": "workspace:*",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { verifyCaptchaToken } from '@kit/auth/captcha/server';
|
|||||||
import { requireUser } from '@kit/supabase/require-user';
|
import { requireUser } from '@kit/supabase/require-user';
|
||||||
import { getSupabaseServerActionClient } from '@kit/supabase/server-actions-client';
|
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<Omit<Args, 'captchaToken'>, z.ZodTypeDef>,
|
Schema extends z.ZodType<Omit<Args, 'captchaToken'>, z.ZodTypeDef>,
|
||||||
Response,
|
Response,
|
||||||
>(
|
>(
|
||||||
fn: (params: z.infer<Schema>, user: User) => Response,
|
fn: (params: z.infer<Schema>, user: User) => Response | Promise<Response>,
|
||||||
config: {
|
config: {
|
||||||
captcha?: boolean;
|
captcha?: boolean;
|
||||||
|
captureException?: boolean;
|
||||||
schema: Schema;
|
schema: Schema;
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
@@ -52,7 +53,20 @@ export function enhanceAction<
|
|||||||
const parsed = zodParseFactory(config.schema);
|
const parsed = zodParseFactory(config.schema);
|
||||||
const data = parsed(params);
|
const data = parsed(params);
|
||||||
|
|
||||||
// pass the data to the action
|
// capture exceptions if required
|
||||||
return fn(data, auth.data);
|
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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { verifyCaptchaToken } from '@kit/auth/captcha/server';
|
|||||||
import { requireUser } from '@kit/supabase/require-user';
|
import { requireUser } from '@kit/supabase/require-user';
|
||||||
import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client';
|
import { getSupabaseRouteHandlerClient } from '@kit/supabase/route-handler-client';
|
||||||
|
|
||||||
import { zodParseFactory } from '../utils';
|
import { captureException, zodParseFactory } from '../utils';
|
||||||
|
|
||||||
interface HandlerParams<Body> {
|
interface HandlerParams<Body> {
|
||||||
request: NextRequest;
|
request: NextRequest;
|
||||||
@@ -53,6 +53,7 @@ export const enhanceRouteHandler = <
|
|||||||
// Parameters object
|
// Parameters object
|
||||||
params?: {
|
params?: {
|
||||||
captcha?: boolean;
|
captcha?: boolean;
|
||||||
|
captureException?: boolean;
|
||||||
schema?: Schema;
|
schema?: Schema;
|
||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
@@ -92,8 +93,21 @@ export const enhanceRouteHandler = <
|
|||||||
body = zodParseFactory(params.schema)(body);
|
body = zodParseFactory(params.schema)(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
// all good, call the handler with the request, body and user
|
const shouldCaptureException = params?.captureException ?? true;
|
||||||
return handler({ request, body, user });
|
|
||||||
|
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 });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -12,3 +12,14 @@ export const zodParseFactory =
|
|||||||
throw new Error(`Invalid data: ${err as string}`);
|
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);
|
||||||
|
}
|
||||||
|
|||||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@@ -1029,6 +1029,10 @@ importers:
|
|||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
|
||||||
packages/next:
|
packages/next:
|
||||||
|
dependencies:
|
||||||
|
'@kit/monitoring':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../monitoring/api
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@kit/auth':
|
'@kit/auth':
|
||||||
specifier: workspace:^
|
specifier: workspace:^
|
||||||
|
|||||||
Reference in New Issue
Block a user