Files
wehub-resource-sync bb5c75ce05
Component Security Validation / Security Audit (push) Has been cancelled
Deploy to Cloudflare Pages / deploy (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:58 +08:00

139 lines
4.5 KiB
JavaScript

/**
* Minimal Sentry error reporter for Cloudflare Workers (no npm dependency).
*
* Sends errors directly to the Sentry envelope API via fetch(), matching this
* project's convention of single-file, zero-dependency Workers.
*
* Secret required (wrangler secret put SENTRY_DSN):
* SENTRY_DSN — DSN from the "aitmpl-workers" Sentry project
*/
/**
* Parse a Sentry DSN into the pieces needed to build the envelope endpoint.
* DSN format: https://<publicKey>@<host>/<projectId>
*/
function parseDsn(dsn) {
try {
const url = new URL(dsn);
return {
publicKey: url.username,
host: url.host,
projectId: url.pathname.replace(/^\//, ''),
};
} catch {
return null;
}
}
/**
* Report an error to Sentry. Safe to call even if SENTRY_DSN is not configured
* (no-op) or if the request to Sentry itself fails (never throws).
*
* @param {object} env - Worker env bindings (needs env.SENTRY_DSN)
* @param {Error|string} error - The error (or message) to report
* @param {object} [context] - Extra tags/context, e.g. { cron, endpoint, status }
*/
async function reportError(env, error, context = {}) {
const dsn = env && env.SENTRY_DSN;
if (!dsn) {
console.error('[sentry] SENTRY_DSN not configured, skipping report:', error);
return;
}
const parsed = parseDsn(dsn);
if (!parsed) {
console.error('[sentry] invalid SENTRY_DSN, skipping report');
return;
}
const message = error instanceof Error ? error.message : String(error);
const stacktrace = error instanceof Error && error.stack ? error.stack : undefined;
const eventId = crypto.randomUUID().replace(/-/g, '');
const timestamp = new Date().toISOString();
const event = {
event_id: eventId,
timestamp,
platform: 'javascript',
logger: 'cloudflare-worker',
environment: 'production',
tags: { worker: context.worker || 'unknown', ...context.tags },
extra: context,
exception: {
values: [
{
type: error instanceof Error ? error.name || 'Error' : 'Error',
value: message,
stacktrace: stacktrace ? { frames: [{ filename: 'worker', function: 'anonymous', context_line: stacktrace }] } : undefined,
},
],
},
};
const envelopeHeader = JSON.stringify({ event_id: eventId, sent_at: timestamp });
const itemHeader = JSON.stringify({ type: 'event' });
const body = `${envelopeHeader}\n${itemHeader}\n${JSON.stringify(event)}\n`;
const endpoint = `https://${parsed.host}/api/${parsed.projectId}/envelope/`;
const authHeader = `Sentry sentry_version=7, sentry_client=aitmpl-worker/1.0, sentry_key=${parsed.publicKey}`;
try {
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-sentry-envelope',
'X-Sentry-Auth': authHeader,
},
body,
});
} catch (sendError) {
// Never let error reporting break the worker itself
console.error('[sentry] failed to send report:', sendError.message);
}
}
/**
* Send a Sentry Cron Monitor check-in.
* @param {object} env - Worker env bindings (needs env.SENTRY_DSN)
* @param {string} monitorSlug - Slug configured in Sentry (Crons > Add Monitor)
* @param {'in_progress'|'ok'|'error'} status
* @param {string} [checkInId] - Pass the id returned from the 'in_progress' call
* so 'ok'/'error' updates the same check-in.
* @returns {Promise<string|undefined>} checkInId
*/
async function checkIn(env, monitorSlug, status, checkInId) {
const dsn = env && env.SENTRY_DSN;
if (!dsn) return undefined;
const parsed = parseDsn(dsn);
if (!parsed) return undefined;
const id = checkInId || crypto.randomUUID().replace(/-/g, '');
const timestamp = new Date().toISOString();
const envelopeHeader = JSON.stringify({ sent_at: timestamp });
const itemPayload = { check_in_id: id, monitor_slug: monitorSlug, status };
const itemHeader = JSON.stringify({ type: 'check_in' });
const body = `${envelopeHeader}\n${itemHeader}\n${JSON.stringify(itemPayload)}\n`;
const endpoint = `https://${parsed.host}/api/${parsed.projectId}/envelope/`;
const authHeader = `Sentry sentry_version=7, sentry_client=aitmpl-worker/1.0, sentry_key=${parsed.publicKey}`;
try {
await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-sentry-envelope',
'X-Sentry-Auth': authHeader,
},
body,
});
} catch (sendError) {
console.error('[sentry] failed to send check-in:', sendError.message);
}
return id;
}
export { reportError, checkIn };