mirror of
https://github.com/wassname/talk.git
synced 2026-06-27 20:23:30 +08:00
feat: initial server impl
This commit is contained in:
Generated
+123
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -27,6 +27,7 @@ export type GraphMiddlewareOptions = Pick<
|
||||
| "tenantCache"
|
||||
| "metrics"
|
||||
| "broker"
|
||||
| "reporter"
|
||||
>;
|
||||
|
||||
export const graphQLHandler = ({
|
||||
|
||||
@@ -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<Express> {
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -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<Pick<AppOptions, "i18n" | "reporter">>;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,37 @@
|
||||
<!doctype html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Error</title>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||
<style type="text/css">
|
||||
body, html { margin: 0; padding: 0; }
|
||||
dl { max-width: 800px; margin: 20px auto; font-size: 23px; }
|
||||
dh { font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<dl>
|
||||
<dh>Message</dh>
|
||||
<dd>{{ error.message }}</dd>
|
||||
<dh>Code</dh>
|
||||
<dd>{{ error.code }}</dd>
|
||||
<dh>ID</dh>
|
||||
<dd>{{ error.id }}</dd>
|
||||
</dl>
|
||||
</body>
|
||||
<head>
|
||||
<title>Error</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<style type="text/css">
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
dl {
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
font-size: 23px;
|
||||
}
|
||||
dh {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<dl>
|
||||
<dh>Message</dh>
|
||||
<dd>{{ error.message }}</dd>
|
||||
<dh>Code</dh>
|
||||
<dd>{{ error.code }}</dd>
|
||||
<dh>ID</dh>
|
||||
<dd>{{ error.id }}</dd>
|
||||
{% if reporter %}
|
||||
<dh>{{ reporter.name }}</dh>
|
||||
<dd>{{ reporter.id }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof loaders>;
|
||||
@@ -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);
|
||||
|
||||
@@ -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<GraphContext> {
|
||||
}): void {
|
||||
if (response.graphqlResponse.errors) {
|
||||
response.graphqlResponse.errors.forEach((err) =>
|
||||
logError(response.context, err)
|
||||
logAndReportError(response.context, err)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}),
|
||||
|
||||
+25
-14
@@ -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.
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "./reporter";
|
||||
export * from "./sentry";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import Sentry, { User } from "@sentry/node";
|
||||
|
||||
import { ErrorReport, ErrorReporter, ErrorReporterScope } from "./reporter";
|
||||
|
||||
interface Context {
|
||||
user: User;
|
||||
tags: Record<string, string>;
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user