-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsentry.ts
More file actions
215 lines (187 loc) · 6.02 KB
/
sentry.ts
File metadata and controls
215 lines (187 loc) · 6.02 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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
/**
* Sentry Configuration
*
* Provides common Sentry configuration for both Node.js and Cloudflare Workers.
* Includes Span Metrics and Sentry Metrics configuration.
*/
import type { Env } from "@/types";
// Define types for Sentry callbacks
interface SentryMetric {
name: string;
type: "counter" | "gauge" | "distribution" | "set";
attributes?: Record<string, unknown>;
unit?: string;
value: number;
}
// SpanJSON represents the serialized span object passed to beforeSendSpan
interface SpanJSON {
data?: Record<string, unknown>;
[key: string]: unknown;
}
// Event represents the error/message event passed to beforeSend
interface SentryEvent {
breadcrumbs?: Array<{
data?: Record<string, unknown>;
[key: string]: unknown;
}>;
extra?: Record<string, unknown>;
contexts?: Record<string, unknown>;
[key: string]: unknown;
}
/**
* Common Sentry configuration options
*
* Includes:
* - Tracing configuration (10% sample rate)
* - Metrics enabled (counters, gauges, distributions)
* - beforeSendSpan callback for global span attributes
* - beforeSendMetric callback for PII filtering
*/
export function getSentryConfig(env: Env): Record<string, unknown> | null {
const dsn = env.SENTRY_DSN as string | undefined;
if (!dsn) {
return null; // Sentry is optional
}
const environment = (env.SENTRY_ENVIRONMENT ||
env.NODE_ENV ||
"development") as string;
const release = env.SENTRY_RELEASE as string | undefined;
// Detect runtime from explicit env.RUNTIME (set by entry points)
// Fallback to process detection only if RUNTIME not set
const runtime: "nodejs" | "cloudflare" =
env.RUNTIME ||
(typeof process !== "undefined" && process.env ? "nodejs" : "cloudflare");
return {
dsn,
environment,
release,
// Set to 1.0 in development/staging for complete observability
// Lower in production to manage quota
tracesSampleRate: environment === "production" ? 0.1 : 1.0,
// Enable Sentry Metrics (counters, gauges, distributions)
enableMetrics: true,
// Enable logs for better debugging
enableLogs: true,
// Send default PII (request headers, IP) for better context
// Safe to enable because we filter sensitive fields via beforeSend callbacks
sendDefaultPii: true,
// Debug mode (verbose logging - useful for development)
debug: environment === "development",
// Vercel AI SDK integration for automatic AI span tracking
// Captures token usage, model info, latency, and errors from AI SDK calls
// Note: Integration setup is handled differently for Cloudflare Workers vs Node.js
// For Cloudflare, integrations are configured in the entry point via withSentry
enableAIIntegration: true,
/**
* beforeSendMetric callback
*
* Filters sensitive data from metrics before sending to Sentry
* Returns null to drop metrics, or the metric to send it
*/
beforeSendMetric: (metric: SentryMetric): SentryMetric | null => {
// List of PII field names to remove
const piiFields = [
"email",
"recipient",
"userEmail",
"user_email",
"username",
"password",
];
// Remove any PII from metric attributes
if (metric.attributes) {
for (const field of piiFields) {
delete metric.attributes[field];
}
}
// Don't send test metrics in production
if (metric.name.startsWith("test.") && environment === "production") {
return null;
}
return metric;
},
/**
* beforeSend callback
*
* Removes PII from error events before sending to Sentry
* This ensures email addresses and other sensitive data never leave the application
*/
beforeSend: (event: SentryEvent): SentryEvent | null => {
// List of PII field names to remove
const piiFields = [
"email",
"recipient",
"userEmail",
"user_email",
"username",
"password",
];
// Helper to recursively remove PII from an object
const removePII = (
obj: Record<string, unknown>
): Record<string, unknown> => {
const cleaned = { ...obj };
for (const key of piiFields) {
delete cleaned[key];
}
// Recursively clean nested objects
for (const [key, value] of Object.entries(cleaned)) {
if (value && typeof value === "object" && !Array.isArray(value)) {
cleaned[key] = removePII(value as Record<string, unknown>);
}
}
return cleaned;
};
// Remove PII from breadcrumbs
if (event.breadcrumbs) {
event.breadcrumbs = event.breadcrumbs.map((breadcrumb) => {
if (breadcrumb.data) {
return { ...breadcrumb, data: removePII(breadcrumb.data) };
}
return breadcrumb;
});
}
// Remove PII from extra context
if (event.extra) {
event.extra = removePII(event.extra);
}
// Remove PII from contexts (signup.email, login.username, etc.)
if (event.contexts) {
event.contexts = removePII(event.contexts);
}
return event;
},
/**
* beforeSendSpan callback
*
* Adds global context to all spans (traces) and removes PII
* Note: beforeSendSpan receives a serialized SpanJSON object, not a Span instance
*/
beforeSendSpan: (span: SpanJSON): SpanJSON => {
// List of PII field names to remove
const piiFields = [
"email",
"recipient",
"userEmail",
"user_email",
"username",
"password",
];
// Initialize data object if it doesn't exist
if (!span.data) {
span.data = {};
}
// Remove PII from span attributes
for (const field of piiFields) {
delete span.data[field];
}
// Add global context directly to span data
span.data.runtime = runtime;
if (release) {
span.data["app.version"] = release;
}
span.data["app.environment"] = environment;
return span;
},
};
}