-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsentry.edge.config.ts
More file actions
100 lines (84 loc) · 3.43 KB
/
sentry.edge.config.ts
File metadata and controls
100 lines (84 loc) · 3.43 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
// This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on).
// The config you add here will be used whenever one of the edge features is loaded.
// Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
import * as Sentry from "@sentry/nextjs";
/**
* Remove the api_secret query parameter from GA4 Measurement Protocol URLs.
* Defense-in-depth companion to the ga4-collect.ts isolation strategy.
*/
function scrubGaApiSecret(url: string): string {
try {
const parsed = new URL(url);
const hostname = parsed.hostname;
// Only scrub GA Measurement Protocol URLs on google-analytics.com and its subdomains
const isGoogleAnalyticsHost =
hostname === "google-analytics.com" ||
hostname === "www.google-analytics.com" ||
hostname.endsWith(".google-analytics.com");
if (!isGoogleAnalyticsHost) {
return url;
}
if (parsed.searchParams.has("api_secret")) {
parsed.searchParams.set("api_secret", "[Filtered]");
return parsed.toString();
}
} catch {
// Unparseable URL — return as-is rather than throwing
}
return url;
}
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Set sample rate (usually lower in production, e.g., 0.1)
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
// Only enable debug logs in development
debug: process.env.NODE_ENV === "development",
// Security: Handle PII carefully
sendDefaultPii: false,
beforeSend(event, hint) {
// Filter out common network errors that don't indicate real issues
const error = hint?.originalException;
if (error && typeof error === 'object' && 'message' in error) {
const errorMessage = String(error.message).toLowerCase();
// Ignore aborted requests (common during hot reload, navigation, etc.)
if (errorMessage.includes('aborted') ||
errorMessage.includes('econnreset') ||
errorMessage.includes('epipe') ||
errorMessage.includes('client closed') ||
errorMessage.includes('socket hang up')) {
return null; // Don't send to Sentry
}
}
// Scrub Authorization headers from all captured requests
if (event.request && event.request.headers) {
const headers = { ...event.request.headers };
delete headers["authorization"];
delete headers["cookie"];
event.request.headers = headers;
}
return event;
},
// Scrub api_secret from GA4 URLs in breadcrumbs (defense-in-depth, C-2)
beforeBreadcrumb(breadcrumb) {
if (breadcrumb.data?.url && typeof breadcrumb.data.url === "string") {
breadcrumb.data = { ...breadcrumb.data, url: scrubGaApiSecret(breadcrumb.data.url) };
}
return breadcrumb;
},
// Scrub api_secret from GA4 URLs in performance/transaction spans (defense-in-depth, C-2)
beforeSendTransaction(event) {
if (Array.isArray(event.spans)) {
for (const span of event.spans) {
if (span.data?.["http.url"] && typeof span.data["http.url"] === "string") {
span.data["http.url"] = scrubGaApiSecret(span.data["http.url"]);
}
if (span.data?.["url"] && typeof span.data["url"] === "string") {
span.data["url"] = scrubGaApiSecret(span.data["url"]);
}
}
}
return event;
},
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
});