[Guide] Setting up Sentry with Redwood (envelop version)

This entire post is written by @pi0neerpat - all credit goes to him. I’m reposting in a separate thread so that it is more easily searchable. Original post here: Using GraphQL Envelop+Helix in Redwood v0.35+ - #28 by dthyresson

As of v0.37, Redwood made the switch to using The Guild’s Envelop. As a result setting up plugins has become substantially easier! This is now the defacto way of setting up Sentry, and likely the easiest one too!


Docs for using Sentry.io with redwood

Install the required libraries:

yarn add @envelop/sentry @sentry/node @sentry/tracing

Create a reusable Sentry file api/src/lib/sentry

import * as Sentry from '@sentry/node'
import * as Tracing from '@sentry/tracing'

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.5,
})

export default Sentry

Then import it into your graphql function api/src/functions/graphql

import { createGraphQLHandler } from '@redwoodjs/graphql-server'
import { useSentry } from '@envelop/sentry'
import Sentry from 'src/lib/sentry'

import { getCurrentUser } from 'src/lib/auth'
import { logger } from 'src/lib/logger'
import directives from 'src/directives/**/*.{js,ts}'
import sdls from 'src/graphql/**/*.sdl.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
import { db } from 'src/lib/db'

const extraPlugins = [
  useSentry({
    includeRawResult: false, // set to `true` in order to include the execution result in the metadata collected
    includeResolverArgs: false, // set to `true` in order to include the args passed to resolvers
    includeExecuteVariables: false, // set to `true` in order to include the operation variables values
    // appendTags: (args) => { return { ... }} // if you wish to add custom "tags" to the Sentry transaction created per operation
  }),
]

export const handler = createGraphQLHandler({
  getCurrentUser,
  loggerConfig: {
    logger,
    options: { tracing: true, operationName: true },
  },
  directives,
  sdls,
  services,
  extraPlugins,
  onException: () => {
    db.$disconnect()
  },
})

For a custom function, such as api/src/functions/checkout

import Sentry from 'src/lib/sentry'
import { processOrder } from 'src/lib/wyre/order'

export const handler = async (event) => {
  // Default response
  let statusCode = 200
  let message = ''

  try {
    let body = event.body
    if (typeof body === 'string') body = JSON.parse(body)

    await processOrder({ orderId: body.orderId, referenceId: body.referenceId })

    return {
      statusCode,
      body: {
        message: 'Success!',
      },
    }
  } catch (e) {
    console.log(event)
    Sentry.captureException(e)
    return {
      statusCode, // Always return 200
      body: {
        message: 'Internal server error',
      },
    }
  }
}

:tada: Tada! Please share your experience if you use Sentry or other plugins

PS extra goodness in getCurrentUser

//api/src/lib/auth.js
import Sentry from 'src/lib/sentry'

export const getCurrentUser = async (session) => {
  const member = await db.member.findUnique({ where: { id: session.id } })
  if (!member) return null
  // WARNING: Returned values here are exposed to the FE
  Sentry.setUser({ username: member.username, id: member.id })
4 Likes

For whatever reason the “extraPlugins” line in the GQL handler was causing this to break. I found adding “plugins: extraPlugins” worked. Final code:

export const handler = createGraphQLHandler({
  getCurrentUser,
  loggerConfig: {
    logger,
    options: { tracing: true, operationName: true },
  },
  directives,
  sdls,
  services,
  plugins: extraPlugins,
  onException: () => {
    db.$disconnect()
  },
})

Hey @shansmith01 - not sure what the issue you’re seeing, but I’ve been using this in production for a few months.

Perhaps your extra plugins don’t have the right shape?

const extraPlugins = [
  !isNonProd && // Means it must be production
    useSentry({
      includeRawResult: false, // set to `true` in order to include the execution result in the metadata collected
      includeResolverArgs: false, // set to `true` in order to include the args passed to resolvers
      includeExecuteVariables: false, // set to `true` in order to include the operation variables values
      // appendTags: (args) => { return { ... }} // if you wish to add custom "tags" to the Sentry transaction created per operation
    }),
]

export const handler = createGraphQLHandler({
  // @ts-expect-error change current user definition
  getCurrentUser,
  loggerConfig: {
    logger,
    options: {
      operationName: true,
      userAgent: true,
      data: false,
    },
  },
  directives,
  sdls,
  services,
  extraPlugins,
  onException: () => {
    // Disconnect from your database with an unhandled exception.
    db.$disconnect()
  },
})