Last active
August 3, 2022 11:17
-
-
Save nodkz/d14b236d67251d2df5674cb446843732 to your computer and use it in GitHub Desktop.
GraphQL error tracking with sentry.io (ApolloServer 2019)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import express from 'express'; | |
import { ApolloServer } from 'apollo-server-express'; | |
import { ApolloServerPlugin } from 'apollo-server-plugin-base'; | |
import * as Sentry from '@sentry/node'; | |
Sentry.init({ | |
environment: process.env.APP_ENV, | |
// see why we use APP_NAME here: https://github.com/getsentry/sentry-cli/issues/482 | |
release: `${process.env.APP_NAME}-${process.env.APP_REVISION}` || '0.0.1', | |
dsn: process.env.SENTRY_DSN, | |
integrations: [ | |
// used for rewriting SourceMaps from js to ts | |
// check that sourcemaps are enabled in tsconfig.js | |
// read the docs https://docs.sentry.io/platforms/node/typescript/ | |
new RewriteFrames({ | |
root: process.cwd(), | |
}) as any, | |
// Output sended data by Sentry to console.log() | |
// new Debug({ stringify: true }), | |
], | |
}); | |
const apolloServerSentryPlugin = { | |
// For plugin definition see the docs: https://www.apollographql.com/docs/apollo-server/integrations/plugins/ | |
requestDidStart() { | |
return { | |
didEncounterErrors(rc) { | |
Sentry.withScope((scope) => { | |
scope.addEventProcessor((event) => | |
Sentry.Handlers.parseRequest(event, (rc.context as any).req) | |
); | |
// public user email | |
const userEmail = (rc.context as any).req?.session?.userId; | |
if (userEmail) { | |
scope.setUser({ | |
// id?: string; | |
ip_address: (rc.context as any).req?.ip, | |
email: userEmail, | |
}); | |
} | |
scope.setTags({ | |
graphql: rc.operation?.operation || 'parse_err', | |
graphqlName: (rc.operationName as any) || (rc.request.operationName as any), | |
}); | |
rc.errors.forEach((error) => { | |
if (error.path || error.name !== 'GraphQLError') { | |
scope.setExtras({ | |
path: error.path, | |
}); | |
Sentry.captureException(error); | |
} else { | |
scope.setExtras({}); | |
Sentry.captureMessage(`GraphQLWrongQuery: ${error.message}`); | |
} | |
}); | |
}); | |
}, | |
}; | |
}, | |
} as ApolloServerPlugin; | |
const server = new ApolloServer({ | |
schema, | |
context: ({ req }) => ({ req }), // important to pass req to context (it is used in `rc.context.req`) | |
introspection: true, | |
playground: { | |
settings: { | |
'request.credentials': 'include', | |
}, | |
} as any, | |
plugins: [apolloServerSentryPlugin], | |
}); | |
const app = express(); | |
server.applyMiddleware({ app }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* eslint-disable no-console, import/first */ | |
import path from 'path'; | |
import express from 'express'; | |
import expressStaticGzip from 'express-static-gzip'; | |
import graphqlHTTP from 'express-graphql'; | |
import PrettyError from 'pretty-error'; | |
import bodyParser from 'body-parser'; | |
import raven from 'raven'; | |
import morgan from 'morgan'; | |
import { graphqlBatchHTTPWrapper } from 'react-relay-network-layer'; | |
import { PORT, PUBLIC_URL } from 'config'; | |
import GraphQLSchema from 'schema'; | |
import * as myJWT from 'app/auth/_jwt'; | |
import serverRenderHtml from './serverRenderHtml'; | |
const pe = new PrettyError(); | |
pe.skipNodeFiles(); | |
pe.skipPackage('express', 'graphql'); | |
raven.config(!__DEV__ && 'https://secret1:secret2@sentry.io/1234567', { | |
release: __REVISION__, | |
tags: { git_commit: __GIT_COMMIT__ }, | |
environment: process.env.NODE_ENV, | |
}).install(); | |
const server = express(); | |
server.use(raven.requestHandler()); | |
server.use(morgan(__DEV__ ? 'dev' : 'combined')); | |
const graphQLMiddleware = graphqlHTTP(req => ({ | |
schema: GraphQLSchema, | |
graphiql: true, | |
formatError: (error) => { | |
if (error.path || error.name !== 'GraphQLError') { | |
console.error(pe.render(error)); | |
raven.captureException(error, | |
raven.parsers.parseRequest(req, { | |
tags: { graphql: 'exec_error' }, | |
extra: { | |
source: error.source && error.source.body, | |
positions: error.positions, | |
path: error.path, | |
}, | |
}) | |
); | |
} else { | |
console.error(pe.render(error.message)); | |
raven.captureMessage(`GraphQLWrongQuery: ${error.message}`, | |
raven.parsers.parseRequest(req, { | |
tags: { graphql: 'wrong_query' }, | |
extra: { | |
source: error.source && error.source.body, | |
positions: error.positions, | |
}, | |
}) | |
); | |
} | |
return { | |
message: error.message, | |
stack: process.env.NODE_ENV === 'development' ? error.stack.split('\n') : null, | |
}; | |
}, | |
context: { | |
userId: req.userId, | |
userEmail: req.userEmail, | |
admin: req.admin, | |
getUserPromise: req.getUserPromise ? req.getUserPromise.bind(req) : null, | |
getAdminPromise: req.getAdminPromise ? req.getAdminPromise.bind(req) : null, | |
ip: req.ip || (req.connection || {}).remoteAddress, | |
}, | |
})); | |
server.use('/graphql/batch', | |
myJWT.validateRequest, | |
bodyParser.json(), | |
graphqlBatchHTTPWrapper(graphQLMiddleware) | |
); | |
server.use('/graphql', | |
myJWT.validateRequest, | |
graphQLMiddleware | |
); | |
server.use(expressStaticGzip(path.join(__dirname, 'public'))); | |
server.get('/*', serverServeApp); | |
// Error handling | |
server.use((err, req, res, next) => { // eslint-disable-line no-unused-vars | |
console.log(pe.render(err)); // eslint-disable-line no-console | |
res.status(err.status || 500); | |
res.setHeader('Content-Type', 'text/plain'); | |
res.send(__DEV__ ? err.stack : 'Oops, internal server error'); | |
}); | |
server.listen(PORT, () => { | |
console.log(`The server is running at http://localhost:${PORT}/`); | |
}); |
With newest sentry ("@sentry/node": "^5.3.0"
) I had to rewrite raven.captureMessage
... to:
Sentry.withScope((scope) => {
scope.addEventProcessor(event => Sentry.Handlers.parseRequest(event, req));
scope.setTags({
graphql: 'wrong_query',
});
scope.setExtras({
source: error.source && error.source.body,
positions: error.positions,
});
Sentry.captureMessage(`GraphQLWrongQuery: ${error.message}`);
});
Similarly with captureException
Any clue someone using Apollo Server 2 with Sentry?
Subscriptions don't go through plugin lifecycle hooks so the above doesn't work :/
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
thank you for this snippet, come in handy