How to capture TypeScript Firebase functions errors in Sentry

  • Łukasz Sągol
    Łukasz Sągol
    Co-founder
We’re building a whiteboard for software engineers. Read about how

If you use TypeScript, Cloud Functions for Firebase and Sentry, there’s an easy way to ensure that any error that occurs in a function is automatically sent to Sentry.

If you write your Firebase functions in a different language, you can almost certainly adapt the code sample below to achieve the same objectives, but for the sake of convenience we’re assuming that you’re writing in TypeScript and using the @sentry/node library to access Sentry.

Using wrappers to catch errors

We’ve created a set of Sentry ‘wrappers’ for Firebase functions that give you a one-line way to capture errors from:

Each wrapper follows the same pattern and sequence of events:

  1. Start a Sentry transaction
  2. Set the transaction context to provide some more information about the function that’s being tracked – e.g. the function name
  3. Try calling the function handler itself
  4. Catch any errors and send them to Sentry
  5. Finish the Sentry transaction

The Sentry wrapper

We’re using onCall functions as an example here but the same principles apply to other types.

import * as Sentry from '@sentry/node'
import { https } from 'firebase-functions'

Sentry
  .init
  // Add your Sentry configuration here or import it
  ()

export const httpsOnCallWrapper = (
  // We pass an identifying ‘name’ as a string
  // This will show up in our Sentry error titles
  // so it needs to a) be unique and b) make sense

  name: string,

  // This is the handler itself, which previously
  // you would have exported directly from the
  // function file

  handler: (data: any, context: https.CallableContext) => any | Promise<any>
) => {
  return async (data: any, context: https.CallableContext) => {
    // 1. Start the Sentry transaction

    const transaction = Sentry.startTransaction({
      name,
      op: 'functions.https.onCall',
    })

    // 2. Set the transaction context
    // In this example, we’re sending the uid from Firebase auth
    // You can send any relevant data here that might help with
    // debugging

    Sentry.setContext('Function context', {
      ...(data || {}),
      uid: context.auth?.uid,
      function: name,
      op: 'functions.https.onCall',
    })

    try {
      // 3. Try calling the function handler itself

      return await handler(data, context)
    } catch (e) {
      // 4. Send any errors to Sentry

      await Sentry.captureException(e)
      await Sentry.flush(1000)

      // Don’t forget to throw them too!

      throw e
    } finally {
      // 5. Finish the Sentry transaction

      Sentry.configureScope((scope) => scope.clear())
      transaction.finish()
    }
  }
}

Suggestions for wrapping other function types:

  • You’ll need to change the argument types for the wrapper and its return to match the expected arguments of the function type
  • For triggered functions, we find it useful to capture some additional information about the trigger in the Sentry context. For example, in Firestore triggered functions, we capture the path of the Firestore document that activated the trigger.

Wrapping functions

To put it all together, take a look at this simple function example:

import * as functions from 'firebase-functions'

const helloWorldHandler = async () => {
  // Do exciting function things here

  return { done: true }
}

exports = module.exports = functions.https.onCall(helloWorldHandler)

Now, to wrap it, all you need to do is:

exports = module.exports = functions.https.onCall(
  httpsOnCallWrapper('helloWorld', helloWorldHandler)
)

instead, and any errors in the helloWorldHandler() function will be captured in Sentry.

If you have questions or comments about this post, please tweet us @qualdesk or me @lukaszsagol. And let us know how you get on.

We’re building a whiteboard for software engineers. Read about how