43
loading...
This website collects cookies to deliver better user experience
tracesSampleRate
such that a sampling of traces count towards our quota. Additionally, we bind a git commit hash (exposed via Heroku's Dyno Metadata feature) to the release version, enabling Sentry to monitor release health.POST /graphql
. To achieve proper specificity, we instantiate Apollo Server with a custom plugin that qualifies transaction names with the contextual GraphQL operation when Apollo receives a request.requestDidStart
eventimport * as Sentry from "@sentry/node";
import { ApolloServerPlugin } from "apollo-server-plugin-base";
export const sentryPlugin: ApolloServerPlugin = {
requestDidStart({ request }) {
if (request.operationName) {
const scope = Sentry.getCurrentHub().getScope();
const transaction = scope?.getTransaction(); // retrieve ongoing transaction
if (transaction) {
// qualify transaction name
// i.e. "POST /graphql" -> "POST /graphql: MyOperation"
scope?.setTransactionName(
`${transaction.name}: ${request.operationName}`
);
}
}
},
};
errors
response body field and will not inherently be captured by Sentry's Express-compatible error handler. We report these errors with an identified user and context by extending our Apollo Server plugin as described in this Sentry blog.didEncounterErrors
eventimport * as Sentry from "@sentry/node";
import { ApolloError } from "apollo-server-express";
import { ApolloServerPlugin } from "apollo-server-plugin-base";
export const sentryPlugin: ApolloServerPlugin = {
requestDidStart({ request }) {
if (request.operationName) {
// qualify transaction name
// ...
}
return {
didEncounterErrors(ctx) {
if (!ctx.operation) {
return; // ignore unparsed operations
}
Sentry.withScope((scope) => {
if (ctx.context.currentUser) {
scope.setUser({
id: String(ctx.context.currentUser.id),
// ...
});
}
for (const error of ctx.errors) {
if (error.originalError instanceof ApolloError) {
continue; // ignore user-facing errors
}
Sentry.captureException(error, {
tags: {
graphqlOperation: ctx.operation?.operation,
graphqlOperationName: ctx.operationName,
},
contexts: {
graphql: {
query: ctx.request.query,
variables: JSON.stringify(
ctx.request.variables,
null,
2
),
errorPath: error.path,
},
},
});
}
});
},
};
},
};
import * as Sentry from "@sentry/node";
const server = app.listen(PORT);
process.on("SIGTERM", async function shutdown(signal: string) {
console.log(`Shutting down via ${signal}`);
try {
await Sentry.close(2000);
} catch (e) {
console.error(e);
}
server.close(() => {
console.log("HTTP server closed");
});
});
import { SentryLink } from "apollo-link-sentry";
const sentryLink = new SentryLink({
setTransaction: false,
setFingerprint: false,
attachBreadcrumbs: {
includeError: true,
},
});
onError
link actually reports GraphQL and network errors to Sentry with an explicit transaction name and context. The error handler arguments are not actually JavaScript Error
objects; therefore, Sentry.captureMessage
is invoked to improve readability within Sentry Issues. GraphQL errors are captured with a more granular fingerprint, splitting Sentry events into groups by GraphQL operation name.onError
link implementationimport { onError } from "@apollo/client/link/error";
import * as Sentry from "@sentry/react";
const errorLink = onError(({ operation, graphQLErrors, networkError }) => {
Sentry.withScope((scope) => {
scope.setTransactionName(operation.operationName);
scope.setContext("apolloGraphQLOperation", {
operationName: operation.operationName,
variables: operation.variables,
extensions: operation.extensions,
});
graphQLErrors?.forEach((error) => {
Sentry.captureMessage(error.message, {
level: Sentry.Severity.Error,
fingerprint: ["{{ default }}", "{{ transaction }}"],
contexts: {
apolloGraphQLError: {
error,
message: error.message,
extensions: error.extensions,
},
},
});
});
if (networkError) {
Sentry.captureMessage(networkError.message, {
level: Sentry.Severity.Error,
contexts: {
apolloNetworkError: {
error: networkError,
extensions: (networkError as any).extensions,
},
},
});
}
});
});