6658 workflows add a first twenty piece email sender (#6965)
This commit is contained in:
@ -0,0 +1,26 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/engine/core-modules/exception-handler/exception-handler.constants';
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
|
||||
describe('ExceptionHandlerService', () => {
|
||||
let service: ExceptionHandlerService;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ExceptionHandlerService,
|
||||
{
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useValue: {},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ExceptionHandlerService>(ExceptionHandlerService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(service).toBeDefined();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
/* eslint-disable no-console */
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerConsoleDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
) {
|
||||
console.group('Exception Captured');
|
||||
console.info(options);
|
||||
console.error(exceptions);
|
||||
console.groupEnd();
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser): void {
|
||||
console.group('Message Captured');
|
||||
console.info(user);
|
||||
console.info(message);
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { ProfilingIntegration } from '@sentry/profiling-node';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import {
|
||||
ExceptionHandlerDriverInterface,
|
||||
ExceptionHandlerSentryDriverFactoryOptions,
|
||||
} from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
export class ExceptionHandlerSentryDriver
|
||||
implements ExceptionHandlerDriverInterface
|
||||
{
|
||||
constructor(options: ExceptionHandlerSentryDriverFactoryOptions['options']) {
|
||||
Sentry.init({
|
||||
environment: options.environment,
|
||||
release: options.release,
|
||||
dsn: options.dsn,
|
||||
integrations: [
|
||||
new Sentry.Integrations.Http({ tracing: true }),
|
||||
new Sentry.Integrations.Express({ app: options.serverInstance }),
|
||||
new Sentry.Integrations.GraphQL(),
|
||||
new Sentry.Integrations.Postgres(),
|
||||
new ProfilingIntegration(),
|
||||
],
|
||||
tracesSampleRate: 0.1,
|
||||
profilesSampleRate: 0.3,
|
||||
debug: options.debug,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (options?.user) {
|
||||
scope.setUser({
|
||||
id: options.user.id,
|
||||
email: options.user.email,
|
||||
firstName: options.user.firstName,
|
||||
lastName: options.user.lastName,
|
||||
workspaceId: options.user.workspaceId,
|
||||
workspaceDisplayName: options.user.workspaceDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
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, user?: ExceptionHandlerUser) {
|
||||
Sentry.captureMessage(message, (scope) => {
|
||||
if (user) {
|
||||
scope.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
workspaceId: user.workspaceId,
|
||||
workspaceDisplayName: user.workspaceDisplayName,
|
||||
});
|
||||
}
|
||||
|
||||
return scope;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
export const EXCEPTION_HANDLER_DRIVER = Symbol('EXCEPTION_HANDLER_DRIVER');
|
||||
@ -0,0 +1,14 @@
|
||||
import { ConfigurableModuleBuilder } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerModuleOptions } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
export const {
|
||||
ConfigurableModuleClass,
|
||||
MODULE_OPTIONS_TOKEN,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} = new ConfigurableModuleBuilder<ExceptionHandlerModuleOptions>({
|
||||
moduleName: 'ExceptionHandlerModule',
|
||||
})
|
||||
.setClassMethodName('forRoot')
|
||||
.build();
|
||||
@ -0,0 +1,42 @@
|
||||
import { HttpAdapterHost } from '@nestjs/core';
|
||||
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { OPTIONS_TYPE } from 'src/engine/core-modules/exception-handler/exception-handler.module-definition';
|
||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
|
||||
/**
|
||||
* ExceptionHandler Module factory
|
||||
* @returns ExceptionHandlerModuleOptions
|
||||
* @param environmentService
|
||||
* @param adapterHost
|
||||
*/
|
||||
export const exceptionHandlerModuleFactory = async (
|
||||
environmentService: EnvironmentService,
|
||||
adapterHost: HttpAdapterHost,
|
||||
): Promise<typeof OPTIONS_TYPE> => {
|
||||
const driverType = environmentService.get('EXCEPTION_HANDLER_DRIVER');
|
||||
|
||||
switch (driverType) {
|
||||
case ExceptionHandlerDriver.Console: {
|
||||
return {
|
||||
type: ExceptionHandlerDriver.Console,
|
||||
};
|
||||
}
|
||||
case ExceptionHandlerDriver.Sentry: {
|
||||
return {
|
||||
type: ExceptionHandlerDriver.Sentry,
|
||||
options: {
|
||||
environment: environmentService.get('SENTRY_ENVIRONMENT'),
|
||||
release: environmentService.get('SENTRY_RELEASE'),
|
||||
dsn: environmentService.get('SENTRY_DSN') ?? '',
|
||||
serverInstance: adapterHost.httpAdapter?.getInstance(),
|
||||
debug: environmentService.get('DEBUG_MODE'),
|
||||
},
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error(
|
||||
`Invalid exception capturer driver type (${driverType}), check your .env file`,
|
||||
);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,59 @@
|
||||
import { DynamicModule, Global, Module } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service';
|
||||
import { ExceptionHandlerDriver } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/engine/core-modules/exception-handler/exception-handler.constants';
|
||||
import {
|
||||
ConfigurableModuleClass,
|
||||
OPTIONS_TYPE,
|
||||
ASYNC_OPTIONS_TYPE,
|
||||
} from 'src/engine/core-modules/exception-handler/exception-handler.module-definition';
|
||||
import { ExceptionHandlerConsoleDriver } from 'src/engine/core-modules/exception-handler/drivers/console.driver';
|
||||
import { ExceptionHandlerSentryDriver } from 'src/engine/core-modules/exception-handler/drivers/sentry.driver';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [ExceptionHandlerService],
|
||||
exports: [ExceptionHandlerService],
|
||||
})
|
||||
export class ExceptionHandlerModule extends ConfigurableModuleClass {
|
||||
static forRoot(options: typeof OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useValue:
|
||||
options.type === ExceptionHandlerDriver.Console
|
||||
? new ExceptionHandlerConsoleDriver()
|
||||
: new ExceptionHandlerSentryDriver(options.options),
|
||||
};
|
||||
const dynamicModule = super.forRoot(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
|
||||
static forRootAsync(options: typeof ASYNC_OPTIONS_TYPE): DynamicModule {
|
||||
const provider = {
|
||||
provide: EXCEPTION_HANDLER_DRIVER,
|
||||
useFactory: async (...args: any[]) => {
|
||||
const config = await options?.useFactory?.(...args);
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config.type === ExceptionHandlerDriver.Console
|
||||
? new ExceptionHandlerConsoleDriver()
|
||||
: new ExceptionHandlerSentryDriver(config.options);
|
||||
},
|
||||
inject: options.inject || [],
|
||||
};
|
||||
const dynamicModule = super.forRootAsync(options);
|
||||
|
||||
return {
|
||||
...dynamicModule,
|
||||
providers: [...(dynamicModule.providers ?? []), provider],
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
|
||||
import { ExceptionHandlerDriverInterface } from 'src/engine/core-modules/exception-handler/interfaces';
|
||||
import { EXCEPTION_HANDLER_DRIVER } from 'src/engine/core-modules/exception-handler/exception-handler.constants';
|
||||
|
||||
@Injectable()
|
||||
export class ExceptionHandlerService {
|
||||
constructor(
|
||||
@Inject(EXCEPTION_HANDLER_DRIVER)
|
||||
private driver: ExceptionHandlerDriverInterface,
|
||||
) {}
|
||||
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
): string[] {
|
||||
return this.driver.captureExceptions(exceptions, options);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
handleStreamOrSingleExecutionResult,
|
||||
Plugin,
|
||||
getDocumentString,
|
||||
} from '@envelop/core';
|
||||
import { OperationDefinitionNode, Kind, print } from 'graphql';
|
||||
|
||||
import { GraphQLContext } from 'src/engine/api/graphql/graphql-config/graphql-config.service';
|
||||
|
||||
export const useSentryTracing = <
|
||||
PluginContext extends GraphQLContext,
|
||||
>(): Plugin<PluginContext> => {
|
||||
return {
|
||||
onExecute({ args }) {
|
||||
const transactionName = args.operationName || 'Anonymous Operation';
|
||||
const rootOperation = args.document.definitions.find(
|
||||
(o) => o.kind === Kind.OPERATION_DEFINITION,
|
||||
) as OperationDefinitionNode;
|
||||
const operationType = rootOperation.operation;
|
||||
|
||||
const user = args.contextValue.user;
|
||||
const workspace = args.contextValue.workspace;
|
||||
const document = getDocumentString(args.document, print);
|
||||
|
||||
Sentry.setTags({
|
||||
operationName: transactionName,
|
||||
operation: operationType,
|
||||
});
|
||||
|
||||
const scope = Sentry.getCurrentScope();
|
||||
|
||||
scope.setTransactionName(transactionName);
|
||||
|
||||
if (user) {
|
||||
scope.setUser({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
firstName: user.firstName,
|
||||
lastName: user.lastName,
|
||||
workspaceId: workspace?.id,
|
||||
workspaceDisplayName: workspace?.displayName,
|
||||
});
|
||||
}
|
||||
|
||||
if (document) {
|
||||
scope.setExtra('document', document);
|
||||
}
|
||||
|
||||
return {
|
||||
onExecuteDone(payload) {
|
||||
return handleStreamOrSingleExecutionResult(payload, () => {});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,10 @@
|
||||
import { ExceptionHandlerOptions } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-options.interface';
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
|
||||
export interface ExceptionHandlerDriverInterface {
|
||||
captureExceptions(
|
||||
exceptions: ReadonlyArray<any>,
|
||||
options?: ExceptionHandlerOptions,
|
||||
): string[];
|
||||
captureMessage(message: string, user?: ExceptionHandlerUser): void;
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
import { OperationTypeNode } from 'graphql';
|
||||
|
||||
import { ExceptionHandlerUser } from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-user.interface';
|
||||
|
||||
export interface ExceptionHandlerOptions {
|
||||
operation?: {
|
||||
type: OperationTypeNode;
|
||||
name: string;
|
||||
};
|
||||
document?: string;
|
||||
user?: ExceptionHandlerUser | null;
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
export interface ExceptionHandlerUser {
|
||||
id?: string;
|
||||
email?: string;
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
workspaceId?: string;
|
||||
workspaceDisplayName?: string;
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { Router } from 'express';
|
||||
|
||||
export enum ExceptionHandlerDriver {
|
||||
Sentry = 'sentry',
|
||||
Console = 'console',
|
||||
}
|
||||
|
||||
export interface ExceptionHandlerSentryDriverFactoryOptions {
|
||||
type: ExceptionHandlerDriver.Sentry;
|
||||
options: {
|
||||
environment?: string;
|
||||
release?: string;
|
||||
dsn: string;
|
||||
serverInstance?: Router;
|
||||
debug?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExceptionHandlerDriverFactoryOptions {
|
||||
type: ExceptionHandlerDriver.Console;
|
||||
}
|
||||
|
||||
export type ExceptionHandlerModuleOptions =
|
||||
| ExceptionHandlerSentryDriverFactoryOptions
|
||||
| ExceptionHandlerDriverFactoryOptions;
|
||||
@ -0,0 +1,2 @@
|
||||
export * from 'src/engine/core-modules/exception-handler/interfaces/exception-handler.interface';
|
||||
export * from 'src/engine/core-modules/exception-handler/interfaces/exception-handler-driver.interface';
|
||||
Reference in New Issue
Block a user