Skip to content

Instantly share code, notes, and snippets.

@nodkz
Last active August 3, 2022 11:17
Show Gist options
  • Save nodkz/d14b236d67251d2df5674cb446843732 to your computer and use it in GitHub Desktop.
Save nodkz/d14b236d67251d2df5674cb446843732 to your computer and use it in GitHub Desktop.
GraphQL error tracking with sentry.io (ApolloServer 2019)
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 });
/* 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}/`);
});
@egoarka
Copy link

egoarka commented Nov 1, 2018

thank you for this snippet, come in handy

@mieszko4
Copy link

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

@romucci
Copy link

romucci commented Jul 9, 2019

Any clue someone using Apollo Server 2 with Sentry?

@intellix
Copy link

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