How to capture TypeScript Firebase functions errors in Sentry
- Łukasz SągolCo-founder
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:
- In-app
onCall
functions - HTTPS
onRequest
functions - Firestore, Realtime Database, Remote Config, Authentication, Analytics, Cloud Storage, Pub/Sub and Test Lab triggered functions
Each wrapper follows the same pattern and sequence of events:
- Start a Sentry transaction
- Set the transaction context to provide some more information about the function that’s being tracked – e.g. the function name
- Try calling the function handler itself
- Catch any errors and send them to Sentry
- 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.