UNPKG

@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
"use strict"; 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;