feat: introduce Sentry
This commit is contained in:
@@ -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",
|
||||
|
@@ -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;
|
||||
|
@@ -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) {
|
||||
|
@@ -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);
|
||||
}
|
||||
};
|
||||
|
@@ -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>;
|
||||
|
9
packages/backend/src/errors/quote-exceeded.ts
Normal file
9
packages/backend/src/errors/quote-exceeded.ts
Normal 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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
},
|
||||
});
|
||||
|
51
packages/backend/src/helpers/sentry.ee.ts
Normal file
51
packages/backend/src/helpers/sentry.ee.ts
Normal 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);
|
||||
}
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,3 +1,7 @@
|
||||
import * as Sentry from './helpers/sentry.ee';
|
||||
|
||||
Sentry.init();
|
||||
|
||||
import './config/orm';
|
||||
import './helpers/check-worker-readiness';
|
||||
import './workers/flow';
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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 () => {
|
||||
|
@@ -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 () => {
|
||||
|
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -333,5 +333,6 @@ declare module 'axios' {
|
||||
|
||||
export interface IRequest extends Request {
|
||||
rawBody?: Buffer;
|
||||
currentUser?: IUser;
|
||||
}
|
||||
|
||||
|
54
yarn.lock
54
yarn.lock
@@ -3415,6 +3415,51 @@
|
||||
component-type "^1.2.1"
|
||||
join-component "^1.1.0"
|
||||
|
||||
"@sentry/core@7.42.0":
|
||||
version "7.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.42.0.tgz#3333a1b868e8e69b6fbc8e5a9e9281be49321ac7"
|
||||
integrity sha512-vNcTyoQz5kUXo5vMGDyc5BJMO0UugPvMfYMQVxqt/BuDNR30LVhY+DL2tW1DFZDvRvyn5At+H7kSTj6GFrANXQ==
|
||||
dependencies:
|
||||
"@sentry/types" "7.42.0"
|
||||
"@sentry/utils" "7.42.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/node@^7.42.0":
|
||||
version "7.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.42.0.tgz#62b31f5b5b8ffb8f2f917deb143e27935b357409"
|
||||
integrity sha512-mmpVSDeoM5aEbKOMq3Wt54wAvH53bkivhRh3Ip+R7Uj3aOKkcVJST2XlbghHgoYQXTWz+pl475EVyODWgY9QYg==
|
||||
dependencies:
|
||||
"@sentry/core" "7.42.0"
|
||||
"@sentry/types" "7.42.0"
|
||||
"@sentry/utils" "7.42.0"
|
||||
cookie "^0.4.1"
|
||||
https-proxy-agent "^5.0.0"
|
||||
lru_map "^0.3.3"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/tracing@^7.42.0":
|
||||
version "7.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.42.0.tgz#bcdac21e1cb5f786465e6252625bd4bf0736e631"
|
||||
integrity sha512-0veGu3Ntweuj1pwWrJIFHmVdow4yufCreGIhsNDyrclwOjaTY3uI8iA6N62+hhtxOvqv+xueB98K1DvT5liPCQ==
|
||||
dependencies:
|
||||
"@sentry/core" "7.42.0"
|
||||
"@sentry/types" "7.42.0"
|
||||
"@sentry/utils" "7.42.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sentry/types@7.42.0":
|
||||
version "7.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.42.0.tgz#e5019cd41a8c4a98c296e2ff28d6adab4b2eb14e"
|
||||
integrity sha512-Ga0xaBIR/peuXQ88hI9a5TNY3GLNoH8jpsgPaAjAtRHkLsTx0y3AR+PrD7pUysza9QjvG+Qux01DRvLgaNKOHA==
|
||||
|
||||
"@sentry/utils@7.42.0":
|
||||
version "7.42.0"
|
||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.42.0.tgz#fcffd0404836cb56975fbef9e889a42cc55de596"
|
||||
integrity sha512-cBiDZVipC+is+IVgsTQLJyZWUZQxlLZ9GarNT+XZOZ5BFh0acFtz88hO6+S7vGmhcx2aCvsdC9yb2Yf+BphK6Q==
|
||||
dependencies:
|
||||
"@sentry/types" "7.42.0"
|
||||
tslib "^1.9.3"
|
||||
|
||||
"@sindresorhus/is@^0.14.0":
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
|
||||
@@ -6778,7 +6823,7 @@ cookie-signature@1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
|
||||
|
||||
cookie@0.4.2:
|
||||
cookie@0.4.2, cookie@^0.4.1:
|
||||
version "0.4.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
|
||||
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
|
||||
@@ -11703,6 +11748,11 @@ lru-cache@^7.3.1:
|
||||
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.4.0.tgz#2830a779b483e9723e20f26fa5278463c50599d8"
|
||||
integrity sha512-YOfuyWa/Ee+PXbDm40j9WXyJrzQUynVbgn4Km643UYcWNcrSfRkKL0WaiUcxcIbkXcVTgNpDqSnPXntWXT75cw==
|
||||
|
||||
lru_map@^0.3.3:
|
||||
version "0.3.3"
|
||||
resolved "https://registry.yarnpkg.com/lru_map/-/lru_map-0.3.3.tgz#b5c8351b9464cbd750335a79650a0ec0e56118dd"
|
||||
integrity sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==
|
||||
|
||||
luxon@2.5.2, luxon@^2.3.1:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-2.5.2.tgz#17ed497f0277e72d58a4756d6a9abee4681457b6"
|
||||
@@ -16799,7 +16849,7 @@ tsconfig@^7.0.0:
|
||||
strip-bom "^3.0.0"
|
||||
strip-json-comments "^2.0.0"
|
||||
|
||||
tslib@^1.8.1, tslib@^1.9.0:
|
||||
tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3:
|
||||
version "1.14.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
Reference in New Issue
Block a user