-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathcloudflare.ts
More file actions
152 lines (133 loc) · 4.79 KB
/
cloudflare.ts
File metadata and controls
152 lines (133 loc) · 4.79 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* Cloudflare Workers Entry Point for Hono App
*
* This entry point runs on Cloudflare Workers edge runtime.
*/
import * as Sentry from "@sentry/cloudflare";
import { createHonoApp } from "@/hono/app";
import { getSentryConfig } from "@/config/sentry";
import type { Env } from "@/types";
/**
* Prepare environment with D1 instrumentation
* Extracts common logic used by both fetch and scheduled handlers
*/
function prepareEnv(env: Env): Env {
let workerEnv: Env = { ...env, RUNTIME: "cloudflare" };
// Instrument D1 database with Sentry if available
if (env.DB && env.SENTRY_DSN) {
try {
const instrumentedD1 = Sentry.instrumentD1WithSentry(env.DB);
workerEnv = { ...workerEnv, DB: instrumentedD1 };
} catch {
// Sentry instrumentation failed, continue with regular D1
}
}
return workerEnv;
}
// Worker handler with fetch and scheduled methods
const workerHandler = {
async fetch(request: Request, env: Env): Promise<Response> {
// Validate required environment variables
// In Cloudflare Workers, we can't exit() so we log errors and return error responses
if (!env.BETTER_AUTH_SECRET) {
console.error(
"❌ FATAL: BETTER_AUTH_SECRET environment variable is required.\n" +
" Set it in wrangler.toml or via Cloudflare dashboard"
);
return new Response(
JSON.stringify({
error: "Server misconfiguration: BETTER_AUTH_SECRET not set",
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
}
// Warn about weak secrets in production
if (
env.SENTRY_ENVIRONMENT === "production" &&
env.BETTER_AUTH_SECRET.length < 32
) {
console.warn("⚠️ WARNING: BETTER_AUTH_SECRET should be >=32 characters");
}
const workerEnv = prepareEnv(env);
const app = createHonoApp({
env: workerEnv,
sentry: Sentry,
runtime: "cloudflare",
});
return app.fetch(request, workerEnv);
},
async scheduled(controller: ScheduledController, env: Env): Promise<void> {
console.log("⏰ Cron triggered:", controller.cron);
try {
// Dynamic imports to reduce cold start time
const { executeScheduledTasks } = await import("../cron/executor");
const { createDatabase } = await import("../db/client");
const workerEnv = prepareEnv(env);
const db = createDatabase(workerEnv);
const result = await executeScheduledTasks(workerEnv, db);
console.log("✅ Cron job completed:", {
rssFetch: result.rssFetch.executed ? "executed" : "skipped",
articlePrune: result.articlePrune.executed ? "executed" : "skipped",
tokenCleanup: result.tokenCleanup.executed ? "executed" : "skipped",
});
} catch (error) {
console.error("❌ Cron job failed:", error);
throw error;
} finally {
// Flush Sentry metrics before Workers execution ends
if (env.SENTRY_DSN) {
await Sentry.flush(2000); // 2 second timeout
}
}
},
};
// Wrap with Sentry using withSentry pattern
export default Sentry.withSentry((env: Env) => {
const config = getSentryConfig(env);
// If no DSN provided, Sentry will be disabled
if (!config) {
console.warn(
"⚠️ Sentry not initialized: SENTRY_DSN not configured. Set SENTRY_DSN in wrangler.toml or Cloudflare secrets."
);
return { dsn: undefined };
}
// Add version metadata if available
const versionId = env?.CF_VERSION_METADATA?.id;
if (versionId && typeof versionId === "string") {
config.release = versionId;
}
// Add Vercel AI SDK integration for automatic AI span tracking
// Captures token usage, model info, latency, and errors from AI SDK calls
// Note: Input/output recording is controlled via experimental_telemetry in AI SDK calls
// Type assertion needed since getSentryConfig returns Record<string, unknown>
const existingIntegrations = Array.isArray(config.integrations)
? (config.integrations as unknown[])
: [];
config.integrations = [
...existingIntegrations,
Sentry.vercelAIIntegration(),
// Automatically capture console.log, console.warn, and console.error as logs
Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
// Hono error capturing integration (enabled by default, but explicit for clarity)
Sentry.honoIntegration(),
];
// Log Sentry initialization in development
const environment = (env.SENTRY_ENVIRONMENT ||
env.NODE_ENV ||
"development") as string;
if (environment === "development") {
console.log("✅ Sentry initialized for backend:", {
environment,
release: config.release,
hasDsn: !!config.dsn,
aiTracking: true,
consoleLogging: true,
httpTracing: true,
trpcTracing: true,
});
}
return config;
}, workerHandler);