Add initial sentry setup

Related to #343 but will probably need a few more changes
This commit is contained in:
Kurt Hutten
2021-06-05 20:32:56 +10:00
parent c38f94558a
commit 2e91c74baf
8 changed files with 230 additions and 4 deletions

View File

@@ -3,6 +3,7 @@ import {
makeMergedSchema,
makeServices,
} from '@redwoodjs/api'
import { createSentryApolloPlugin } from 'src/lib/sentry'
import schemas from 'src/graphql/**/*.{js,ts}'
import services from 'src/services/**/*.{js,ts}'
@@ -16,6 +17,9 @@ export const handler = createGraphQLHandler({
schemas,
services: makeServices({ services }),
}),
plugins: [
createSentryApolloPlugin(),
],
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()

View File

@@ -1,8 +1,9 @@
import { createUserInsecure } from 'src/services/users/users.js'
import { db } from 'src/lib/db'
import { sentryWrapper } from 'src/lib/sentry'
import { enforceAlphaNumeric, generateUniqueString } from 'src/services/helpers'
export const handler = async (req, _context) => {
const unWrappedHandler = async (req, _context) => {
const body = JSON.parse(req.body)
console.log(body)
console.log(_context)
@@ -82,3 +83,5 @@ export const handler = async (req, _context) => {
}
}
}
export const handler = sentryWrapper(unWrappedHandler)

106
app/api/src/lib/sentry.ts Normal file
View File

@@ -0,0 +1,106 @@
import * as Sentry from '@sentry/node'
import { context, Config, ApolloError } from '@redwoodjs/api'
let sentryInitialized = false
if (process.env.SENTRY_DSN && !sentryInitialized) {
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.CONTEXT,
release: process.env.COMMIT_REF,
})
sentryInitialized = true
}
async function reportError(error) {
if (!sentryInitialized) return
// If you do have authentication set up, we can add
// some user data to help debug issues
// if (context.currentUser) {
// Sentry.configureScope((scope) => {
// scope.setUser({
// id: context?.currentUser?.id,
// email: context?.currentUser?.email,
// })
// })
// }
if (typeof error === 'string') {
Sentry.captureMessage(error)
} else {
Sentry.captureException(error)
}
await Sentry.flush()
}
export const sentryWrapper = (handler) => async (event, lambdaContext) => {
lambdaContext.callbackWaitsForEmptyEventLoop = false
try {
return await new Promise((resolve, reject) => {
const callback = (err, result) => {
if (err) {
reject(err)
} else {
resolve(result)
}
}
const resp = handler(event, lambdaContext, callback)
if (resp?.then) {
resp.then(resolve, reject)
}
})
} catch (e) {
// This catches both sync errors & promise
// rejections, because we 'await' on the handler
await reportError(e)
throw e
}
}
export const createSentryApolloPlugin: Config['plugins'][number] = () => ({
requestDidStart: () => {
return {
didEncounterErrors(ctx) {
// If we couldn't parse the operation, don't
// do anything here
if (!ctx.operation) {
return;
}
for (const err of ctx.errors) {
// Only report internal server errors,
// all errors extending ApolloError should be user-facing
if (err instanceof ApolloError) {
continue;
}
// Add scoped report details and send to Sentry
Sentry.withScope(scope => {
// Annotate whether failing operation was query/mutation/subscription
scope.setTag("kind", ctx.operation.operation);
// Log query and variables as extras (make sure to strip out sensitive data!)
scope.setExtra("query", ctx.request.query);
scope.setExtra("variables", ctx.request.variables);
if (err.path) {
// We can also add the path as breadcrumb
scope.addBreadcrumb({
category: "query-path",
message: err.path.join(" > "),
level: Sentry.Severity.Debug
});
}
const transactionId = ctx.request.http.headers.get(
"x-transaction-id"
);
if (transactionId) {
scope.setTransaction(transactionId);
}
Sentry.captureException(err);
});
}
}
}
}
})