@envelop/sentry
Version:
This plugins collects errors and performance tracing for your execution flow, and reports it to [Sentry](https://sentry.io/).
207 lines (206 loc) • 9.82 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.useSentry = exports.defaultSkipError = void 0;
const tslib_1 = require("tslib");
const core_1 = require("@envelop/core");
const on_resolve_1 = require("@envelop/on-resolve");
const Sentry = tslib_1.__importStar(require("@sentry/node"));
const graphql_1 = require("graphql");
function defaultSkipError(error) {
return error instanceof Error;
}
exports.defaultSkipError = defaultSkipError;
const sentryTracingSymbol = Symbol('sentryTracing');
const useSentry = (options = {}) => {
function pick(key, defaultValue) {
return options[key] ?? defaultValue;
}
const startTransaction = pick('startTransaction', true);
const trackResolvers = pick('trackResolvers', true);
const includeResolverArgs = pick('includeResolverArgs', false);
const includeRawResult = pick('includeRawResult', false);
const includeExecuteVariables = pick('includeExecuteVariables', false);
const renameTransaction = pick('renameTransaction', false);
const skipOperation = pick('skip', () => false);
const skipError = pick('skipError', defaultSkipError);
function addEventId(err, eventId) {
if (options.eventIdKey === null) {
return err;
}
const eventIdKey = options.eventIdKey ?? 'sentryEventId';
return new graphql_1.GraphQLError(err.message, err.nodes, err.source, err.positions, err.path, undefined, {
...err.extensions,
[eventIdKey]: eventId,
});
}
const onResolve = trackResolvers
? ({ args: resolversArgs, info, context }) => {
const { rootSpan, opName, operationType } = context[sentryTracingSymbol];
if (rootSpan) {
const { fieldName, returnType, parentType } = info;
const parent = rootSpan;
const tags = {
fieldName,
parentType: parentType.toString(),
returnType: returnType.toString(),
};
if (includeResolverArgs) {
tags.args = JSON.stringify(resolversArgs || {});
}
const childSpan = parent.startChild({
op: `${parentType.name}.${fieldName}`,
tags,
});
return ({ result }) => {
if (includeRawResult) {
childSpan.setData('result', result);
}
if (result instanceof Error && !skipError(result)) {
// Map index values in list to $index for better grouping of events.
const errorPath = (0, graphql_1.responsePathAsArray)(info.path)
.map(v => (typeof v === 'number' ? '$index' : v))
.join(' > ');
Sentry.captureException(result, {
fingerprint: ['graphql', errorPath, opName, operationType],
});
}
childSpan.finish();
};
}
return () => { };
}
: undefined;
return {
onPluginInit({ addPlugin }) {
if (onResolve) {
addPlugin((0, on_resolve_1.useOnResolve)(onResolve));
}
},
onExecute({ args, extendContext }) {
if (skipOperation(args)) {
return;
}
const rootOperation = args.document.definitions.find(
// @ts-expect-error TODO: not sure how we will make it dev friendly
o => o.kind === graphql_1.Kind.OPERATION_DEFINITION);
const operationType = rootOperation.operation;
const document = (0, graphql_1.print)(args.document);
const opName = args.operationName || rootOperation.name?.value || 'Anonymous Operation';
const addedTags = (options.appendTags && options.appendTags(args)) || {};
const traceparentData = (options.traceparentData && options.traceparentData(args)) || {};
const transactionName = options.transactionName ? options.transactionName(args) : opName;
const op = options.operationName ? options.operationName(args) : 'execute';
const tags = {
operationName: opName,
operation: operationType,
...addedTags,
};
let rootSpan;
if (startTransaction) {
rootSpan = Sentry.startTransaction({
name: transactionName,
op,
tags,
...traceparentData,
});
if (!rootSpan) {
const error = [
`Could not create the root Sentry transaction for the GraphQL operation "${transactionName}".`,
`It's very likely that this is because you have not included the Sentry tracing SDK in your app's runtime before handling the request.`,
];
throw new Error(error.join('\n'));
}
}
else {
const scope = Sentry.getCurrentHub().getScope();
const parentSpan = scope?.getSpan();
const span = parentSpan?.startChild({
description: transactionName,
op,
tags,
});
if (!span) {
// eslint-disable-next-line no-console
console.warn([
`Flag "startTransaction" is enabled but Sentry failed to find a transaction.`,
`Try to create a transaction before GraphQL execution phase is started.`,
].join('\n'));
return {};
}
rootSpan = span;
if (renameTransaction) {
scope.setTransactionName(transactionName);
}
}
Sentry.configureScope(scope => scope.setSpan(rootSpan));
rootSpan.setData('document', document);
if (options.configureScope) {
Sentry.configureScope(scope => options.configureScope(args, scope));
}
if (onResolve) {
const sentryContext = {
rootSpan,
opName,
operationType,
};
extendContext({ [sentryTracingSymbol]: sentryContext });
}
return {
onExecuteDone(payload) {
const handleResult = ({ result, setResult }) => {
if (includeRawResult) {
rootSpan.setData('result', result);
}
if (result.errors && result.errors.length > 0) {
Sentry.withScope(scope => {
scope.setTransactionName(opName);
scope.setTag('operation', operationType);
scope.setTag('operationName', opName);
scope.setExtra('document', document);
scope.setTags(addedTags || {});
if (includeRawResult) {
scope.setExtra('result', result);
}
if (includeExecuteVariables) {
scope.setExtra('variables', args.variableValues);
}
const errors = result.errors?.map(err => {
const errorPath = (err.path ?? []).join(' > ');
if (errorPath) {
scope.addBreadcrumb({
category: 'execution-path',
message: errorPath,
level: 'debug',
});
}
// Map index values in list to $index for better grouping of events.
const errorPathWithIndex = (err.path ?? [])
.map((v) => (typeof v === 'number' ? '$index' : v))
.join(' > ');
const eventId = Sentry.captureException(err, {
fingerprint: ['graphql', errorPathWithIndex, opName, operationType],
contexts: {
GraphQL: {
operationName: opName,
operationType,
variables: args.variableValues,
},
},
});
return addEventId(err, eventId);
});
setResult({
...result,
errors,
});
});
}
rootSpan.finish();
};
return (0, core_1.handleStreamOrSingleExecutionResult)(payload, handleResult);
},
};
},
};
};
exports.useSentry = useSentry;