DEV Community

Daniel
Daniel

Posted on

Integrating Sentry with NestJS scheduled jobs

After integrating @ntegral/nestjs-sentry in my NestJS project, I was surprised to find errors in my logs that weren't being reported to Sentry.

After a bit of investigation, I found that NestJS' native concepts of Interceptors and ExceptionFilters are built around the idea that the execution context will be somehow request-generated. i.e. These constructs expect errors to be triggered by an external request to the server via e.g. HTTP or GraphQL.

Unfortunately, when using the Cron decorator from @nestjs/schedule, my code isn't actually triggered by an external request, so errors thrown in these contexts don't seem to bubble up to the normal interceptor or exception filter pipelines.

To solve this, I took inspiration from this StackOverflow answer to create a decorator that I can use to wrap my Cron methods in an error handler that reports any caught errors to Sentry directly.

It looks like this:

// decorators/sentry-overwatch.decorator.ts import { Inject } from "@nestjs/common"; import { SentryService } from "@ntegral/nestjs-sentry"; export const SentryOverwatchAsync = () => { const injectSentry = Inject(SentryService); return ( target: any, _propertyKey: string, propertyDescriptor: PropertyDescriptor, ) => { injectSentry(target, "sentry"); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const originalMethod: () => Promise<void> = propertyDescriptor.value; propertyDescriptor.value = async function (...args: any[]) { try { return await originalMethod.apply(this, args); } catch (error) { const sentry = this.sentry as SentryService; sentry.instance().captureException(error); throw error; } }; }; }; 
Enter fullscreen mode Exit fullscreen mode

This particular function is designed to decorate an async function. For the non-async version, you'd just need to remove the async in the propertyDescriptor.value definition and the await when calling originalMethod.

With a little more work, one could write something more generalized to detect whether or not the return value is a Promise and do the right thing, but my use case is simple.

I'm then able to wrap my original function like so:

// decorators/cron.decorator.ts // Decorator ordering is important here. Swapping the order // results in Nest failing to recognize this as a scheduled // job @Cron("*/5 * * * *") @SentryOverwatchAsync() async scheduledTask(): Promise<void> { // ... } 
Enter fullscreen mode Exit fullscreen mode

But now I have to add @SentryOverwatchAsync() every time I declare a @Cron scheduled job. A little annoying, and I'm sure I'm going to forget at some point.

So using decorator composition I decided to re-export my own version of the @Cron decorator that packages the native Nest decorator in with my new custom decorator:

import { applyDecorators } from "@nestjs/common"; import { Cron as NestCron, CronOptions } from "@nestjs/schedule"; import { SentryOverwatchAsync } from "./sentry-overwatch.decorator"; export const Cron = (cronTime: string | Date, options?: CronOptions) => { // Ordering is important here, too! // The order these must appear in seems to be the reverse of // what you'd normally expect when decorating functions // declaratively. Likely because the order you specify here // is the order the decorators will be applied to the  // function in. return applyDecorators(SentryOverwatchAsync(), NestCron(cronTime, options)); }; 
Enter fullscreen mode Exit fullscreen mode

Now all I need to do is swap all of my usages of Cron to my internal Cron decorator and I have complete Sentry overwatch.

Peace of mind: achieved!

Top comments (2)

Collapse
 
wnbsmart profile image
Maroje Macola

Thanks for this tutorial Daniel, it works like a charm

Collapse
 
rohanrajpal profile image
Rohan Rajpal

Love the workaround, thanks a ton!