From 4a94b81230cdddb2b6cd2fd4b0789d428201694e Mon Sep 17 00:00:00 2001 From: Wyatt Johnson Date: Fri, 14 Aug 2020 13:51:00 -0600 Subject: [PATCH] feat: initial server impl --- package-lock.json | 123 ++++++++++++++++++ package.json | 1 + src/core/server/app/handlers/api/graphql.ts | 1 + src/core/server/app/index.ts | 20 +-- src/core/server/app/middleware/error.ts | 119 ++++++++++++++--- src/core/server/app/middleware/logging.ts | 22 +--- src/core/server/app/router/api/index.ts | 4 +- src/core/server/app/views/error.html | 57 ++++---- src/core/server/config.ts | 10 ++ src/core/server/graph/context.ts | 4 + .../graph/extensions/LoggerExtension.ts | 34 ++++- src/core/server/graph/subscriptions/server.ts | 8 +- src/core/server/index.ts | 39 ++++-- src/core/server/services/errors/index.ts | 2 + src/core/server/services/errors/reporter.ts | 18 +++ src/core/server/services/errors/sentry.ts | 46 +++++++ 16 files changed, 422 insertions(+), 86 deletions(-) create mode 100644 src/core/server/services/errors/index.ts create mode 100644 src/core/server/services/errors/reporter.ts create mode 100644 src/core/server/services/errors/sentry.ts diff --git a/package-lock.json b/package-lock.json index 4e3b57ffc..5c5d7cba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7743,6 +7743,124 @@ "join-component": "^1.1.0" } }, + "@sentry/apm": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/apm/-/apm-5.21.1.tgz", + "integrity": "sha512-mxMOCpeXULbQCC/f9SwPqW+g12mk3nWRNjeAUm5dyiKHY13agtQBSSYs4ROEH190YxmwTZr3vxhlR2jNSdSZcg==", + "requires": { + "@sentry/browser": "5.21.1", + "@sentry/hub": "5.21.1", + "@sentry/minimal": "5.21.1", + "@sentry/types": "5.21.1", + "@sentry/utils": "5.21.1", + "tslib": "^1.9.3" + } + }, + "@sentry/browser": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.21.1.tgz", + "integrity": "sha512-sUxsW545klZxJE4iBAYQ8SuVS85HTOGNmIIIZWFUogB5oW3O0L+nJluXEqf/pHU82LnjDIzqsWCYQ0cRUaeYow==", + "requires": { + "@sentry/core": "5.21.1", + "@sentry/types": "5.21.1", + "@sentry/utils": "5.21.1", + "tslib": "^1.9.3" + } + }, + "@sentry/core": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.21.1.tgz", + "integrity": "sha512-Luulwx3GLUiY0gmHOhU+4eSga28Ce8DwoBcRq9GkGuhPu9r80057d5urxrDLp/leIZBXVvpY7tvmSN/rMtvF9w==", + "requires": { + "@sentry/hub": "5.21.1", + "@sentry/minimal": "5.21.1", + "@sentry/types": "5.21.1", + "@sentry/utils": "5.21.1", + "tslib": "^1.9.3" + } + }, + "@sentry/hub": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.21.1.tgz", + "integrity": "sha512-x5i9Ggi5ZYMhBYL5kyTu2fUJ6owjKH2tgJL3UExoZdRyZkbLAFZb+DtfSnteWgQ6wriGfgPD3r/hAIEdaomk2A==", + "requires": { + "@sentry/types": "5.21.1", + "@sentry/utils": "5.21.1", + "tslib": "^1.9.3" + } + }, + "@sentry/minimal": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.21.1.tgz", + "integrity": "sha512-OBVPASZ+mcXMKajvJon9RjEZ+ny3+VGhOI66acoP1hmYxKvji1OC2bYEuP1r4qtHxWVLAdV7qFj3EQ9ckErZmQ==", + "requires": { + "@sentry/hub": "5.21.1", + "@sentry/types": "5.21.1", + "tslib": "^1.9.3" + } + }, + "@sentry/node": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-5.21.1.tgz", + "integrity": "sha512-+QLqGz6+/gtShv0F16nI2+AuVEDZG2k9L25BVCNoysYzH1J1/QIKHsl7YF2trDMlWM4T7cbu5Fh8AhK6an+5/g==", + "requires": { + "@sentry/apm": "5.21.1", + "@sentry/core": "5.21.1", + "@sentry/hub": "5.21.1", + "@sentry/types": "5.21.1", + "@sentry/utils": "5.21.1", + "cookie": "^0.4.1", + "https-proxy-agent": "^5.0.0", + "lru_map": "^0.3.3", + "tslib": "^1.9.3" + }, + "dependencies": { + "agent-base": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz", + "integrity": "sha512-01q25QQDwLSsyfhrKbn8yuur+JNw0H+0Y4JiGIKd3z9aYk/w/2kxD/Upc+t2ZBBSUNff50VjPsSW2YxM8QYKVg==", + "requires": { + "debug": "4" + } + }, + "cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + }, + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + } + } + }, + "@sentry/types": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.21.1.tgz", + "integrity": "sha512-hFN4aDduMpjj6vZSIIp+9kSr8MglcKO/UmbuUXN6hKLewhxt+Zj2wjXN7ulSs5OK5mjXP9QLA5YJvVQsl2//qw==" + }, + "@sentry/utils": { + "version": "5.21.1", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.21.1.tgz", + "integrity": "sha512-p5vPuc7+GfOmW8CXxWd0samS77Q00YrN8q5TC/ztF8nBhEF18GiMeWAdQnlSwt3iWal3q3gSSrbF4c9guIugng==", + "requires": { + "@sentry/types": "5.21.1", + "tslib": "^1.9.3" + } + }, "@sindresorhus/is": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", @@ -37889,6 +38007,11 @@ "es5-ext": "~0.10.2" } }, + "lru_map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/lru_map/-/lru_map-0.3.3.tgz", + "integrity": "sha1-tcg1G5Rky9dQM1p5ZQoOwOVhGN0=" + }, "luxon": { "version": "1.22.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.22.2.tgz", diff --git a/package.json b/package.json index fe4f93db5..e2ce9c64f 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@hapi/joi": "^17.1.1", "@metascraper/helpers": "^5.11.6", "@rudderstack/rudder-sdk-node": "0.0.2", + "@sentry/node": "^5.21.1", "abort-controller": "^3.0.0", "akismet-api": "^5.0.0", "apollo-server-core": "^2.14.4", diff --git a/src/core/server/app/handlers/api/graphql.ts b/src/core/server/app/handlers/api/graphql.ts index 48b45e1b1..4294b0eb0 100644 --- a/src/core/server/app/handlers/api/graphql.ts +++ b/src/core/server/app/handlers/api/graphql.ts @@ -27,6 +27,7 @@ export type GraphMiddlewareOptions = Pick< | "tenantCache" | "metrics" | "broker" + | "reporter" >; export const graphQLHandler = ({ diff --git a/src/core/server/app/index.ts b/src/core/server/app/index.ts index f856ca5df..5d12bdf63 100644 --- a/src/core/server/app/index.ts +++ b/src/core/server/app/index.ts @@ -29,6 +29,7 @@ import { NotifierQueue } from "coral-server/queue/tasks/notifier"; import { RejectorQueue } from "coral-server/queue/tasks/rejector"; import { ScraperQueue } from "coral-server/queue/tasks/scraper"; import { WebhookQueue } from "coral-server/queue/tasks/webhook"; +import { ErrorReporter } from "coral-server/services/errors"; import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; import { Metrics } from "coral-server/services/metrics"; @@ -40,32 +41,33 @@ import { TenantCache } from "coral-server/services/tenant/cache"; import { healthHandler, versionHandler } from "./handlers"; import { compileTrust } from "./helpers"; import { basicAuth } from "./middleware/basicAuth"; -import { accessLogger, errorLogger } from "./middleware/logging"; +import { accessLogger } from "./middleware/logging"; import { metricsRecorder } from "./middleware/metrics"; import serveStatic from "./middleware/serveStatic"; import { createRouter } from "./router"; export interface AppOptions { + broker: CoralEventListenerBroker; config: Config; disableClientRoutes: boolean; i18n: I18n; mailerQueue: MailerQueue; - scraperQueue: ScraperQueue; - rejectorQueue: RejectorQueue; - webhookQueue: WebhookQueue; - notifierQueue: NotifierQueue; metrics: Metrics; + migrationManager: MigrationManager; mongo: Db; + notifierQueue: NotifierQueue; parent: Express; persistedQueriesRequired: boolean; persistedQueryCache: PersistedQueryCache; pubsub: RedisPubSub; redis: AugmentedRedis; + rejectorQueue: RejectorQueue; + reporter?: ErrorReporter; schema: GraphQLSchema; + scraperQueue: ScraperQueue; signingConfig: JWTSigningConfig; tenantCache: TenantCache; - migrationManager: MigrationManager; - broker: CoralEventListenerBroker; + webhookQueue: WebhookQueue; } /** @@ -113,8 +115,7 @@ export async function createApp(options: AppOptions): Promise { // Error Handling parent.use(notFoundMiddleware); - parent.use(errorLogger); - parent.use(HTMLErrorHandler(options.i18n)); + parent.use(HTMLErrorHandler(options)); return parent; } @@ -235,7 +236,6 @@ export default function createMetricsServer(config: Config) { // Error handling. server.use(notFoundMiddleware); - server.use(errorLogger); server.use(JSONErrorHandler()); return server; diff --git a/src/core/server/app/middleware/error.ts b/src/core/server/app/middleware/error.ts index 7b5b1047a..5fa72b086 100644 --- a/src/core/server/app/middleware/error.ts +++ b/src/core/server/app/middleware/error.ts @@ -1,9 +1,19 @@ import { FluentBundle } from "@fluent/bundle/compat"; +import { Response } from "express"; -import { CoralError, WrappedInternalError } from "coral-server/errors"; +import { + CoralError, + CoralErrorExtensions, + WrappedInternalError, +} from "coral-server/errors"; +import logger from "coral-server/logger"; +import { ErrorReport, ErrorReporterScope } from "coral-server/services/errors"; import { I18n } from "coral-server/services/i18n"; import { ErrorRequestHandler, Request } from "coral-server/types/express"; +import { AppOptions } from "../"; +import { extractLoggerMetadata } from "./logging"; + /** * wrapError ensures that the error being propagated is a CoralError. * @@ -37,28 +47,103 @@ const serializeError = (err: CoralError, req: Request, bundles?: I18n) => { }; }; -export const JSONErrorHandler = (bundles?: I18n): ErrorRequestHandler => ( - err, - req, - res, - next -) => { +type ErrorHandlerOptions = Partial>; + +interface ErrorHandlerResponse { + status: number; + context: { + error: CoralErrorExtensions; + report: ErrorReport | null; + }; +} + +function wrapAndReport( + err: Error, + req: Request, + res: Response, + { i18n, reporter }: ErrorHandlerOptions +): ErrorHandlerResponse { + // Grab the logger. + const log = req.coral ? req.coral.logger : logger; + // Wrap the error if it needs to be wrapped. const e = wrapError(err); + // Serialize the error. + const { error } = serializeError(e, req, i18n); + + // If there's no reporter active, then return now. + if (!reporter) { + // Log the error. + log.error( + { ...extractLoggerMetadata(req, res), err, statusCode: e.status }, + "http error" + ); + + return { + status: e.status, + context: { + error, + report: null, + }, + }; + } + + // Collect the error scope for the reporter. + const scope: ErrorReporterScope = { + ipAddress: req.ip, + }; + + // Add Tenant details to the scope. + if (req.coral.tenant) { + scope.tenantID = req.coral.tenant.id; + scope.tenantDomain = req.coral.tenant.domain; + + // Can't have a User if we don't have a Tenant.. So check now that we have a + // User and add it to the scope. + if (req.user) { + scope.userID = req.user.id; + scope.userRole = req.user.role; + } + } + + // Report the error. + const report = reporter.report(e, scope); + + // Log the error. + log.error( + { + ...extractLoggerMetadata(req, res), + err, + statusCode: e.status, + report, + }, + "http error" + ); + + return { + status: e.status, + context: { + error, + report, + }, + }; +} + +export const JSONErrorHandler = ( + options: ErrorHandlerOptions = {} +): ErrorRequestHandler => (err, req, res, next) => { + const { status, context } = wrapAndReport(err, req, res, options); + // Send the response via JSON. - res.status(e.status).json(serializeError(e, req, bundles)); + res.status(status).json(context); }; -export const HTMLErrorHandler = (bundles?: I18n): ErrorRequestHandler => ( - err, - req, - res, - next -) => { - // Wrap the error if it needs to be wrapped. - const e = wrapError(err); +export const HTMLErrorHandler = ( + options: ErrorHandlerOptions = {} +): ErrorRequestHandler => (err, req, res, next) => { + const { status, context } = wrapAndReport(err, req, res, options); // Send the response via HTML. - res.status(e.status).render("error", serializeError(e, req, bundles)); + res.status(status).render("error", context); }; diff --git a/src/core/server/app/middleware/logging.ts b/src/core/server/app/middleware/logging.ts index 354a8e119..e72ec2d31 100644 --- a/src/core/server/app/middleware/logging.ts +++ b/src/core/server/app/middleware/logging.ts @@ -3,12 +3,9 @@ import onFinished from "on-finished"; import { createTimer } from "coral-server/helpers"; import logger from "coral-server/logger"; -import { - ErrorRequestHandler, - RequestHandler, -} from "coral-server/types/express"; +import { RequestHandler } from "coral-server/types/express"; -const extractMetadata = (req: Request, res: Response) => ({ +export const extractLoggerMetadata = (req: Request, res: Response) => ({ url: req.originalUrl || req.url, method: req.method, statusCode: res.statusCode, @@ -27,18 +24,11 @@ export const accessLogger: RequestHandler = (req, res, next) => { const log = req.coral ? req.coral.logger : logger; // Log this out. - log.debug({ ...extractMetadata(req, res), responseTime }, "http request"); + log.debug( + { ...extractLoggerMetadata(req, res), responseTime }, + "http request" + ); }); next(); }; - -export const errorLogger: ErrorRequestHandler = (err, req, res, next) => { - // Grab the logger. - const log = req.coral ? req.coral.logger : logger; - - // Log this out. - log.error({ ...extractMetadata(req, res), err }, "http error"); - - next(err); -}; diff --git a/src/core/server/app/router/api/index.ts b/src/core/server/app/router/api/index.ts index 09b8bf3ab..6acab91e7 100644 --- a/src/core/server/app/router/api/index.ts +++ b/src/core/server/app/router/api/index.ts @@ -9,7 +9,6 @@ import { JSONErrorHandler } from "coral-server/app/middleware/error"; import { persistedQueryMiddleware } from "coral-server/app/middleware/graphql"; import { jsonMiddleware } from "coral-server/app/middleware/json"; import { loggedInMiddleware } from "coral-server/app/middleware/loggedIn"; -import { errorLogger } from "coral-server/app/middleware/logging"; import { notFoundMiddleware } from "coral-server/app/middleware/notFound"; import { authenticate } from "coral-server/app/middleware/passport"; import { roleMiddleware } from "coral-server/app/middleware/role"; @@ -81,8 +80,7 @@ export function createAPIRouter(app: AppOptions, options: RouterOptions) { // General API error handler. router.use(notFoundMiddleware); - router.use(errorLogger); - router.use(JSONErrorHandler(app.i18n)); + router.use(JSONErrorHandler(app)); return router; } diff --git a/src/core/server/app/views/error.html b/src/core/server/app/views/error.html index b53ca8a77..1acd59d9b 100644 --- a/src/core/server/app/views/error.html +++ b/src/core/server/app/views/error.html @@ -1,24 +1,37 @@ - + - - Error - - - - - -
- Message -
{{ error.message }}
- Code -
{{ error.code }}
- ID -
{{ error.id }}
-
- + + Error + + + + + +
+ Message +
{{ error.message }}
+ Code +
{{ error.code }}
+ ID +
{{ error.id }}
+ {% if reporter %} + {{ reporter.name }} +
{{ reporter.id }}
+ {% endif %} +
+ - diff --git a/src/core/server/config.ts b/src/core/server/config.ts index 4fc5739ef..bbcc1443b 100644 --- a/src/core/server/config.ts +++ b/src/core/server/config.ts @@ -282,6 +282,16 @@ const config = convict({ default: ms("100ms"), env: "WORD_LIST_TIMEOUT", }, + sentry_frontend_key: { + format: String, + default: "", + env: "SENTRY_FRONTEND_KEY", + }, + sentry_backend_key: { + format: String, + default: "", + env: "SENTRY_BACKEND_KEY", + }, analytics_frontend_key: { doc: "Analytics write key from RudderStack for the Javascript client.", format: String, diff --git a/src/core/server/graph/context.ts b/src/core/server/graph/context.ts index 8145b43e7..4f24af12a 100644 --- a/src/core/server/graph/context.ts +++ b/src/core/server/graph/context.ts @@ -16,6 +16,7 @@ import { NotifierQueue } from "coral-server/queue/tasks/notifier"; import { RejectorQueue } from "coral-server/queue/tasks/rejector"; import { ScraperQueue } from "coral-server/queue/tasks/scraper"; import { WebhookQueue } from "coral-server/queue/tasks/webhook"; +import { ErrorReporter } from "coral-server/services/errors"; import { I18n } from "coral-server/services/i18n"; import { JWTSigningConfig } from "coral-server/services/jwt"; import { AugmentedRedis } from "coral-server/services/redis"; @@ -33,6 +34,7 @@ export interface GraphContextOptions { logger?: Logger; now?: Date; persisted?: PersistedQuery; + reporter?: ErrorReporter; req?: Request; signingConfig?: JWTSigningConfig; user?: User; @@ -57,6 +59,7 @@ export default class GraphContext { public readonly broker: CoralEventPublisherBroker; public readonly disableCaching: boolean; public readonly i18n: I18n; + public readonly reporter?: ErrorReporter; public readonly id: string; public readonly lang: LanguageCode; public readonly loaders: ReturnType; @@ -108,6 +111,7 @@ export default class GraphContext { this.webhookQueue = options.webhookQueue; this.signingConfig = options.signingConfig; this.clientID = options.clientID; + this.reporter = options.reporter; this.broker = options.broker.instance(this); this.loaders = loaders(this); diff --git a/src/core/server/graph/extensions/LoggerExtension.ts b/src/core/server/graph/extensions/LoggerExtension.ts index 098c4ea22..ae57fd8f2 100644 --- a/src/core/server/graph/extensions/LoggerExtension.ts +++ b/src/core/server/graph/extensions/LoggerExtension.ts @@ -8,11 +8,41 @@ import { import GraphContext from "coral-server/graph/context"; import { createTimer } from "coral-server/helpers"; import logger from "coral-server/logger"; +import { ErrorReporterScope } from "coral-server/services/errors"; import { getOperationMetadata, getPersistedQueryMetadata } from "./helpers"; -export function logError(ctx: GraphContext, err: GraphQLFormattedError) { +export function logAndReportError( + ctx: GraphContext, + err: GraphQLFormattedError +) { ctx.logger.error({ err }, "graphql query error"); + + // If there's no reporter active, then return now. + if (!ctx.reporter) { + return; + } + + // Collect the error scope for the reporter. + const scope: ErrorReporterScope = { + ipAddress: ctx.req?.ip, + }; + + // Add Tenant details to the scope. + scope.tenantID = ctx.tenant.id; + scope.tenantDomain = ctx.tenant.domain; + + // Add User details if there is any to the request. + if (ctx.user) { + scope.userID = ctx.user.id; + scope.userRole = ctx.user.role; + } + + // Report the error and get back the report ID. + const report = ctx.reporter.report(err, scope); + + // Log that we reported an error. + ctx.logger.error({ err, report }, "graphql query error"); } export function logQuery( @@ -103,7 +133,7 @@ export class LoggerExtension implements GraphQLExtension { }): void { if (response.graphqlResponse.errors) { response.graphqlResponse.errors.forEach((err) => - logError(response.context, err) + logAndReportError(response.context, err) ); } } diff --git a/src/core/server/graph/subscriptions/server.ts b/src/core/server/graph/subscriptions/server.ts index a2265a276..0ec58d13b 100644 --- a/src/core/server/graph/subscriptions/server.ts +++ b/src/core/server/graph/subscriptions/server.ts @@ -29,7 +29,11 @@ import { TenantNotFoundError, WrappedInternalError, } from "coral-server/errors"; -import { enrichError, logError, logQuery } from "coral-server/graph/extensions"; +import { + enrichError, + logAndReportError, + logQuery, +} from "coral-server/graph/extensions"; import { getOperationMetadata } from "coral-server/graph/extensions/helpers"; import { getPersistedQuery } from "coral-server/graph/persisted"; import logger from "coral-server/logger"; @@ -199,7 +203,7 @@ export function formatResponse( const enriched = enrichError(context, err); // Log the error out. - logError(context, enriched); + logAndReportError(context, enriched); return enriched; }), diff --git a/src/core/server/index.ts b/src/core/server/index.ts index a69e4fd3f..7e6b2f29e 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -44,6 +44,7 @@ import { WebhookCoralEventListener, } from "./events/listeners"; import CoralEventListenerBroker from "./events/publisher"; +import { ErrorReporter, SentryErrorReporter } from "./services/errors"; import { isInstalled } from "./services/tenant"; export interface ServerOptions { @@ -115,6 +116,8 @@ class Server { // migrationManager is the manager for performing migrations on Coral. private migrationManager: MigrationManager; + private readonly reporter?: ErrorReporter; + /** * broker stores a reference to all of the listeners that can be used in * conjunction with an event to publish activity occurring inside Coral. @@ -143,6 +146,13 @@ class Server { } } + // Configure the error reporter. + if (this.config.get("sentry_backend_key")) { + this.reporter = new SentryErrorReporter( + this.config.get("sentry_backend_key") + ); + } + // Load the graph schemas. this.schema = getTenantSchema(); @@ -302,28 +312,29 @@ class Server { const disableClientRoutes = this.config.get("disable_client_routes"); const options: AppOptions = { - parent, broker: this.broker, - pubsub: this.pubsub, - mongo: this.mongo, - redis: this.redis, - signingConfig: this.signingConfig, - tenantCache: this.tenantCache, config: this.config, - schema: this.schema, + disableClientRoutes, i18n: this.i18n, mailerQueue: this.tasks.mailer, - scraperQueue: this.tasks.scraper, - rejectorQueue: this.tasks.rejector, - webhookQueue: this.tasks.webhook, - notifierQueue: this.tasks.notifier, - disableClientRoutes, - persistedQueryCache: this.persistedQueryCache, metrics: createMetrics(), + migrationManager: this.migrationManager, + mongo: this.mongo, + notifierQueue: this.tasks.notifier, + parent, persistedQueriesRequired: this.config.get("env") === "production" && !this.config.get("enable_graphiql"), - migrationManager: this.migrationManager, + persistedQueryCache: this.persistedQueryCache, + pubsub: this.pubsub, + redis: this.redis, + rejectorQueue: this.tasks.rejector, + reporter: this.reporter, + schema: this.schema, + scraperQueue: this.tasks.scraper, + signingConfig: this.signingConfig, + tenantCache: this.tenantCache, + webhookQueue: this.tasks.webhook, }; // Create the Coral App, branching off from the parent app. diff --git a/src/core/server/services/errors/index.ts b/src/core/server/services/errors/index.ts new file mode 100644 index 000000000..970f7345c --- /dev/null +++ b/src/core/server/services/errors/index.ts @@ -0,0 +1,2 @@ +export * from "./reporter"; +export * from "./sentry"; diff --git a/src/core/server/services/errors/reporter.ts b/src/core/server/services/errors/reporter.ts new file mode 100644 index 000000000..f731fac5d --- /dev/null +++ b/src/core/server/services/errors/reporter.ts @@ -0,0 +1,18 @@ +import { GQLUSER_ROLE } from "coral-server/graph/schema/__generated__/types"; + +export interface ErrorReporterScope { + tenantID?: string; + tenantDomain?: string; + userID?: string; + userRole?: GQLUSER_ROLE; + ipAddress?: string; +} + +export interface ErrorReport { + name: string; + id: string; +} + +export abstract class ErrorReporter { + public abstract report(err: any, scope: ErrorReporterScope): ErrorReport; +} diff --git a/src/core/server/services/errors/sentry.ts b/src/core/server/services/errors/sentry.ts new file mode 100644 index 000000000..cbd767208 --- /dev/null +++ b/src/core/server/services/errors/sentry.ts @@ -0,0 +1,46 @@ +import Sentry, { User } from "@sentry/node"; + +import { ErrorReport, ErrorReporter, ErrorReporterScope } from "./reporter"; + +interface Context { + user: User; + tags: Record; +} + +export class SentryErrorReporter extends ErrorReporter { + public readonly name = "sentry"; + + constructor(dsn: string) { + // Setup the base error reporter. + super(); + + // Initialize sentry. + Sentry.init({ dsn }); + } + + public report(err: any, scope: ErrorReporterScope): ErrorReport { + // Transform the scope to a sentry scope. + const context: Context = { + user: { + id: scope.userID, + role: scope.userRole, + }, + tags: {}, + }; + + // Add the Tenant's ID and domain if they are provided. + if (scope.tenantID && scope.tenantDomain) { + context.tags.tenantID = scope.tenantID; + context.tags.tenantDomain = scope.tenantDomain; + } + + // Capture and report the error to Sentry. + const id = Sentry.captureException(err, context); + + // Return the error report. + return { + name: "sentry", + id, + }; + } +}