feat: add user to sentry (#3467)
* feat: wip add user to sentry * feat: wip interceptor * feat: wip add user to sentry * feat: add user into sentry errors * fix: hide stack trace in production * fix: properly log commands and handle exceptions * fix: filter command exceptions * feat: handle jobs errors
This commit is contained in:
@ -1,16 +1,26 @@
|
||||
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerConsoleDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
captureException(exception: unknown) {
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
) {
|
||||
console.group('Exception Captured');
|
||||
console.error(exception);
|
||||
console.info(options);
|
||||
console.error(exceptions);
|
||||
console.groupEnd();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
captureMessage(message: string): void {
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser): void {
|
||||
console.group('Message Captured');
|
||||
console.info(user);
|
||||
console.info(message);
|
||||
console.groupEnd();
|
||||
}
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { ProfilingIntegration } from '@sentry/profiling-node';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import {
|
||||
ExceptionHandlerDriverInterface,
|
||||
ExceptionHandlerSentryDriverFactoryOptions,
|
||||
@ -30,11 +33,69 @@ export class ExceptionHandlerSentryDriver
|
||||
});
|
||||
}
|
||||
|
||||
captureException(exception: Error) {
|
||||
Sentry.captureException(exception);
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
) {
|
||||
const eventIds: string[] = [];
|
||||
|
||||
Sentry.withScope((scope) => {
|
||||
if (options?.operation) {
|
||||
scope.setTag('operation', options.operation.name);
|
||||
scope.setTag('operationName', options.operation.name);
|
||||
}
|
||||
|
||||
if (options?.document) {
|
||||
scope.setExtra('document', options.document);
|
||||
}
|
||||
|
||||
for (const exception of exceptions) {
|
||||
const errorPath = (exception.path ?? [])
|
||||
.map((v: string | number) => (typeof v === 'number' ? '$index' : v))
|
||||
.join(' > ');
|
||||
|
||||
if (errorPath) {
|
||||
scope.addBreadcrumb({
|
||||
category: 'execution-path',
|
||||
message: errorPath,
|
||||
level: 'debug',
|
||||
});
|
||||
}
|
||||
|
||||
const eventId = Sentry.captureException(exception, {
|
||||
fingerprint: [
|
||||
'graphql',
|
||||
errorPath,
|
||||
options?.operation?.name,
|
||||
options?.operation?.type,
|
||||
],
|
||||
contexts: {
|
||||
GraphQL: {
|
||||
operationName: options?.operation?.name,
|
||||
operationType: options?.operation?.type,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
eventIds.push(eventId);
|
||||
}
|
||||
});
|
||||
|
||||
return eventIds;
|
||||
}
|
||||
|
||||
captureMessage(message: string) {
|
||||
Sentry.captureMessage(message);
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser) {
|
||||
Sentry.captureMessage(message, (scope) => {
|
||||
if (user) {
|
||||
scope.setUser({
|
||||
id: user.id,
|
||||
ip_address: user.ipAddress,
|
||||
email: user.email,
|
||||
username: user.username,
|
||||
});
|
||||
}
|
||||
|
||||
return scope;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
|
||||
import { ExceptionHandlerOptions } from 'src/integrations/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import { EXCEPTION_HANDLER_DRIVER } from './exception-handler.constants';
|
||||
import { ExceptionHandlerDriverInterface } from 'src/integrations/exception-handler/interfaces';
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/integrations/exception-handler/exception-handler.constants';
|
||||
|
||||
@Injectable()
|
||||
export class ExceptionHandlerService {
|
||||
@ -11,7 +12,10 @@ export class ExceptionHandlerService {
|
||||
private driver: ExceptionHandlerDriverInterface,
|
||||
) {}
|
||||
|
||||
captureException(exception: unknown) {
|
||||
this.driver.captureException(exception);
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
): string[] {
|
||||
return this.driver.captureExceptions(exceptions, options);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,150 @@
|
||||
import { GraphQLError, Kind, OperationDefinitionNode, print } from 'graphql';
|
||||
import * as express from 'express';
|
||||
import {
|
||||
getDocumentString,
|
||||
handleStreamOrSingleExecutionResult,
|
||||
OnExecuteDoneHookResultOnNextHook,
|
||||
Plugin,
|
||||
} from '@envelop/core';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/integrations/exception-handler/interfaces/exception-handler-user.interface';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/integrations/exception-handler/exception-handler.service';
|
||||
import { TokenService } from 'src/core/auth/services/token.service';
|
||||
import {
|
||||
convertExceptionToGraphQLError,
|
||||
filterException,
|
||||
} from 'src/filters/utils/global-exception-handler.util';
|
||||
|
||||
export type ExceptionHandlerPluginOptions = {
|
||||
/**
|
||||
* The driver to use to handle exceptions.
|
||||
*/
|
||||
exceptionHandlerService: ExceptionHandlerService;
|
||||
/**
|
||||
* The token service to use to get the token from the request.
|
||||
*/
|
||||
tokenService: TokenService;
|
||||
/**
|
||||
* The key of the event id in the error's extension. `null` to disable.
|
||||
* @default exceptionEventId
|
||||
*/
|
||||
eventIdKey?: string | null;
|
||||
};
|
||||
|
||||
export const useExceptionHandler = <
|
||||
PluginContext extends Record<string, any> = object,
|
||||
>(
|
||||
options: ExceptionHandlerPluginOptions,
|
||||
): Plugin<PluginContext> => {
|
||||
const exceptionHandlerService = options.exceptionHandlerService;
|
||||
const tokenService = options.tokenService;
|
||||
const eventIdKey = options.eventIdKey === null ? null : 'exceptionEventId';
|
||||
|
||||
function addEventId(
|
||||
err: GraphQLError,
|
||||
eventId: string | undefined | null,
|
||||
): GraphQLError {
|
||||
if (eventIdKey !== null && eventId) {
|
||||
err.extensions[eventIdKey] = eventId;
|
||||
}
|
||||
|
||||
return err;
|
||||
}
|
||||
|
||||
return {
|
||||
async onExecute({ args }) {
|
||||
const rootOperation = args.document.definitions.find(
|
||||
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
||||
) as OperationDefinitionNode;
|
||||
const operationType = rootOperation.operation;
|
||||
const document = getDocumentString(args.document, print);
|
||||
const request = args.contextValue.req as express.Request;
|
||||
const opName =
|
||||
args.operationName ||
|
||||
rootOperation.name?.value ||
|
||||
'Anonymous Operation';
|
||||
let user: ExceptionHandlerUser | undefined;
|
||||
|
||||
if (tokenService.isTokenPresent(request)) {
|
||||
try {
|
||||
const data = await tokenService.validateToken(request);
|
||||
|
||||
user = {
|
||||
id: data.user?.id,
|
||||
email: data.user?.email,
|
||||
};
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return {
|
||||
onExecuteDone(payload) {
|
||||
const handleResult: OnExecuteDoneHookResultOnNextHook<object> = ({
|
||||
result,
|
||||
setResult,
|
||||
}) => {
|
||||
if (result.errors && result.errors.length > 0) {
|
||||
const exceptions = result.errors.reduce<{
|
||||
filtered: any[];
|
||||
unfiltered: any[];
|
||||
}>(
|
||||
(acc, err) => {
|
||||
// Filter out exceptions that we don't want to be captured by exception handler
|
||||
if (filterException(err)) {
|
||||
acc.filtered.push(err);
|
||||
} else {
|
||||
acc.unfiltered.push(err);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{
|
||||
filtered: [],
|
||||
unfiltered: [],
|
||||
},
|
||||
);
|
||||
|
||||
if (exceptions.unfiltered.length > 0) {
|
||||
const eventIds = exceptionHandlerService.captureExceptions(
|
||||
exceptions.unfiltered,
|
||||
{
|
||||
operation: {
|
||||
name: opName,
|
||||
type: operationType,
|
||||
},
|
||||
document,
|
||||
user,
|
||||
},
|
||||
);
|
||||
|
||||
exceptions.unfiltered.map((err, i) =>
|
||||
addEventId(err, eventIds?.[i]),
|
||||
);
|
||||
}
|
||||
|
||||
const concatenatedErrors = [
|
||||
...exceptions.filtered,
|
||||
...exceptions.unfiltered,
|
||||
];
|
||||
const errors = concatenatedErrors.map((err) => {
|
||||
// Properly convert errors to GraphQLErrors
|
||||
const graphQLError = convertExceptionToGraphQLError(
|
||||
err.originalError,
|
||||
);
|
||||
|
||||
return graphQLError;
|
||||
});
|
||||
|
||||
setResult({
|
||||
...result,
|
||||
errors,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return handleStreamOrSingleExecutionResult(payload, handleResult);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -1,4 +1,10 @@
|
||||
import { ExceptionHandlerOptions } from './exception-handler-options.interface';
|
||||
import { ExceptionHandlerUser } from './exception-handler-user.interface';
|
||||
|
||||
export interface ExceptionHandlerDriverInterface {
|
||||
captureException(exception: unknown): void;
|
||||
captureMessage(message: string): void;
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
): string[];
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser): void;
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { OperationTypeNode } from 'graphql';
|
||||
|
||||
import { ExceptionHandlerUser } from './exception-handler-user.interface';
|
||||
|
||||
export interface ExceptionHandlerOptions {
|
||||
operation?: {
|
||||
type: OperationTypeNode;
|
||||
name: string;
|
||||
};
|
||||
document?: string;
|
||||
user?: ExceptionHandlerUser;
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
export interface ExceptionHandlerUser {
|
||||
id?: string;
|
||||
ipAddress?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
}
|
||||
Reference in New Issue
Block a user