[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 })
5 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()
  },
})

Hello @pi0neerpat and @danny - thank you for putting together this helpful guide. I just wanted to bring up that adding the sentry envelop plugin appears to unmask all errors on Redwood v3.2 (not just the built-in GraphQL errors as specified here: GraphQL | RedwoodJS Docs).

This is of course a serious security issue; I was wondering whether you also encountered this and found a solution? Would it potentially be due to the fact that the extra plugins are added after the envelop plugins that are bundled Redwood?

Yes, in my experience I am no longer able to use the masking feature. Would be very helpful to be able to use both!

Just a thought, what if you add the useMaskedError plugin after useSentry again?

I haven’t tested it myself, but worth a shot.

Just to note that we have since started using GraphQL Yoga under the hood that supports error masking out of the box, but I don’t see a reason why you couldn’t add another envelop plugin!

1 Like

the install command seems wrong? Shouldn’t it be enough, to install it to the api folder?

Had some trouble getting this working initially and had to make some changes:

  1. Install the packages to the API workspace
yarn workspace api add @envelop/sentry @sentry/node @sentry/tracing
  1. Import sentry to the global namespace instead of as a named import
import '@sentry/tracing' // instead of import * as Tracing from '@sentry/tracing'
  1. extraPlugins is the correct key for assigning the plugin in the GraphQL handler

@pi0neerpat @euirim @danny are errors still unmasked still an issue with using the envelop plugin?

Here’s what you need to do to create a release and upload source maps.

redwood.toml

[web]
sourceMap = true

Api seems to always create them, so we’re fine there.

Create an SENTRY_AUTH_TOKEN according to sentrys docs and make it known to your CI.

As I’m trying to speak with a self signed local instance:

.sentryclirc

[defaults]
org=orgname
project=projectname
url=https://our-sentry-server.local

[http]
verify_ssl=false

You probably already have two sentry.ts files, add this:

release: 'web@' + process.env.CI_COMMIT_SHA,

Or, depending if it’s the api or web one:

release: 'api@' + process.env.CI_COMMIT_SHA,

Dockerfile

# do stuff

COPY --from=build /app/.sentryclirc /app/.sentryclirc

# do more stuff

RUN yarn global add @redwoodjs/cli prisma @sentry/cli

RUN sentry-cli releases new api@$CI_COMMIT_SHA && \
  sentry-cli releases files api@$CI_COMMIT_SHA upload-sourcemaps api/dist --rewrite --url-prefix "/app/api/dist" && \
  sentry-cli releases finalize api@$CI_COMMIT_SHA

RUN sentry-cli releases new web@$CI_COMMIT_SHA && \
  sentry-cli releases files web@$CI_COMMIT_SHA upload-sourcemaps web/dist && \
  sentry-cli releases finalize web@$CI_COMMIT_SHA

# remove the maps so that the browser can't offer them
RUN rm -rf /app/web/dist/static/js/*.map

# do more stuff

That should allow the maps to show up fine in sentry