feat: initial server impl

This commit is contained in:
Wyatt Johnson
2020-08-14 13:51:00 -06:00
parent e85947af74
commit 4a94b81230
16 changed files with 422 additions and 86 deletions
+123
View File
@@ -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",
+1
View File
@@ -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 = ({
+10 -10
View File
@@ -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;
+102 -17
View File
@@ -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);
};
+6 -16
View File
@@ -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);
};
+1 -3
View File
@@ -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;
}
+35 -22
View File
@@ -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>
+10
View File
@@ -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,
+4
View File
@@ -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
View File
@@ -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.
+2
View File
@@ -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;
}
+46
View File
@@ -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,
};
}
}