Advertisement

← vibesdk

worker/observability/sentry.ts

3.0 KB Download file
import * as Sentry from '@sentry/cloudflare';
import type { Hono } from 'hono';
import { HTTPException } from 'hono/http-exception';
import type { AppEnv } from '../types/appenv';

export function sentryOptions(env: Env) : Sentry.CloudflareOptions {
    const transportOptions: Sentry.CloudflareOptions['transportOptions'] = {};
    if (env.CF_ACCESS_ID && env.CF_ACCESS_SECRET) {
        transportOptions.headers = {
            'CF-Access-Client-Id': env.CF_ACCESS_ID,
            'CF-Access-Client-Secret': env.CF_ACCESS_SECRET,
        };
    }
	return {
		dsn: env.SENTRY_DSN,
		release: env.CF_VERSION_METADATA.id,
		environment: env.ENVIRONMENT,
		enableLogs: true,
		sendDefaultPii: true,
		tracesSampleRate: 1.0,
        transportOptions,
        allowUrls: [
            // Only capture errors from our API endpoints
            new RegExp(`^https://${env.CUSTOM_DOMAIN.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/api/.*$`)
        ]
	};
}

export function initHonoSentry(app: Hono<AppEnv>): void {
	// Report unhandled exceptions from routes/middleware
	app.onError((err, c) => {
		Sentry.captureException(err);
		if (err instanceof HTTPException) {
			return err.getResponse();
		}
		return c.json({ error: 'Internal server error' }, 500);
	});

	// Light context binding for better traces
	app.use('*', async (c, next) => {
		try {
			const url = new URL(c.req.url);
			Sentry.setTag('http.method', c.req.method);
			Sentry.setTag('http.path', url.pathname);
			const cfRay = c.req.header('cf-ray');
			if (cfRay) Sentry.setTag('cf_ray', cfRay);
		} catch {
            console.error('Failed to set Sentry context');
		}
		return next();
	});
}

export type SecurityEventType =
	| 'csrf_violation'
	| 'rate_limit_exceeded'
	| 'auth_violation'
	| 'oauth_state_mismatch'
	| 'jwt_invalid'
	| string;

export type SecuritySeverity = 'debug' | 'info' | 'warning' | 'error' | 'fatal';

export interface SecurityEventOptions {
    level?: SecuritySeverity;
    error?: unknown;
}

export function captureSecurityEvent(
    type: SecurityEventType,
    data: Record<string, unknown> = {},
    options: SecurityEventOptions = {},
): void {
    try {
        const level: SecuritySeverity = options.level ?? 'warning';
        Sentry.withScope((scope) => {
            scope.setTag('security_event', type);
            scope.setContext('security', data);
            scope.setLevel(level);
            Sentry.addBreadcrumb({
                category: 'security',
                level,
                data: { type, ...data },
            });
            if (options.error !== undefined) {
                Sentry.captureException(options.error, { level, extra: data });
            } else {
                Sentry.captureMessage(`[security] ${type}`, level);
            }
        });
    } catch {
        // no-op: telemetry must not break the app
        console.error('Failed to capture security event');
    }
}

export function captureException(error: Error): void {
    Sentry.captureException(error);
}