Private Singleton for Scoping Sentry SDK

Time to read: 7 minutes

Sentry is a great error tracking tool. Sentry browser defaults to using global this as a memoization tool.

This blog explores singleton typescript patterns for scoping sentry down when using it on someone elses website.

This will hopefully help you only capture your own errors, and also not annotate any existing sentry contexts with your embedded applications data.

But why 🤷

Embedded products are things that bundle into other websites. You might have one???

I'll narrow this down to javascript served (probably from a CDN) onto someone elses website; your customers if you are lucky.

Third party JS is a necessary component of some products. The maintenance and observability of those 3rd party scripts by the authors is crucial.

When you welcome 3rd party JS to your website, you allow it to do a lot. If you are doing this without a Content Security Policy (CSP) then your users data could be at risk and used in any way.

Manning has a book about it; here's a snippet:

Third-party JavaScript applications are self-contained components, typically small scripts or widgets, that add functionality to websites. As the name implies, they're offered by independent organizations, with code and asset files served from a remote web address. Third Party JS, Ben Vinegar and Anton Kovalyov - Manning

Design

To create a global singleton like Sentry I will use the Singleton pattern.

I can make use of a private constructor and static methods that refer to a closured instance to make the API seem like it's mimicking Sentry.

When it's finished it will work like this:

import { ScopedSentry } from './utils'

// Your code goes here

ScopedSentry.trackSessionId("11-22-33-asd")
ScopedSentry.captureException(new Error("captured"))

Creating a singleton

We need to do four things to create a singleton.

  1. First we need to create a class and declare it to have an instance property that is of it's self as a type.
  2. Second, be sure to declare the constructor to be private so that calls to new ScopedSentry will raise compiler errors in Typescript.
  3. Thirdly we need to write the method that can be used to create or return the instance. Notice that the constructor is called in this private method. This is fine because we can invoke private methods when we are inside a private scope already.
  4. So lastly we will implement the constructor for the Singleton class.

The code for this is below.

// ../_snippets/scoped-sentry/index.ts

import * as Sentry from "@sentry/browser";
import { defaultStackParser, Hub, makeFetchTransport } from "@sentry/browser";

export class ScopedSentry {
  private static instance: ScopedSentry;
  private hub: Sentry.Hub;
  private client: Sentry.BrowserClient;

  private constructor() {
    this.hub = new Hub();
    this.client = this.buildClient();
    this.hub.bindClient(this.client);
  }

  private static getInstance(): ScopedSentry {
    if (!ScopedSentry.instance) {
      ScopedSentry.instance = new ScopedSentry();
    }
    return ScopedSentry.instance;
  }

  public static trackSessionId(sessionId: string) {
    const { hub } = this.getInstance();
    hub.configureScope(function (scope) {
      scope.setTag("session-id", sessionId);
    });
  }

  public static captureMessage(...args: Parameters<Hub["captureMessage"]>) {
    const { hub } = this.getInstance();
    hub.captureMessage(...args);
    this.safeFlush();
  }

  public static captureException(...args: Parameters<Hub["captureException"]>) {
    const { hub } = this.getInstance();
    hub.captureException(...args);
    this.safeFlush();
  }

  public static async safeFlush() {
    const { client } = this.getInstance();
    return client
      .flush(300)
      .then((flushed) => {
        return flushed;
      })
      .catch((e) => {
        console.warn("Flush failed");
      });
  }

  private buildClient() {
    return new Sentry.BrowserClient({
      dsn: process.env.SENTRY_DSN,
      environment: process.env.SENTRY_ENVIRONMENT,
      integrations: [],
      stackParser: defaultStackParser,
      transport: makeFetchTransport,
    });
  }
}

I've added some public methods that fetch the private instance of the closured Hub and then provide additional context. I've also implemented a public methods for captureMessage and captureException that will ensure errors are sent to sentry within 300 milliseconds using the flush.

Wrap up

I've taken inspiration from this thread and read a fair amount of Sentry browser and javascript SDK sort code to arrive at this solution.

Hopefully learning how to create private singletons is useful for you 👍