Skip to content

Commit fa24bfb

Browse files
committed
feat: enable Sentry error reporting with CF Access auth and improved logging integration
1 parent 1cab165 commit fa24bfb

5 files changed

Lines changed: 27 additions & 20 deletions

File tree

worker/app.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ import { CsrfService } from './services/csrf/CsrfService';
99
import { SecurityError, SecurityErrorType } from 'shared/types/errors';
1010
import { getGlobalConfigurableSettings } from './config';
1111
import { AuthConfig, setAuthLevel } from './middleware/auth/routeAuth';
12-
// import { initHonoSentry } from './observability/sentry';
12+
import { initHonoSentry } from './observability/sentry';
1313

1414
export function createApp(env: Env): Hono<AppEnv> {
1515
const app = new Hono<AppEnv>();
1616

1717
// Observability: Sentry error reporting & context
18-
// initHonoSentry(app);
18+
initHonoSentry(app);
1919

2020
// Apply global security middlewares (skip for WebSocket upgrades)
2121
app.use('*', async (c, next) => {
@@ -77,7 +77,7 @@ export function createApp(env: Env): Hono<AppEnv> {
7777
c.set('config', config);
7878

7979
// Apply global rate limit middleware. Should this be moved after setupRoutes so that maybe 'user' is available?
80-
await RateLimitService.enforceGlobalApiRateLimit(env, c.get('config').security.rateLimit, c.get('user'), c.req.raw)
80+
await RateLimitService.enforceGlobalApiRateLimit(env, c.get('config').security.rateLimit, null, c.req.raw)
8181
await next();
8282
})
8383

worker/index.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { proxyToSandbox } from '@cloudflare/sandbox';
44
import { isDispatcherAvailable } from './utils/dispatcherUtils';
55
import { createApp } from './app';
66
import * as Sentry from '@sentry/cloudflare';
7-
import { sentryOptions, captureSecurityEvent } from './observability/sentry';
7+
import { sentryOptions } from './observability/sentry';
88

99
// Durable Object and Service exports
1010
export { UserAppSandboxService, DeployerService } from './services/sandbox/sandboxSdkClient';
@@ -43,7 +43,6 @@ async function handleUserAppRequest(request: Request, env: Env): Promise<Respons
4343
logger.info(`Sandbox miss for ${hostname}, attempting dispatch to permanent worker.`);
4444
if (!isDispatcherAvailable(env)) {
4545
logger.warn(`Dispatcher not available, cannot serve: ${hostname}`);
46-
captureSecurityEvent('dispatcher_unavailable', { hostname }, { level: 'error' });
4746
return new Response('This application is not currently available.', { status: 404 });
4847
}
4948

@@ -56,9 +55,7 @@ async function handleUserAppRequest(request: Request, env: Env): Promise<Respons
5655
return await worker.fetch(request);
5756
} catch (error: any) {
5857
// This block catches errors if the binding doesn't exist or if worker.fetch() fails.
59-
logger.error(`Error dispatching to worker '${appName}': ${error.message}`);
60-
captureSecurityEvent('dispatch_error', { subdomain: appName, hostname }, { level: 'error', error });
61-
// Return a generic error to the user to avoid leaking implementation details.
58+
logger.warn(`Error dispatching to worker '${appName}': ${error.message}`);
6259
return new Response('An error occurred while loading this application.', { status: 500 });
6360
}
6461
}
@@ -82,7 +79,6 @@ const worker = {
8279
// 2. Security: Immediately reject any requests made via an IP address.
8380
const ipRegex = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
8481
if (ipRegex.test(hostname)) {
85-
captureSecurityEvent('forbidden_ip_access', { hostname }, { level: 'warning' });
8682
return new Response('Access denied. Please use the assigned domain name.', { status: 403 });
8783
}
8884

@@ -112,9 +108,6 @@ const worker = {
112108
return handleUserAppRequest(request, env);
113109
}
114110

115-
// Route 3: Catch-all for invalid hostnames.
116-
// This is a security measure to prevent unauthorized domains from accessing the worker.
117-
captureSecurityEvent('invalid_hostname', { hostname }, { level: 'warning' });
118111
return new Response('Not Found', { status: 404 });
119112
},
120113
} satisfies ExportedHandler<Env>;

worker/logger/core.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/**
22
* Simple Structured Logger
33
*/
4-
4+
import * as Sentry from '@sentry/cloudflare';
55
import type { LoggerConfig, ObjectContext, LogEntry, LogLevel } from './types';
66

77
const DEFAULT_CONFIG: LoggerConfig = {
@@ -377,6 +377,7 @@ export class StructuredLogger {
377377

378378
error(message: string, ...args: unknown[]): void {
379379
const { data, error } = this.processArgsWithError(args);
380+
Sentry.captureException(error || new Error(message), { extra: data });
380381
this.log('error', message, data, error);
381382
}
382383

worker/middleware/auth/routeAuth.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { errorResponse } from '../../api/responses';
1212
import { Context } from 'hono';
1313
import { AppEnv } from '../../types/appenv';
1414
import { RateLimitExceededError } from 'shared/types/errors';
15+
import * as Sentry from '@sentry/cloudflare';
1516

1617
const logger = createLogger('RouteAuth');
1718

@@ -146,6 +147,11 @@ export async function enforceAuthRequirement(c: Context<AppEnv>) : Promise<Respo
146147
if (!user && (requirement.level === 'authenticated' || requirement.level === 'owner-only')) {
147148
user = await authMiddleware(c.req.raw, c.env);
148149
c.set('user', user);
150+
if (user) {
151+
Sentry.setUser({ id: user.id, email: user.email });
152+
} else {
153+
logger.warn('No user found');
154+
}
149155

150156
try {
151157
await RateLimitService.enforceAuthRateLimit(c.env, c.get('config').security.rateLimit, user, c.req.raw);
@@ -162,7 +168,7 @@ export async function enforceAuthRequirement(c: Context<AppEnv>) : Promise<Respo
162168
const env = c.env;
163169
const result = await routeAuthChecks(user, env, requirement, params);
164170
if (!result.success) {
165-
logger.error('Authentication check failed', result.response);
171+
logger.warn('Authentication check failed', result.response);
166172
return result.response;
167173
}
168174
}

worker/observability/sentry.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,22 @@ import type { Hono } from 'hono';
33
import { HTTPException } from 'hono/http-exception';
44
import type { AppEnv } from '../types/appenv';
55

6-
export function sentryOptions(env: Env) {
6+
export function sentryOptions(env: Env) : Sentry.CloudflareOptions {
7+
let transportOptions : Sentry.CloudflareOptions['transportOptions'] = {};
8+
if (env.CF_ACCESS_ID && env.CF_ACCESS_SECRET) {
9+
transportOptions.headers = {
10+
'CF-Access-Client-Id': env.CF_ACCESS_ID,
11+
'CF-Access-Client-Secret': env.CF_ACCESS_SECRET,
12+
};
13+
}
714
return {
815
dsn: env.SENTRY_DSN,
916
release: env.CF_VERSION_METADATA.id,
1017
environment: env.ENVIRONMENT,
1118
enableLogs: true,
1219
sendDefaultPii: true,
1320
tracesSampleRate: 1.0,
21+
transportOptions,
1422
};
1523
}
1624

@@ -32,11 +40,6 @@ export function initHonoSentry(app: Hono<AppEnv>): void {
3240
Sentry.setTag('http.path', url.pathname);
3341
const cfRay = c.req.header('cf-ray');
3442
if (cfRay) Sentry.setTag('cf_ray', cfRay);
35-
36-
const user = c.get('user');
37-
if (user) {
38-
Sentry.setUser({ id: user.id, email: user.email });
39-
}
4043
} catch {
4144
console.error('Failed to set Sentry context');
4245
}
@@ -86,3 +89,7 @@ export function captureSecurityEvent(
8689
console.error('Failed to capture security event');
8790
}
8891
}
92+
93+
export function captureException(error: Error): void {
94+
Sentry.captureException(error);
95+
}

0 commit comments

Comments
 (0)