feat: introduce Sentry

This commit is contained in:
Ali BARIN
2023-03-10 18:53:10 +00:00
parent 2c18667ffd
commit 4d90df9d9a
19 changed files with 197 additions and 13 deletions

View File

@@ -27,6 +27,8 @@
"@graphql-tools/graphql-file-loader": "^7.3.4",
"@graphql-tools/load": "^7.5.2",
"@rudderstack/rudder-sdk-node": "^1.1.2",
"@sentry/node": "^7.42.0",
"@sentry/tracing": "^7.42.0",
"@types/luxon": "^2.3.1",
"ajv-formats": "^2.1.1",
"axios": "0.24.0",

View File

@@ -1,9 +1,12 @@
import createError from 'http-errors';
import express from 'express';
import appConfig from './config/app';
import cors from 'cors';
import { IRequest } from '@automatisch/types';
import appConfig from './config/app';
import corsOptions from './config/cors-options';
import morgan from './helpers/morgan';
import * as Sentry from './helpers/sentry.ee';
import appAssetsHandler from './helpers/app-assets-handler';
import webUIHandler from './helpers/web-ui-handler';
import errorHandler from './helpers/error-handler';
@@ -14,12 +17,16 @@ import {
} from './helpers/create-bull-board-handler';
import injectBullBoardHandler from './helpers/inject-bull-board-handler';
import router from './routes';
import { IRequest } from '@automatisch/types';
createBullBoardHandler(serverAdapter);
const app = express();
Sentry.init(app);
Sentry.attachRequestHandler(app);
Sentry.attachTracingHandler(app);
injectBullBoardHandler(app, serverAdapter);
appAssetsHandler(app);
@@ -50,6 +57,8 @@ app.use(function (req, res, next) {
next(createError(404));
});
Sentry.attachErrorHandler(app);
app.use(errorHandler);
export default app;

View File

@@ -44,6 +44,7 @@ type AppConfig = {
stripeStarterPriceKey: string;
stripeGrowthPriceKey: string;
licenseKey: string;
sentryDsn: string;
};
const host = process.env.HOST || 'localhost';
@@ -115,6 +116,7 @@ const appConfig: AppConfig = {
stripeStarterPriceKey: process.env.STRIPE_STARTER_PRICE_KEY,
stripeGrowthPriceKey: process.env.STRIPE_GROWTH_PRICE_KEY,
licenseKey: process.env.LICENSE_KEY,
sentryDsn: process.env.SENTRY_DSN,
};
if (!appConfig.encryptionKey) {

View File

@@ -1,5 +1,7 @@
import { Response } from 'express';
import { IRequest } from '@automatisch/types';
import * as Sentry from '../../helpers/sentry.ee';
import Billing from '../../helpers/billing/index.ee';
import appConfig from '../../config/app';
import logger from '../../helpers/logger';
@@ -18,6 +20,8 @@ export default async (request: IRequest, response: Response) => {
return response.sendStatus(200);
} catch (error) {
logger.error(`Webhook Error: ${error.message}`);
Sentry.captureException(error);
return response.sendStatus(400);
}
};

View File

@@ -2,6 +2,7 @@ import { IJSONObject } from '@automatisch/types';
export default class BaseError extends Error {
details = {};
statusCode?: number;
constructor(error?: string | IJSONObject) {
let computedError: Record<string, unknown>;

View File

@@ -0,0 +1,9 @@
import BaseError from './base';
export default class QuotaExceededError extends BaseError {
constructor(error = 'The allowed task quota has been exhausted!') {
super(error);
this.statusCode = 422;
}
}

View File

@@ -1,16 +1,15 @@
import { Request, Response } from 'express';
import { NextFunction, Request, Response } from 'express';
import logger from './logger';
type Error = {
message: string;
};
import BaseError from '../errors/base';
const errorHandler = (err: Error, req: Request, res: Response): void => {
// Do not remove `next` argument as the function signature will not fit for an error handler middleware
const errorHandler = (err: BaseError, req: Request, res: Response, next: NextFunction): void => {
if (err.message === 'Not Found') {
res.status(404).end();
} else {
logger.error(err.message);
res.status(500).end();
res.status(err.statusCode || 500).send(err.message);
}
};

View File

@@ -4,8 +4,10 @@ import { loadSchemaSync } from '@graphql-tools/load';
import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader';
import { addResolversToSchema } from '@graphql-tools/schema';
import { applyMiddleware } from 'graphql-middleware';
import logger from '../helpers/logger';
import authentication from '../helpers/authentication';
import * as Sentry from '../helpers/sentry.ee';
import resolvers from '../graphql/resolvers';
import HttpError from '../errors/http';
@@ -28,6 +30,15 @@ const graphQLInstance = graphqlHTTP({
delete (error.originalError as HttpError).response;
}
Sentry.captureException(error, {
tags: { graphql: true },
extra: {
source: error.source?.body,
positions: error.positions,
path: error.path
}
})
return error;
},
});

View File

@@ -0,0 +1,51 @@
import { Express } from 'express';
import * as Sentry from '@sentry/node';
import type { CaptureContext } from '@sentry/types';
import * as Tracing from '@sentry/tracing';
import appConfig from '../config/app';
export function init(app?: Express) {
if (!appConfig.isCloud) return;
return Sentry.init({
enabled: !!appConfig.sentryDsn,
dsn: appConfig.sentryDsn,
integrations: [
app && new Sentry.Integrations.Http({ tracing: true }),
app && new Tracing.Integrations.Express({ app }),
app && new Tracing.Integrations.GraphQL(),
].filter(Boolean),
tracesSampleRate: 1.0,
});
}
export function attachRequestHandler(app: Express) {
if (!appConfig.isCloud) return;
app.use(Sentry.Handlers.requestHandler());
}
export function attachTracingHandler(app: Express) {
if (!appConfig.isCloud) return;
app.use(Sentry.Handlers.tracingHandler());
}
export function attachErrorHandler(app: Express) {
if (!appConfig.isCloud) return;
app.use(Sentry.Handlers.errorHandler({
shouldHandleError() {
// TODO: narrow down the captured errors in time as we receive samples
return true;
}
}));
}
export function captureException(exception: any, captureContext?: CaptureContext) {
if (!appConfig.isCloud) return;
return Sentry.captureException(exception, captureContext);
}

View File

@@ -7,6 +7,7 @@ import Step from './step';
import User from './user';
import Execution from './execution';
import Telemetry from '../helpers/telemetry';
import QuotaExceededError from '../errors/quote-exceeded';
class Flow extends Base {
id!: string;
@@ -152,7 +153,7 @@ class Flow extends Base {
const hasExceeded = await this.checkIfQuotaExceeded();
if (hasExceeded) {
throw new Error('The allowed task quota has been exhausted!');
throw new QuotaExceededError();
}
return this;

View File

@@ -21,7 +21,7 @@ const exposeError = (handler: RequestHandler) => async (req: IRequest, res: Resp
try {
await handler(req, res, next);
} catch (err) {
res.status(422).send(err);
next(err);
}
}

View File

@@ -1,3 +1,7 @@
import * as Sentry from './helpers/sentry.ee';
Sentry.init();
import './config/orm';
import './helpers/check-worker-readiness';
import './workers/flow';

View File

@@ -1,4 +1,6 @@
import { Worker } from 'bullmq';
import * as Sentry from '../helpers/sentry.ee';
import redisConfig from '../config/redis';
import logger from '../helpers/logger';
import Step from '../models/step';
@@ -65,6 +67,12 @@ worker.on('failed', (job, err) => {
logger.info(
`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}`
);
Sentry.captureException(err, {
extra: {
jobId: job.id,
}
});
});
process.on('SIGTERM', async () => {

View File

@@ -1,4 +1,6 @@
import { Worker } from 'bullmq';
import * as Sentry from '../helpers/sentry.ee';
import redisConfig from '../config/redis';
import logger from '../helpers/logger';
import User from '../models/user';
@@ -37,6 +39,12 @@ worker.on('failed', (job, err) => {
logger.info(
`JOB ID: ${job.id} - The user with the ID of '${job.data.id}' has failed to be deleted! ${err.message}`
);
Sentry.captureException(err, {
extra: {
jobId: job.id,
}
});
});
process.on('SIGTERM', async () => {

View File

@@ -1,4 +1,6 @@
import { Worker } from 'bullmq';
import * as Sentry from '../helpers/sentry.ee';
import redisConfig from '../config/redis';
import logger from '../helpers/logger';
import mailer from '../helpers/mailer.ee';
@@ -30,6 +32,12 @@ worker.on('failed', (job, err) => {
logger.info(
`JOB ID: ${job.id} - ${job.data.subject} email to ${job.data.email} has failed to send with ${err.message}`
);
Sentry.captureException(err, {
extra: {
jobId: job.id,
}
});
});
process.on('SIGTERM', async () => {

View File

@@ -1,4 +1,6 @@
import { Worker } from 'bullmq';
import * as Sentry from '../helpers/sentry.ee';
import redisConfig from '../config/redis';
import logger from '../helpers/logger';
import triggerQueue from '../queues/trigger';
@@ -65,6 +67,12 @@ worker.on('failed', (job, err) => {
logger.info(
`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}`
);
Sentry.captureException(err, {
extra: {
jobId: job.id,
}
});
});
process.on('SIGTERM', async () => {

View File

@@ -1,7 +1,9 @@
import { Worker } from 'bullmq';
import { IJSONObject, ITriggerItem } from '@automatisch/types';
import * as Sentry from '../helpers/sentry.ee';
import redisConfig from '../config/redis';
import logger from '../helpers/logger';
import { IJSONObject, ITriggerItem } from '@automatisch/types';
import actionQueue from '../queues/action';
import Step from '../models/step';
import { processTrigger } from '../services/trigger';
@@ -51,6 +53,12 @@ worker.on('failed', (job, err) => {
logger.info(
`JOB ID: ${job.id} - FLOW ID: ${job.data.flowId} has failed to start with ${err.message}`
);
Sentry.captureException(err, {
extra: {
jobId: job.id,
}
});
});
process.on('SIGTERM', async () => {

View File

@@ -333,5 +333,6 @@ declare module 'axios' {
export interface IRequest extends Request {
rawBody?: Buffer;
currentUser?: IUser;
}