Files
MyIP/tests/sentry-tunnel.test.js
T
jason5ng32andClaude Fable 5 9bfbf21a60 Feat(monitoring): integrate Sentry across frontend and backend, env-gated end to end
Frontend (@sentry/vue, loaded via build-time-gated dynamic import — no
VITE_SENTRY_DSN_FRONTEND, no Sentry in the bundle; init joins the mount
gate so instrumentation precedes the first render):
- uncaught errors, route-change tracing / Web Vitals, error-only masked
  Session Replay, console.error capture fingerprinted per message prefix
  so each fallback source stays a distinct issue
- explicit ip-source:exhausted signal via the app-events bus
- hidden source maps uploaded (then deleted) by @sentry/vite-plugin,
  gated on SENTRY_AUTH_TOKEN

Backend (@sentry/node, bootstrapped by sentry-instrument.js via
node --import so ESM loader hooks land before express; never loaded
without SENTRY_DSN_BACKEND):
- Express error capture + per-route tracing
- logger hook mirrors warn+ lines to Sentry Logs and elevates error+
  to grouped, alertable Issues
- Sentry Crons check-ins for the MaxMind / CAIDA / service-status jobs
  via the no-op-without-DSN wrapper common/sentry-cron.js

Delivery & ops:
- first-party tunnel /api/monitoring relays envelopes past ad blockers;
  DSN-allowlisted (never an open relay), exempt from the shared rate
  limiter with its own generous ceiling
- rate-limit ledger file becomes opt-in now that the warn line reaches
  Sentry Logs; ecosystem.config.cjs carries the --import flag for pm2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-12 11:16:17 +08:00

65 lines
2.3 KiB
JavaScript

// api/sentry-tunnel.js — envelope-header parsing and the guard branches that
// return before any upstream forward (method gate / unconfigured / invalid
// envelope / foreign DSN). No real Sentry traffic is ever sent.
import { test, beforeEach } from 'node:test';
import assert from 'node:assert';
import tunnelHandler, { parseEnvelopeDsn } from '../api/sentry-tunnel.js';
const OUR_DSN = 'https://publickey@o111.ingest.sentry.io/222';
const makeRes = () => {
const res = {
statusCode: null,
body: null,
status(code) { this.statusCode = code; return this; },
json(payload) { this.body = payload; return this; },
send(payload) { this.body = payload; return this; },
};
return res;
};
const envelopeFor = (dsn) => Buffer.from(`${JSON.stringify({ dsn })}\n{"type":"event"}\n{}`);
beforeEach(() => {
delete process.env.VITE_SENTRY_DSN_FRONTEND;
});
test('parseEnvelopeDsn extracts the DSN from the envelope header', () => {
const dsn = parseEnvelopeDsn(envelopeFor(OUR_DSN));
assert.strictEqual(dsn.host, 'o111.ingest.sentry.io');
assert.strictEqual(dsn.pathname, '/222');
});
test('parseEnvelopeDsn returns null for garbage input', () => {
assert.strictEqual(parseEnvelopeDsn(Buffer.from('not json')), null);
assert.strictEqual(parseEnvelopeDsn(Buffer.from('{"no_dsn":1}\n{}')), null);
});
test('rejects non-POST methods with 405', async () => {
const res = makeRes();
await tunnelHandler({ method: 'GET' }, res);
assert.strictEqual(res.statusCode, 405);
});
test('returns 404 when no frontend DSN is configured', async () => {
const res = makeRes();
await tunnelHandler({ method: 'POST', body: envelopeFor(OUR_DSN) }, res);
assert.strictEqual(res.statusCode, 404);
});
test('returns 400 on an unparseable envelope', async () => {
process.env.VITE_SENTRY_DSN_FRONTEND = OUR_DSN;
const res = makeRes();
await tunnelHandler({ method: 'POST', body: Buffer.from('garbage') }, res);
assert.strictEqual(res.statusCode, 400);
});
test('returns 403 when the envelope targets a foreign DSN', async () => {
process.env.VITE_SENTRY_DSN_FRONTEND = OUR_DSN;
const res = makeRes();
const foreign = envelopeFor('https://otherkey@o999.ingest.sentry.io/888');
await tunnelHandler({ method: 'POST', body: foreign }, res);
assert.strictEqual(res.statusCode, 403);
});