mirror of
https://github.com/jason5ng32/MyIP.git
synced 2026-11-04 08:55:47 +00:00
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>
61 lines
2.5 KiB
JavaScript
61 lines
2.5 KiB
JavaScript
// /api/monitoring — first-party relay ("tunnel") for frontend Sentry
|
|
// envelopes, so ad/privacy blockers that block *.ingest.sentry.io don't
|
|
// silence error reporting. The browser SDK posts envelopes here (see
|
|
// `tunnel` in frontend/sentry-init.js); we validate that the envelope is
|
|
// addressed to OUR project and forward it. backend-server.js mounts the
|
|
// route only when VITE_SENTRY_DSN_FRONTEND is set.
|
|
import { fetchUpstream } from '../common/fetch-with-timeout.js';
|
|
import logger from '../common/logger.js';
|
|
|
|
// The envelope header is the first newline-delimited JSON line and carries
|
|
// the DSN the SDK was configured with. Returns a URL or null. Exported for
|
|
// tests.
|
|
export const parseEnvelopeDsn = (rawBody) => {
|
|
try {
|
|
const text = rawBody.toString('utf8');
|
|
const newline = text.indexOf('\n');
|
|
const header = JSON.parse(newline === -1 ? text : text.slice(0, newline));
|
|
return header?.dsn ? new URL(header.dsn) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export default async (req, res) => {
|
|
// Defensive method gate (route already POST-only) — covered by tests.
|
|
if (req.method !== 'POST') {
|
|
return res.status(405).json({ error: 'Method Not Allowed' });
|
|
}
|
|
|
|
// Only the DSN this deployment was built with may be relayed — anything
|
|
// else would make us an open proxy to arbitrary Sentry accounts.
|
|
const allowedDsn = process.env.VITE_SENTRY_DSN_FRONTEND;
|
|
if (!allowedDsn) {
|
|
return res.status(404).json({ error: 'Tunnel not configured' });
|
|
}
|
|
|
|
const dsn = parseEnvelopeDsn(req.body);
|
|
if (!dsn) {
|
|
return res.status(400).json({ error: 'Invalid envelope' });
|
|
}
|
|
if (dsn.href !== new URL(allowedDsn).href) {
|
|
return res.status(403).json({ error: 'DSN not allowed' });
|
|
}
|
|
|
|
try {
|
|
const projectId = dsn.pathname.replace(/^\//, '');
|
|
const upstream = `https://${dsn.host}/api/${projectId}/envelope/`;
|
|
const apiRes = await fetchUpstream(upstream, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/x-sentry-envelope' },
|
|
body: req.body,
|
|
});
|
|
res.status(apiRes.status).send(await apiRes.text());
|
|
} catch (error) {
|
|
// warn, not error: a transient relay failure is infra noise, not an
|
|
// application defect worth a grouped Issue of its own.
|
|
logger.warn({ err: error }, 'sentry tunnel forward failed');
|
|
res.status(502).json({ error: 'Relay failed' });
|
|
}
|
|
};
|