2024-05-01 update. Vendure added sentry plugin, which might make most of this post useless. I did not have time to explore the plugin yet. https://docs.vendure.io/reference/core-plugins/sentry-plugin/
Vendure.io is a great framework, but the official documentation does not specify anything related to error logging, or even where does the error boundary end.
Vendure does by default handle all errors thrown through the graphql and rest endpoints.
But it is quite easy to stop the whole server by simply not handling exception for example in event subscribers.
async onApplicationBootstrap()
{
this.eventBus
.ofType(eventType)
.pipe(filterRule)
.subscribe(async (event) => {
throw new Error('Not allowed')
});
}
You will stop the entire server process by throwing exception, and it is quite easy to do such mistake.
yarn add @sentry/node
yarn add @sentry/tracing
in your vendure-config file
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: IS_DEV,
});
export const config: VendureConfig = {
...
}
Errors that are thrown during the request-response cycle are caught and returned in the graphql. So here we only need to add logging.
import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';
import * as Sentry from "@sentry/node";
import "@sentry/tracing";
@Catch()
export class SentryExceptionFilter implements ExceptionFilter {
catch(exception: Error, host: ArgumentsHost) {
Sentry.captureException(exception)
}
}
@VendurePlugin({
...
providers: [
...
{
provide: APP_FILTER,
useClass: SentryExceptionFilter,
},
],
})
This captures the error when returing through GraphQL, but it still does not solve the issue for consumers and cronjobs.
To make sure all subscribers are error-free, I created a simple reusable event subscriber.
export abstract class BaseSubscriber<T extends VendureEvent & {ctx: RequestContext}> {
constructor(
protected transactionalConnection: TransactionalConnection
) {
}
public async subscribe(event: T): Promise<void> {
try {
await this.transactionalConnection.withTransaction(event.ctx, async (ctx) => {
await this.doSubscribe(ctx, event);
})
} catch (e) {
Sentry.captureException(e);
}
}
protected abstract doSubscribe(ctx: RequestContext, event: T): Promise<void>;
}
The actual event subscriber will look like this
@Injectable()
export class FulfillmentOnFulfillmentCreatedSubscriber extends BaseSubscriber<FulfillmentEvent> {
public static eventType = FulfillmentEvent;
public static filterRule = filter((event: FulfillmentEvent) => event.entity.handlerCode === CONTEST_FULFILLMENT_HANDLER_CODE);
constructor(
transactionalConnection: TransactionalConnection
) {
super(transactionalConnection);
}
public async doSubscribe(ctx: RequestContext, event: FulfillmentEvent): Promise<void>
{
// do your action
}
}
Then add the event subscriber like so in your plugin
async onApplicationBootstrap() {
this.eventBus
.ofType(CouponFulfillmentOnPaymentSettledSubscriber.eventType)
.pipe(CouponFulfillmentOnPaymentSettledSubscriber.filterRule)
.subscribe(this.couponFulfillmentOnPaymentSettledSubscriber.subscribe);
}
Manually wrapping each cron in the exception handler
const createSafeTransactionalCron = (app: INestApplicationContext, time: string, callback: (ctx: RequestContext) => Promise<void>) => {
return new CronJob(time, async () => {
const sentryLogger = app.get(SentryLogger);
const connection = app.get(TransactionalConnection);
try {
await connection.withTransaction(async (ctx) => {
await callback(ctx)
})
} catch (e) {
sentryLogger.logException(e);
}
})
}
bootstrapWorker(config)
.then(worker => {
const job = createSafeTransactionalCron(worker.app, `*/15 * * * * *`, async (ctx) => {
throw new Error('Test error');
});
job.start();
})
This should ensure that the app does not crash, and all exceptions will be visible in Sentry