diff --git a/packages/host/app/lib/current-run.ts b/packages/host/app/lib/current-run.ts index 5aa9d34745..5d2730401a 100644 --- a/packages/host/app/lib/current-run.ts +++ b/packages/host/app/lib/current-run.ts @@ -36,6 +36,7 @@ import { type SerializedError, } from '@cardstack/runtime-common/error'; import { RealmPaths, LocalPath } from '@cardstack/runtime-common/paths'; +import { reportError } from '@cardstack/runtime-common/realm'; import { isIgnored, type Reader, @@ -475,9 +476,9 @@ export class CurrentRun { deferred.reject(err); throw err; } - log.warn( - `encountered error indexing card instance ${path}: ${error.error.detail}`, - ); + let warning = `encountered error indexing card instance ${path}: ${error.error.detail}`; + log.warn(warning); + reportError(new Error(warning)); this.setInstance(instanceURL, error); deferred.fulfill(); } diff --git a/packages/realm-server/fastboot.ts b/packages/realm-server/fastboot.ts index f223d43657..3cb0f17dee 100644 --- a/packages/realm-server/fastboot.ts +++ b/packages/realm-server/fastboot.ts @@ -7,6 +7,7 @@ import { type RunnerOpts, } from '@cardstack/runtime-common/search-index'; import { JSDOM } from 'jsdom'; +import { type ErrorReporter } from '@cardstack/runtime-common/realm'; const appName = '@cardstack/host'; export async function makeFastBootIndexRunner( @@ -15,6 +16,11 @@ export async function makeFastBootIndexRunner( ): Promise<{ getRunner: IndexRunner; distPath: string }> { let fastboot: FastBootInstance; let distPath: string; + + let globalWithErrorReporter = global as typeof globalThis & { + __boxelErrorReporter: ErrorReporter; + }; + if (typeof dist === 'string') { distPath = dist; fastboot = new FastBoot({ @@ -22,6 +28,7 @@ export async function makeFastBootIndexRunner( resilient: false, buildSandboxGlobals(defaultGlobals: any) { return Object.assign({}, defaultGlobals, { + __boxelErrorReporter: globalWithErrorReporter.__boxelErrorReporter, URL: globalThis.URL, Request: globalThis.Request, Response: globalThis.Response, @@ -38,6 +45,7 @@ export async function makeFastBootIndexRunner( dist, (defaultGlobals: any) => { return Object.assign({}, defaultGlobals, { + __boxelErrorReporter: globalWithErrorReporter.__boxelErrorReporter, URL: globalThis.URL, Request: globalThis.Request, Response: globalThis.Response, diff --git a/packages/realm-server/main.ts b/packages/realm-server/main.ts index bc15cfd0af..7e22e81d23 100644 --- a/packages/realm-server/main.ts +++ b/packages/realm-server/main.ts @@ -9,9 +9,27 @@ import { RunnerOptionsManager } from '@cardstack/runtime-common/search-index'; import { readFileSync } from 'fs-extra'; import { shimExternals } from './lib/externals'; import { type RealmPermissions as RealmPermissionsInterface } from '@cardstack/runtime-common/realm'; +import * as Sentry from '@sentry/node'; +import { setErrorReporter } from '@cardstack/runtime-common/realm'; import fs from 'fs'; +let log = logger('main'); + +if (process.env.REALM_SENTRY_DSN) { + log.info('Setting up Sentry.'); + Sentry.init({ + dsn: process.env.REALM_SENTRY_DSN, + environment: process.env.REALM_SENTRY_ENVIRONMENT || 'development', + }); + + setErrorReporter(Sentry.captureException); +} else { + log.warn( + `No REALM_SENTRY_DSN environment variable found, skipping Sentry setup.`, + ); +} + const REALM_SECRET_SEED = process.env.REALM_SECRET_SEED; if (!REALM_SECRET_SEED) { console.error( @@ -111,7 +129,6 @@ if ( process.exit(-1); } -let log = logger('main'); let virtualNetwork = new VirtualNetwork(); let loader = virtualNetwork.createLoader(); shimExternals(virtualNetwork); diff --git a/packages/realm-server/package.json b/packages/realm-server/package.json index 25e8815900..fcefe62fda 100644 --- a/packages/realm-server/package.json +++ b/packages/realm-server/package.json @@ -7,6 +7,7 @@ "@cardstack/runtime-common": "workspace:*", "@koa/cors": "^4.0.0", "@koa/router": "^12.0.0", + "@sentry/node": "^7.105.0", "@types/basic-auth": "^1.1.3", "@types/eventsource": "^1.1.11", "@types/flat": "^5.0.5", diff --git a/packages/realm-server/server.ts b/packages/realm-server/server.ts index dae271574d..76b1ba5e25 100644 --- a/packages/realm-server/server.ts +++ b/packages/realm-server/server.ts @@ -27,6 +27,7 @@ import './lib/externals'; import { nodeStreamToText } from './stream'; import mime from 'mime-types'; import { extractSupportedMimeType } from '@cardstack/runtime-common/router'; +import * as Sentry from '@sentry/node'; interface Options { assetsURL?: URL; @@ -96,6 +97,13 @@ export class RealmServer { .use(router.routes()) .use(this.serveFromRealm); + app.on('error', (err, ctx) => { + Sentry.withScope((scope) => { + scope.setSDKProcessingMetadata({ request: ctx.request }); + Sentry.captureException(err); + }); + }); + return app; } @@ -119,6 +127,10 @@ export class RealmServer { } private serveFromRealm = async (ctxt: Koa.Context, _next: Koa.Next) => { + if (ctxt.request.path === '/_boom') { + throw new Error('boom'); + } + let realm = this.realms.find((r) => { let reversedResolution = r.loader.reverseResolution( fullRequestURL(ctxt).href, diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index f1970cee35..d85f3f33d4 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -1676,6 +1676,22 @@ function lastModifiedHeader( ) as {} | { 'last-modified': string }; } +export type ErrorReporter = (error: Error) => void; + +let globalWithErrorReporter = global as typeof globalThis & { + __boxelErrorReporter: ErrorReporter; +}; + +export function setErrorReporter(reporter: ErrorReporter) { + globalWithErrorReporter.__boxelErrorReporter = reporter; +} + +export function reportError(error: Error) { + if (globalWithErrorReporter.__boxelErrorReporter) { + globalWithErrorReporter.__boxelErrorReporter(error); + } +} + export interface CardDefinitionResource { id: string; type: 'card-definition'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2f8030850..44f5cb46c0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1407,6 +1407,9 @@ importers: '@koa/router': specifier: ^12.0.0 version: 12.0.0 + '@sentry/node': + specifier: ^7.105.0 + version: 7.105.0 '@types/basic-auth': specifier: ^1.1.3 version: 1.1.3 @@ -4920,7 +4923,6 @@ packages: '@sentry/core': 7.105.0 '@sentry/types': 7.105.0 '@sentry/utils': 7.105.0 - dev: false /@sentry/core@7.105.0: resolution: {integrity: sha512-5xsaTG6jZincTeJUmZomlv20mVRZUEF1U/g89lmrSOybyk2+opEnB1JeBn4ODwnvmSik8r2QLr6/RiYlaxRJCg==} @@ -4928,7 +4930,6 @@ packages: dependencies: '@sentry/types': 7.105.0 '@sentry/utils': 7.105.0 - dev: false /@sentry/node@7.105.0: resolution: {integrity: sha512-b0QwZ7vT4hcJi6LmNRh3dcaYpLtXnkYXkL0rfhMb8hN8sUx8zuOWFMI7j0cfAloVThUeJVwGyv9dERfzGS2r2w==} @@ -4938,19 +4939,16 @@ packages: '@sentry/core': 7.105.0 '@sentry/types': 7.105.0 '@sentry/utils': 7.105.0 - dev: false /@sentry/types@7.105.0: resolution: {integrity: sha512-80o0KMVM+X2Ym9hoQxvJetkJJwkpCg7o6tHHFXI+Rp7fawc2iCMTa0IRQMUiSkFvntQLYIdDoNNuKdzz2PbQGA==} engines: {node: '>=8'} - dev: false /@sentry/utils@7.105.0: resolution: {integrity: sha512-YVAV0c2KLM8+VZCicQ/E/P2+J9Vs0hGhrXwV7w6ZEAtvxrg4oF270toL1WRhvcaf8JO4J1v4V+LuU6Txs4uEeQ==} engines: {node: '>=8'} dependencies: '@sentry/types': 7.105.0 - dev: false /@sideway/address@4.1.4: resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}