From 7413777a321289fb72fff7794167cd3e49bfbf59 Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Thu, 5 Nov 2026 13:51:22 +0000 Subject: [PATCH 1/2] test(attachments): pin the Sentry event for an attachment that could not be served --- test/attachments-endpoint.js | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test/attachments-endpoint.js b/test/attachments-endpoint.js index 1aaa66e..8aa805e 100644 --- a/test/attachments-endpoint.js +++ b/test/attachments-endpoint.js @@ -25,3 +25,39 @@ test('returns 502 for an attachment that is only partly synced', async (t) => { assert.equal(response.statusCode, 502) assert.equal(response.json().error.code, 'ATTACHMENT_UPSTREAM_UNAVAILABLE') }) + +test('reports an attachment that could not be served', async (t) => { + /** @type {Array<{ message: string, context: any }>} */ + const events = [] + const server = createTestServer(t, { + captureMessage(message, context) { + events.push({ message, context }) + }, + }) + await server.listen() + + const projectPublicId = await server.comapeo.createProject({ + name: 'project', + }) + const project = await server.comapeo.getProject(projectPublicId) + const blob = await createPartialPhotoBlob(t, project) + + await server.inject({ + method: 'GET', + url: `/projects/${projectPublicId}/attachments/${blob.driveId}/photo/${blob.name}`, + headers: { Authorization: 'Bearer ' + BEARER_TOKEN }, + }) + + assert.equal(events.length, 1) + const [event] = events + assert.equal(event?.message, 'attachment.nonok') + assert.equal(event?.context.level, 'warning') + assert.deepEqual(event?.context.tags, { + project: projectPublicId, + drive: blob.driveId.slice(0, 7), + type: 'photo', + variant: 'original', + class: 'incomplete', + http_status: 502, + }) +}) From 5c8f101e6a058d5a02e3b8f71fe21ce0db39c38d Mon Sep 17 00:00:00 2001 From: Gregor MacLennan Date: Thu, 5 Nov 2026 13:52:17 +0000 Subject: [PATCH 2/2] feat(attachments): report attachments that could not be served to Sentry --- README.md | 1 + package-lock.json | 271 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + src/app.js | 2 + src/routes.js | 63 ++++++++++- src/server.js | 35 ++++++ 6 files changed, 369 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 521e5c7..88f8296 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Server configuration is done using environment variables. The following environm | `SERVER_NAME` | No | Friendly server name, seen by users when adding server | `CoMapeo Server` | | `ALLOWED_PROJECTS` | No | Number of projects allowed to register with the server | `1` | | `STORAGE_DIR` | No | Path for storing app & project data | `$CWD/data` | +| `SENTRY_DSN` | No | Sentry DSN. Reports crashes and attachments that could not be served | | | `AUDIT_MAX_ATTACHMENTS` | No | Attachments checked per project by the attachment audit, newest first | `200` | If you are using Nginx to act as a reverse proxy for your CoMapeo Cloud server, ensure your proxy headers are configured to support WebSockets. diff --git a/package-lock.json b/package-lock.json index 20c9096..b8a541c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@fastify/websocket": "^10.0.1", "@mapeo/crypto": "^1.0.0-alpha.10", "@mapeo/default-config": "^6.0.0", + "@sentry/node": "^10.76.2", "@sinclair/typebox": "^0.33.17", "ajv": "^8.17.1", "env-schema": "^6.0.0", @@ -1765,6 +1766,120 @@ "node": ">= 10" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", + "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/api-logs": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", + "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api": "^1.3.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.12.0.tgz", + "integrity": "sha512-1HsSAuvT4/my0QrXWsyXFdWjKaPeCHsismKZgEEIb8NK13LP6DrMRlsUbvD/0F9mDWK3xDom9AMv3IscxgOnOQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.220.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz", + "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/api-logs": "0.220.0", + "import-in-the-middle": "^3.0.0", + "require-in-the-middle": "^8.0.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "node_modules/@opentelemetry/resources": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.12.0.tgz", + "integrity": "sha512-+rSRoOdln6NhXTVcBdHSlZxJDNzwxadeweK2ltZO3tVtfp6aklABc06xclDk6ovRR+EiGgIcSbh/6NgjFhnb3A==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.12.0.tgz", + "integrity": "sha512-ImVuRQ6faOsTi1Wna88s2avgrrf/iDgXPigIeynkUhjstMuoQUDOJXgkez2j1mlU4btjXcXBGa+q7qKFYR3GsA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/sdk-trace-base": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.12.0.tgz", + "integrity": "sha512-JOERgZCxRM06FEJ5kEWxlSRChah6MmXDx3KwCruqEv/iEyLkTokCSemf2y1w8oIHtfrr8EVTpzebq/31vn2Cpg==", + "deprecated": "Use @opentelemetry/sdk-trace package", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.12.0", + "@opentelemetry/resources": "2.12.0", + "@opentelemetry/sdk-trace": "2.12.0", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.44.0.tgz", + "integrity": "sha512-E3JkOlMBi3N8OGGkbl7tFyqk2MAS6/lySRZBK3Mu7AzjF1fT8oIChH9ChE48SUWzDn9Kdwta+G/p5LJ4xW4CLQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1838,6 +1953,117 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@sentry/conventions": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", + "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.76.2.tgz", + "integrity": "sha512-vndrQalRiwYatTkjlLWvyf69KocXIb9TKJ4A9oiyWguL8PJYtqIRTlRe4OpkpjLrxS18fvRdV0jEm29c/9p3hQ==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.76.2.tgz", + "integrity": "sha512-LSwGGE/GceTbkDHRoQQr3lq/dpxxA6RdooBbuGTJ/S6BdqKX2kw2FdV7Se9eJzZJJJ5AxCZum37+ejYS7N2pJg==", + "license": "MIT", + "dependencies": { + "@opentelemetry/api": "^1.9.1", + "@opentelemetry/instrumentation": "^0.220.0", + "@opentelemetry/sdk-trace-base": "^2.9.0", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2", + "@sentry/node-core": "10.76.2", + "@sentry/opentelemetry": "10.76.2", + "@sentry/server-utils": "10.76.2", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sentry/node-core": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.76.2.tgz", + "integrity": "sha512-TrgAc/SvU5kQfjw8pz6I8LLIUiblOSf9BPPNkTbS0Dzcqa4yQLVbmsbS3rvGT7ot64m9Vjzn327ZRy63MlI6/w==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2", + "@sentry/opentelemetry": "10.76.2", + "import-in-the-middle": "^3.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1", + "@opentelemetry/instrumentation": ">=0.57.1 <1", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@opentelemetry/core": { + "optional": true + }, + "@opentelemetry/exporter-trace-otlp-http": { + "optional": true + }, + "@opentelemetry/instrumentation": { + "optional": true + }, + "@opentelemetry/sdk-trace-base": { + "optional": true + } + } + }, + "node_modules/@sentry/opentelemetry": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.76.2.tgz", + "integrity": "sha512-deqWrKJGaku3nhiZchg34z8W9v6kUwtzoQ4cmEq6CeT8/4X93X4YfeJgoh1dhbswFRpwor4pQ6kj0KttagKtfA==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/core": "^1.30.1 || ^2.1.0", + "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0" + } + }, + "node_modules/@sentry/server-utils": { + "version": "10.76.2", + "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.76.2.tgz", + "integrity": "sha512-wogj6Trj8fAu+aD7HbpvHEBlyANMOvvO9MP5NFJaiQbprLKugtCdE6rFTxDhofXvRo4nswL9m4Cvysg3k5KcvQ==", + "license": "MIT", + "dependencies": { + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.76.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@sinclair/typebox": { "version": "0.33.17", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.33.17.tgz", @@ -2818,6 +3044,12 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/cjs-module-lexer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.3.0.tgz", + "integrity": "sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==", + "license": "MIT" + }, "node_modules/cli-cursor": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", @@ -4021,6 +4253,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.4.tgz", + "integrity": "sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==", + "license": "MIT" + }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -5241,6 +5479,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-in-the-middle": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.5.3.tgz", + "integrity": "sha512-usm/IiY63VtOyx/dm7uEoZV+EsanCDmwRUroQZIkFLGu1NVXZ8mZqIOhaN4QIJ7EsbMTuQbJXwCySCx+ozMA8g==", + "license": "Apache-2.0", + "dependencies": { + "cjs-module-lexer": "^2.2.0", + "es-module-lexer": "^3.0.3", + "module-details-from-path": "^1.0.4" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6655,6 +6907,12 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, + "node_modules/module-details-from-path": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz", + "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -7958,6 +8216,19 @@ "node": ">=0.10.0" } }, + "node_modules/require-in-the-middle": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz", + "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.5", + "module-details-from-path": "^1.0.3" + }, + "engines": { + "node": ">=9.3.0 || >=8.10.0 <9.0.0" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", diff --git a/package.json b/package.json index dabf4f3..9ac57b3 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "@fastify/websocket": "^10.0.1", "@mapeo/crypto": "^1.0.0-alpha.10", "@mapeo/default-config": "^6.0.0", + "@sentry/node": "^10.76.2", "@sinclair/typebox": "^0.33.17", "ajv": "^8.17.1", "env-schema": "^6.0.0", diff --git a/src/app.js b/src/app.js index bd5bcbc..80c7bf6 100644 --- a/src/app.js +++ b/src/app.js @@ -29,6 +29,7 @@ async function comapeoServer( serverName, allowedHosts, allowedProjects, + captureMessage, auditMaxAttachments, ...comapeoPluginOpts }, @@ -42,6 +43,7 @@ async function comapeoServer( serverBearerToken, serverName, allowedProjects, + captureMessage, auditMaxAttachments, }) } diff --git a/src/routes.js b/src/routes.js index 5ac2767..4ced80f 100644 --- a/src/routes.js +++ b/src/routes.js @@ -7,7 +7,11 @@ import assert from 'node:assert/strict' import * as fs from 'node:fs' import { STATUS_CODES } from 'node:http' -import { auditAttachments } from './attachments-audit.js' +import { + auditAttachments, + fetchFailure, + upstreamFailure, +} from './attachments-audit.js' import { Field as fieldSchema } from './datatypes/field.js' import { Observation as observationSchema } from './datatypes/observation.js' import { Preset as presetSchema } from './datatypes/preset.js' @@ -29,6 +33,11 @@ import { wsCoreReplicator } from './ws-core-replicator.js' /** @typedef {{baseUrl: URL, projectPublicId: string, project: MapeoProject}} MapDocParam */ /** @typedef {{docId: string, versionId: string}} Ref*/ /** @typedef {{docId: string, versionId: string, url: string}} UrlRef */ +/** @import { AttachmentCheck } from './attachments-audit.js' */ +/** + * Shaped like Sentry's captureMessage so it can be passed through directly + * @typedef {(message: string, context: { level: 'info' | 'warning', tags: Record, extra?: Record, fingerprint?: string[] }) => unknown} CaptureMessage + */ const BEARER_SPACE_LENGTH = 'Bearer '.length @@ -58,13 +67,20 @@ const HOP_BY_HOP_HEADERS = new Set([ * @prop {string} serverBearerToken * @prop {string} serverName * @prop {undefined | number | string[]} [allowedProjects=1] + * @prop {CaptureMessage} [captureMessage] receives one event per attachment that could not be served * @prop {number} [auditMaxAttachments] cap on attachments checked per audit */ /** @type {FastifyPluginAsync} */ export default async function routes( fastify, - { serverBearerToken, serverName, allowedProjects = 1, auditMaxAttachments }, + { + serverBearerToken, + serverName, + allowedProjects = 1, + captureMessage, + auditMaxAttachments, + }, ) { /** @type {Set | number} */ const allowedProjectsSetOrNumber = Array.isArray(allowedProjects) @@ -532,19 +548,58 @@ export default async function routes( ...typeAndVariant, }) + /** + * @param {AttachmentCheck} check + * @param {number} httpStatus sent to the client + * @param {unknown} [err] + */ + const reportFailure = (check, httpStatus, err) => { + req.log.warn( + { err, ...check, blobUrl }, + 'attachment could not be served', + ) + captureMessage?.('attachment.nonok', { + level: 'warning', + tags: { + project: req.params.projectPublicId, + drive: req.params.driveDiscoveryId.slice(0, 7), + type: typeAndVariant.type, + variant: typeAndVariant.variant, + class: check.status, + http_status: httpStatus, + ...(check.upstreamCode && { upstream_code: check.upstreamCode }), + }, + extra: { detail: check.detail }, + fingerprint: [ + 'attachment.nonok', + check.status, + check.upstreamCode ?? '', + ], + }) + } + let proxiedResponse try { proxiedResponse = await fetch(blobUrl) } catch (err) { // Core's blob server resets the socket when a blob's early blocks are missing - req.log.warn({ err }, 'attachment blob fetch failed') - throw errors.attachmentUpstreamUnavailableError() + const error = errors.attachmentUpstreamUnavailableError() + reportFailure(fetchFailure(err), error.statusCode, err) + throw error } reply.code(proxiedResponse.status) for (const [headerName, headerValue] of proxiedResponse.headers) { if (HOP_BY_HOP_HEADERS.has(headerName)) continue reply.header(headerName, headerValue) } + if (proxiedResponse.status !== 200) { + const body = await proxiedResponse.text() + reportFailure( + upstreamFailure(proxiedResponse, body), + proxiedResponse.status, + ) + return reply.send(body) + } return reply.send(proxiedResponse.body) }, ) diff --git a/src/server.js b/src/server.js index 5bea5a6..ca6ee78 100644 --- a/src/server.js +++ b/src/server.js @@ -33,6 +33,12 @@ const schema = Type.Object({ description: 'number of projects allowed to join the server', }), ), + SENTRY_DSN: Type.Optional( + Type.String({ + description: + 'Sentry DSN; error and attachment reporting is off when unset', + }), + ), AUDIT_MAX_ATTACHMENTS: Type.Integer({ minimum: 1, default: 200, @@ -44,6 +50,33 @@ const schema = Type.Object({ /** @type {ReturnType>} */ const config = envSchema({ schema, dotenv: true }) +/** @type {typeof import('@sentry/node') | undefined} */ +let Sentry +if (config.SENTRY_DSN) { + // Loaded on demand: importing it costs about 50 MB RSS + Sentry = await import('@sentry/node') + const { version } = JSON.parse( + await fsPromises.readFile( + new URL('../package.json', import.meta.url), + 'utf8', + ), + ) + Sentry.init({ + dsn: config.SENTRY_DSN, + release: `comapeo-cloud@${version}`, + // Only manual captures are used; fastify and core are already imported + registerEsmLoaderHooks: false, + // Sentry's default 'warn' mode would stop unhandled rejections crashing the process + integrations: [Sentry.onUnhandledRejectionIntegration({ mode: 'strict' })], + beforeSend(event) { + // Request headers carry the bearer token + delete event.request?.headers + delete event.request?.cookies + return event + }, + }) +} + const coreStorage = path.join(config.STORAGE_DIR, CORE_DIR_NAME) const dbFolder = path.join(config.STORAGE_DIR, DB_DIR_NAME) const rootKeyFile = path.join(config.STORAGE_DIR, ROOT_KEY_FILE_NAME) @@ -95,6 +128,7 @@ fastify.register(comapeoServer, { dbFolder, projectMigrationsFolder, clientMigrationsFolder, + ...(Sentry && { captureMessage: Sentry.captureMessage }), auditMaxAttachments: config.AUDIT_MAX_ATTACHMENTS, }) @@ -111,6 +145,7 @@ try { async function closeGracefully(signal) { console.log(`Received signal to terminate: ${signal}`) await fastify.close() + await Sentry?.close(2000) console.log('Gracefully closed fastify') process.kill(process.pid, signal) }