DEV Community

Daniel
Daniel

Posted on

6 1

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!

Image of Datadog

The Future of AI, LLMs, and Observability on Google Cloud

Datadog sat down with Google’s Director of AI to discuss the current and future states of AI, ML, and LLMs on Google Cloud. Discover 7 key insights for technical leaders, covering everything from upskilling teams to observability best practices

Learn More

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!

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more