6658 workflows add a first twenty piece email sender (#6965)

This commit is contained in:
martmull
2024-09-12 11:00:25 +02:00
committed by GitHub
parent f8e5b333d9
commit 3190f4a87b
397 changed files with 1143 additions and 1037 deletions

View File

@ -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();
});
});

View File

@ -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();
}
}

View File

@ -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;
});
}
}

View File

@ -0,0 +1 @@
export const EXCEPTION_HANDLER_DRIVER = Symbol('EXCEPTION_HANDLER_DRIVER');

View File

@ -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();

View File

@ -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`,
);
}
};

View 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],
};
}
}

View File

@ -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);
}
}

View File

@ -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, () => {});
},
};
},
};
};

View File

@ -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;
}

View File

@ -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;
}

View File

@ -0,0 +1,8 @@
export interface ExceptionHandlerUser {
id?: string;
email?: string;
firstName?: string;
lastName?: string;
workspaceId?: string;
workspaceDisplayName?: string;
}

View File

@ -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;

View File

@ -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';