@debugbundle/sdk-browser 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,17 +2,20 @@
2
2
 
3
3
  Browser SDK for DebugBundle.
4
4
 
5
- Use this package to capture frontend exceptions, breadcrumbs, probe data, and browser device context. The recommended transport is the same-origin browser relay served by your backend.
5
+ ![npm](https://img.shields.io/npm/v/%40debugbundle%2Fsdk-browser?label=npm)
6
+ ![License](https://img.shields.io/badge/license-AGPL--3.0--only-blue)
6
7
 
7
- ## Install
8
+ Use this package to capture frontend exceptions, breadcrumbs, first-party request failures, browser device context, trace headers, and probe data. The recommended transport is a same-origin browser relay served by your backend.
8
9
 
9
- Stable npm release:
10
+ ## Installation
10
11
 
11
12
  ```bash
12
13
  npm install @debugbundle/sdk-browser
13
14
  ```
14
15
 
15
- ## Example
16
+ Keep `@debugbundle/sdk-browser` and `@debugbundle/sdk-node` on the same release version. If you pin the core-owned support packages directly, keep `@debugbundle/shared-types` and `@debugbundle/redaction` on the same version too.
17
+
18
+ ## Quick Start
16
19
 
17
20
  ```ts
18
21
  import { createDebugBundleBrowserSdk } from "@debugbundle/sdk-browser";
@@ -21,12 +24,137 @@ const debugbundle = createDebugBundleBrowserSdk();
21
24
 
22
25
  debugbundle.init({
23
26
  endpoint: "/debugbundle/browser",
24
- service: "example-web",
27
+ service: "web",
25
28
  environment: "production"
26
29
  });
27
30
  ```
28
31
 
29
- ## Notes
32
+ The browser SDK starts capture only after `init()` is called. Importing the package has no side effects.
33
+
34
+ ## Transport Modes
35
+
36
+ | Mode | Configuration | Use when |
37
+ | --- | --- | --- |
38
+ | Relay | `endpoint: "/debugbundle/browser"` | Recommended for full-stack apps. Browser events go to your backend first. |
39
+ | Direct cloud | `projectToken` plus the hosted endpoint | Frontend-only apps without a backend. Use a dedicated write-only token with allowed-origin restrictions. |
40
+
41
+ For relay setup, see <https://debugbundle.com/docs/sdks/browser-relay>.
42
+
43
+ ### Configuration source precedence
44
+
45
+ 1. Explicit `init(...)` fields win.
46
+ 2. Omitted values fall back to package defaults such as `service: "browser-app"` and `environment: "development"`.
47
+ 3. Capture-policy fields are server-owned and arrive from `GET /v1/sdk/config`; they are not accepted from local browser config.
48
+
49
+ Relay mode should configure only the same-origin path plus the service/environment names. Direct-cloud mode requires a dedicated public write-only token and a real ingestion endpoint URL.
50
+
51
+ ## What It Captures
52
+
53
+ - Frontend exceptions and unhandled promise rejections
54
+ - Recent breadcrumbs from clicks, route changes, console warnings/errors, and first-party network requests
55
+ - First-party request failures that should become incident signals
56
+ - Browser and device context such as user agent, viewport, screen, locale, connection type, and color scheme
57
+ - `X-DebugBundle-Trace-Id` headers on allowed outgoing requests for frontend/backend correlation
58
+ - Always-on probe ring buffers that flush with exceptions
59
+
60
+ Breadcrumbs are kept in memory and attached to frontend exceptions by default. They are not independently shipped unless configured.
61
+
62
+ ## Configuration
63
+
64
+ | Option | Default | Purpose |
65
+ | --- | --- | --- |
66
+ | `endpoint` | derived from transport | Relay or ingestion endpoint. |
67
+ | `projectToken` | none | Direct cloud write-only token for frontend-only deployments. Omit when using relay. |
68
+ | `service` | `browser-app` | Frontend service name shown on incidents and bundles. |
69
+ | `environment` | `development` | Runtime environment such as `production`, `staging`, or `development`. |
70
+ | `enabled` | `true` | Disable all capture without removing instrumentation. |
71
+ | `redactFields` | common sensitive fields | Additional field names to redact. |
72
+ | `sampleRate` | `1.0` | Per-event sampling rate. |
73
+ | `sessionSampleRate` | `1.0` | Per-session capture sampling rate. |
74
+ | `batchSize` | `10` | Events per batch before flushing. |
75
+ | `flushInterval` | `3000` | Flush interval in milliseconds. |
76
+ | `logLevel` | `warning` | Minimum captured browser log severity. |
77
+ | `maxBreadcrumbs` | `10` | Breadcrumb ring-buffer size. |
78
+ | `breadcrumbsOnErrorOnly` | `true` | Attach breadcrumbs to exceptions instead of shipping them independently. |
79
+ | `captureNetwork` | `true` | Capture first-party network breadcrumbs and failure signals. |
80
+ | `captureClicks` | `true` | Capture click breadcrumbs. |
81
+ | `captureRouteChanges` | `true` | Capture route-change breadcrumbs. |
82
+ | `captureConsole` | `false` | Capture console warnings and errors. |
83
+ | `networkFilter` | default failure filtering | Include or exclude requests by URL, status, or response time. |
84
+ | `maxEventsPerSession` | `100` | Cap non-exception events per browser session. |
85
+ | `tracePropagationTargets` | same-origin | URLs allowed to receive `X-DebugBundle-Trace-Id`. |
86
+ | `maxProbeLabels` | `50` | Maximum distinct probe labels buffered in memory. |
87
+ | `maxProbeEntriesPerLabel` | `10` | Maximum entries retained per probe label. |
88
+ | `probeFlushOnError` | `true` | Attach buffered probe data to captured exceptions. |
89
+ | `requestTimeoutMs` | `5000` | Transport timeout in milliseconds. |
90
+ | `transport` | fetch transport | Custom transport function for tests or advanced routing. |
91
+
92
+ ## Service naming guidance
93
+
94
+ Keep the browser service name distinct from backend deployables inside the same DebugBundle project. A common pattern is `checkout-web` for the browser frontend and `checkout-api` for the backend relay host.
95
+
96
+ When you send through a same-origin relay, the browser service name should stay browser-owned. The backend relay should not overwrite it unless you intentionally want a shared surface name.
97
+
98
+ ## Explicit Capture
99
+
100
+ ```ts
101
+ debugbundle.captureException(error, { route: window.location.pathname });
102
+ debugbundle.captureLog("checkout warning", "warning", { cartId });
103
+ debugbundle.captureMessage("user started checkout");
104
+ debugbundle.probe("checkout.cart", { itemCount: cart.items.length });
105
+
106
+ await debugbundle.flush();
107
+ ```
108
+
109
+ ## Safety and Privacy
110
+
111
+ - SDK failures are caught internally and do not break the host page.
112
+ - Sensitive fields are redacted before transport.
113
+ - Duplicate event storms are suppressed locally.
114
+ - Browser project tokens are never needed when using the same-origin relay.
115
+ - Breadcrumb and probe buffers are in-memory only.
116
+
117
+ ## Safe startup behavior
118
+
119
+ - Relay mode keeps browser-visible credentials out of the page and does not require a token in frontend config.
120
+ - Invalid relay paths or missing direct-cloud credentials fail closed without crashing the host page.
121
+ - `status()` exposes whether the SDK is healthy, degraded, or disconnected.
122
+ - Auth-rejected direct-cloud responses stop pretending capture is healthy and clear buffered events only after the endpoint explicitly rejects the token.
123
+
124
+ ## First-event verification
125
+
126
+ Minimal application check:
127
+
128
+ ```ts
129
+ import { createDebugBundleBrowserSdk } from "@debugbundle/sdk-browser";
130
+
131
+ const debugbundle = createDebugBundleBrowserSdk();
132
+
133
+ debugbundle.init({
134
+ endpoint: "/debugbundle/browser",
135
+ service: "checkout-web",
136
+ environment: "development"
137
+ });
138
+
139
+ debugbundle.captureException(new Error("debugbundle browser smoke"));
140
+ await debugbundle.flush();
141
+ console.log(debugbundle.status());
142
+ ```
143
+
144
+ Repository-level verification runs the same clean-install smoke used by CI and release:
145
+
146
+ ```bash
147
+ pnpm build
148
+ pnpm smoke:packed
149
+ ```
150
+
151
+ ## Documentation
152
+
153
+ - Browser SDK docs: <https://debugbundle.com/docs/sdks/browser>
154
+ - Browser relay: <https://debugbundle.com/docs/sdks/browser-relay>
155
+ - SDK overview: <https://debugbundle.com/docs/sdks>
156
+ - Repository: <https://github.com/debugbundle/debugbundle-js>
157
+
158
+ ## License
30
159
 
31
- - Published from the standalone `debugbundle/debugbundle-js` repository.
32
- - Depends on the core-owned published `@debugbundle/shared-types` and `@debugbundle/redaction` packages.
160
+ AGPL-3.0-only.
@@ -0,0 +1,4 @@
1
+ import type { BrowserCaptureRule, BrowserCaptureRuleEvaluationResult, EventEnvelope } from "./types.js";
2
+ export declare function parseRemoteCaptureRulesPayload(payload: unknown): BrowserCaptureRule[];
3
+ export declare function evaluateBrowserCaptureRulesForEvent(rules: readonly BrowserCaptureRule[], projectId: string, event: EventEnvelope, now: string): BrowserCaptureRuleEvaluationResult | null;
4
+ //# sourceMappingURL=capture-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capture-rules.d.ts","sourceRoot":"","sources":["../src/capture-rules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAElB,kCAAkC,EAKlC,aAAa,EACd,MAAM,YAAY,CAAC;AAmUpB,wBAAgB,8BAA8B,CAAC,OAAO,EAAE,OAAO,GAAG,kBAAkB,EAAE,CASrF;AAuQD,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,SAAS,kBAAkB,EAAE,EACpC,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,aAAa,EACpB,GAAG,EAAE,MAAM,GACV,kCAAkC,GAAG,IAAI,CA0C3C"}
@@ -0,0 +1,515 @@
1
+ function asRecord(value) {
2
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
3
+ return null;
4
+ }
5
+ return value;
6
+ }
7
+ function asString(value) {
8
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
9
+ }
10
+ function asBoolean(value) {
11
+ return typeof value === "boolean" ? value : null;
12
+ }
13
+ function asInteger(value) {
14
+ return typeof value === "number" && Number.isInteger(value) ? value : null;
15
+ }
16
+ function asNumber(value) {
17
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
18
+ }
19
+ function normalizeRuntime(value) {
20
+ switch (value?.trim().toLowerCase()) {
21
+ case "browser":
22
+ return "browser";
23
+ case "node":
24
+ case "nodejs":
25
+ return "node";
26
+ case "python":
27
+ return "python";
28
+ case "php":
29
+ return "php";
30
+ case "java":
31
+ return "java";
32
+ case "go":
33
+ case "golang":
34
+ return "go";
35
+ case "ruby":
36
+ return "ruby";
37
+ default:
38
+ return "unknown";
39
+ }
40
+ }
41
+ function normalizePath(value) {
42
+ const path = value.split(/[?#]/, 1)[0] ?? "";
43
+ if (path.length === 0) {
44
+ return "/";
45
+ }
46
+ return path.startsWith("/") ? path : `/${path}`;
47
+ }
48
+ function normalizeEvaluationUrl(value) {
49
+ const trimmed = value?.trim();
50
+ if (trimmed === undefined || trimmed.length === 0) {
51
+ return {};
52
+ }
53
+ if (trimmed.startsWith("/")) {
54
+ return {
55
+ url: { path: normalizePath(trimmed) },
56
+ first_party: true
57
+ };
58
+ }
59
+ try {
60
+ const parsed = new URL(trimmed);
61
+ if (parsed.protocol === "http:" || parsed.protocol === "https:") {
62
+ return {
63
+ url: {
64
+ ...(parsed.hostname.length > 0 ? { host: parsed.hostname.toLowerCase() } : {}),
65
+ path: normalizePath(parsed.pathname)
66
+ },
67
+ first_party: false
68
+ };
69
+ }
70
+ }
71
+ catch {
72
+ return {};
73
+ }
74
+ return {};
75
+ }
76
+ function parseStringArray(value) {
77
+ if (!Array.isArray(value)) {
78
+ return undefined;
79
+ }
80
+ const values = value
81
+ .map((entry) => asString(entry))
82
+ .filter((entry) => entry !== null);
83
+ return values.length > 0 ? Array.from(new Set(values)) : undefined;
84
+ }
85
+ function parseNumberArray(value) {
86
+ if (!Array.isArray(value)) {
87
+ return undefined;
88
+ }
89
+ const values = value
90
+ .map((entry) => asInteger(entry))
91
+ .filter((entry) => entry !== null);
92
+ return values.length > 0 ? Array.from(new Set(values)).sort((left, right) => left - right) : undefined;
93
+ }
94
+ function parseStatusRanges(value) {
95
+ if (!Array.isArray(value)) {
96
+ return undefined;
97
+ }
98
+ const ranges = value
99
+ .map((entry) => {
100
+ const record = asRecord(entry);
101
+ const start = asInteger(record?.["start"]);
102
+ const end = asInteger(record?.["end"]);
103
+ if (start === null || end === null || start > end) {
104
+ return null;
105
+ }
106
+ return { start, end };
107
+ })
108
+ .filter((entry) => entry !== null);
109
+ return ranges.length > 0 ? ranges : undefined;
110
+ }
111
+ function parseUrlMatcher(value) {
112
+ const record = asRecord(value);
113
+ if (record === null) {
114
+ return undefined;
115
+ }
116
+ const host = asString(record["host"])?.toLowerCase();
117
+ const hostSuffix = asString(record["host_suffix"])?.toLowerCase();
118
+ const pathPrefix = asString(record["path_prefix"]);
119
+ const pathEquals = asString(record["path_equals"]);
120
+ if (host === undefined && hostSuffix === undefined && pathPrefix === undefined && pathEquals === undefined) {
121
+ return undefined;
122
+ }
123
+ return {
124
+ ...(host === undefined ? {} : { host }),
125
+ ...(hostSuffix === undefined ? {} : { host_suffix: hostSuffix }),
126
+ ...(pathPrefix === null ? {} : { path_prefix: normalizePath(pathPrefix) }),
127
+ ...(pathEquals === null ? {} : { path_equals: normalizePath(pathEquals) })
128
+ };
129
+ }
130
+ function parseMatcher(value) {
131
+ const record = asRecord(value);
132
+ if (record === null) {
133
+ return null;
134
+ }
135
+ const eventTypes = parseStringArray(record["event_types"]);
136
+ const services = parseStringArray(record["services"]);
137
+ const environments = parseStringArray(record["environments"]);
138
+ const runtimeValues = parseStringArray(record["runtime"]);
139
+ const firstParty = asBoolean(record["first_party"]);
140
+ const errorName = asString(record["error_name"]);
141
+ const messageContains = asString(record["message_contains"]);
142
+ const messageEquals = asString(record["message_equals"]);
143
+ const resourceUrl = parseUrlMatcher(record["resource_url"]);
144
+ const requestUrl = parseUrlMatcher(record["request_url"]);
145
+ const statusCodes = parseNumberArray(record["status_codes"]);
146
+ const statusRanges = parseStatusRanges(record["status_ranges"]);
147
+ const matcher = {
148
+ ...(eventTypes === undefined ? {} : { event_types: eventTypes }),
149
+ ...(services === undefined ? {} : { services }),
150
+ ...(environments === undefined ? {} : { environments }),
151
+ ...(runtimeValues === undefined ? {} : { runtime: runtimeValues.map((entry) => normalizeRuntime(entry)) }),
152
+ ...(firstParty === null ? {} : { first_party: firstParty }),
153
+ ...(errorName === null ? {} : { error_name: errorName }),
154
+ ...(messageContains === null ? {} : { message_contains: messageContains }),
155
+ ...(messageEquals === null ? {} : { message_equals: messageEquals }),
156
+ ...(record["browser_event_kind"] === "window_error" || record["browser_event_kind"] === "resource_error"
157
+ ? { browser_event_kind: record["browser_event_kind"] }
158
+ : {}),
159
+ ...(resourceUrl === undefined ? {} : { resource_url: resourceUrl }),
160
+ ...(requestUrl === undefined ? {} : { request_url: requestUrl }),
161
+ ...(statusCodes === undefined ? {} : { status_codes: statusCodes }),
162
+ ...(statusRanges === undefined ? {} : { status_ranges: statusRanges })
163
+ };
164
+ const fingerprintRecord = asRecord(record["fingerprint"]);
165
+ const fingerprintVersion = asString(fingerprintRecord?.["version"]);
166
+ const fingerprintValue = asString(fingerprintRecord?.["value"]);
167
+ if (fingerprintVersion !== null && fingerprintValue !== null) {
168
+ matcher.fingerprint = {
169
+ version: fingerprintVersion,
170
+ value: fingerprintValue
171
+ };
172
+ }
173
+ const narrowingKeys = [
174
+ matcher.services,
175
+ matcher.environments,
176
+ matcher.runtime,
177
+ matcher.first_party,
178
+ matcher.error_name,
179
+ matcher.message_contains,
180
+ matcher.message_equals,
181
+ matcher.browser_event_kind,
182
+ matcher.resource_url,
183
+ matcher.request_url,
184
+ matcher.status_codes,
185
+ matcher.status_ranges,
186
+ matcher.fingerprint
187
+ ];
188
+ if (!narrowingKeys.some((entry) => entry !== undefined)) {
189
+ return null;
190
+ }
191
+ if (matcher.browser_event_kind === "resource_error" &&
192
+ matcher.resource_url === undefined &&
193
+ matcher.fingerprint === undefined) {
194
+ return null;
195
+ }
196
+ return matcher;
197
+ }
198
+ function parseCaptureRule(value) {
199
+ const record = asRecord(value);
200
+ if (record === null) {
201
+ return null;
202
+ }
203
+ const id = asString(record["id"]);
204
+ const projectId = asString(record["project_id"]);
205
+ const name = asString(record["name"]);
206
+ const actionValue = record["action"];
207
+ const matcher = parseMatcher(record["matcher"]);
208
+ const enabled = asBoolean(record["enabled"]);
209
+ const updatedAt = asString(record["updated_at"]);
210
+ const createdAt = asString(record["created_at"]);
211
+ const action = actionValue === "demote" || actionValue === "sample" || actionValue === "drop"
212
+ ? actionValue
213
+ : null;
214
+ if (id === null || projectId === null || name === null || action === null || matcher === null || enabled === null || updatedAt === null || createdAt === null) {
215
+ return null;
216
+ }
217
+ const sampleRate = record["sample_rate"] === null ? null : asNumber(record["sample_rate"]);
218
+ const sampleEventClass = record["sample_event_class"] === "preserve" || record["sample_event_class"] === "context"
219
+ ? record["sample_event_class"]
220
+ : record["sample_event_class"] === null || record["sample_event_class"] === undefined
221
+ ? null
222
+ : null;
223
+ if (action === "sample" && (sampleRate === null || sampleEventClass === null)) {
224
+ return null;
225
+ }
226
+ if (action !== "sample" && (sampleRate !== null || sampleEventClass !== null)) {
227
+ return null;
228
+ }
229
+ return {
230
+ id,
231
+ project_id: projectId,
232
+ name,
233
+ description: record["description"] === null ? null : asString(record["description"]),
234
+ enabled,
235
+ action,
236
+ matcher,
237
+ sample_rate: sampleRate,
238
+ sample_event_class: sampleEventClass,
239
+ created_by_user_id: record["created_by_user_id"] === null ? null : asString(record["created_by_user_id"]),
240
+ created_from_incident_id: record["created_from_incident_id"] === null ? null : asString(record["created_from_incident_id"]),
241
+ created_from_event_id: record["created_from_event_id"] === null ? null : asString(record["created_from_event_id"]),
242
+ expires_at: record["expires_at"] === null ? null : asString(record["expires_at"]),
243
+ hit_count: asInteger(record["hit_count"]) ?? 0,
244
+ last_matched_at: record["last_matched_at"] === null ? null : asString(record["last_matched_at"]),
245
+ created_at: createdAt,
246
+ updated_at: updatedAt
247
+ };
248
+ }
249
+ export function parseRemoteCaptureRulesPayload(payload) {
250
+ const record = asRecord(payload);
251
+ if (record === null || !Array.isArray(record["capture_rules"])) {
252
+ return [];
253
+ }
254
+ return record["capture_rules"]
255
+ .map((candidate) => parseCaptureRule(candidate))
256
+ .filter((rule) => rule !== null);
257
+ }
258
+ function buildEvaluationContext(projectId, event) {
259
+ const base = {
260
+ project_id: projectId,
261
+ event_id: event.event_id,
262
+ event_type: event.event_type,
263
+ service: event.service.name,
264
+ environment: event.service.environment,
265
+ runtime: normalizeRuntime(event.service.runtime)
266
+ };
267
+ if (event.event_type === "frontend_exception") {
268
+ const payload = event.payload;
269
+ const browserEvent = typeof payload["browser_event"] === "object" && payload["browser_event"] !== null
270
+ ? payload["browser_event"]
271
+ : null;
272
+ const target = typeof browserEvent?.["target"] === "object" && browserEvent["target"] !== null
273
+ ? browserEvent["target"]
274
+ : null;
275
+ const sourceUrl = typeof target?.["source_url"] === "string"
276
+ ? target["source_url"]
277
+ : typeof browserEvent?.["file_name"] === "string"
278
+ ? browserEvent["file_name"]
279
+ : null;
280
+ const browserEventKind = browserEvent?.["kind"] === "window_error" || browserEvent?.["kind"] === "resource_error"
281
+ ? browserEvent["kind"]
282
+ : undefined;
283
+ const resourceUrl = normalizeEvaluationUrl(sourceUrl);
284
+ return {
285
+ ...base,
286
+ ...(resourceUrl.first_party === undefined ? {} : { first_party: resourceUrl.first_party }),
287
+ error_name: event.payload.name,
288
+ message: event.payload.message,
289
+ ...(browserEventKind === undefined ? {} : { browser_event_kind: browserEventKind }),
290
+ ...(resourceUrl.url === undefined ? {} : { resource_url: resourceUrl.url })
291
+ };
292
+ }
293
+ if (event.event_type === "request_event") {
294
+ const requestUrl = normalizeEvaluationUrl(event.payload.path);
295
+ return {
296
+ ...base,
297
+ first_party: requestUrl.first_party ?? true,
298
+ ...(requestUrl.url === undefined ? {} : { request_url: requestUrl.url }),
299
+ status_code: event.payload.response_status
300
+ };
301
+ }
302
+ if (event.event_type === "frontend_breadcrumb" && event.payload.breadcrumb_type === "network_request") {
303
+ const rawUrl = typeof event.payload.data["url"] === "string" ? event.payload.data["url"] : null;
304
+ const requestUrl = normalizeEvaluationUrl(rawUrl);
305
+ const statusCode = typeof event.payload.data["status_code"] === "number" ? event.payload.data["status_code"] : undefined;
306
+ return {
307
+ ...base,
308
+ ...(requestUrl.first_party === undefined ? {} : { first_party: requestUrl.first_party }),
309
+ ...(requestUrl.url === undefined ? {} : { request_url: requestUrl.url }),
310
+ ...(statusCode === undefined ? {} : { status_code: statusCode })
311
+ };
312
+ }
313
+ if (event.event_type === "log_event") {
314
+ return {
315
+ ...base,
316
+ message: event.payload.message
317
+ };
318
+ }
319
+ return base;
320
+ }
321
+ function matchesUrlMatcher(matcher, value) {
322
+ if (matcher === undefined) {
323
+ return true;
324
+ }
325
+ if (value === undefined) {
326
+ return false;
327
+ }
328
+ if (matcher.host !== undefined && value.host !== matcher.host) {
329
+ return false;
330
+ }
331
+ if (matcher.host_suffix !== undefined && (value.host === undefined || !value.host.endsWith(matcher.host_suffix))) {
332
+ return false;
333
+ }
334
+ if (matcher.path_equals !== undefined && value.path !== matcher.path_equals) {
335
+ return false;
336
+ }
337
+ if (matcher.path_prefix !== undefined && !value.path.startsWith(matcher.path_prefix)) {
338
+ return false;
339
+ }
340
+ return true;
341
+ }
342
+ function matchesRule(rule, context) {
343
+ const matcher = rule.matcher;
344
+ if (matcher.event_types !== undefined && !matcher.event_types.includes(context.event_type)) {
345
+ return false;
346
+ }
347
+ if (matcher.services !== undefined && (context.service === undefined || !matcher.services.includes(context.service))) {
348
+ return false;
349
+ }
350
+ if (matcher.environments !== undefined && (context.environment === undefined || !matcher.environments.includes(context.environment))) {
351
+ return false;
352
+ }
353
+ if (matcher.runtime !== undefined && !matcher.runtime.includes(context.runtime)) {
354
+ return false;
355
+ }
356
+ if (matcher.first_party !== undefined && matcher.first_party !== context.first_party) {
357
+ return false;
358
+ }
359
+ if (matcher.error_name !== undefined && matcher.error_name !== context.error_name) {
360
+ return false;
361
+ }
362
+ if (matcher.message_equals !== undefined && matcher.message_equals !== context.message) {
363
+ return false;
364
+ }
365
+ if (matcher.message_contains !== undefined && (context.message === undefined || !context.message.includes(matcher.message_contains))) {
366
+ return false;
367
+ }
368
+ if (matcher.browser_event_kind !== undefined && matcher.browser_event_kind !== context.browser_event_kind) {
369
+ return false;
370
+ }
371
+ if (!matchesUrlMatcher(matcher.resource_url, context.resource_url)) {
372
+ return false;
373
+ }
374
+ if (!matchesUrlMatcher(matcher.request_url, context.request_url)) {
375
+ return false;
376
+ }
377
+ if (matcher.status_codes !== undefined && (context.status_code === undefined || !matcher.status_codes.includes(context.status_code))) {
378
+ return false;
379
+ }
380
+ if (matcher.status_ranges !== undefined &&
381
+ (context.status_code === undefined ||
382
+ !matcher.status_ranges.some((range) => {
383
+ const statusCode = context.status_code;
384
+ return statusCode !== undefined && statusCode >= range.start && statusCode <= range.end;
385
+ }))) {
386
+ return false;
387
+ }
388
+ if (matcher.fingerprint !== undefined &&
389
+ (context.fingerprint === undefined ||
390
+ context.fingerprint.version !== matcher.fingerprint.version ||
391
+ context.fingerprint.value !== matcher.fingerprint.value)) {
392
+ return false;
393
+ }
394
+ return true;
395
+ }
396
+ function getSpecificityScore(rule) {
397
+ const matcher = rule.matcher;
398
+ let score = 0;
399
+ if (matcher.fingerprint !== undefined) {
400
+ score += 1000;
401
+ }
402
+ if (matcher.resource_url?.host !== undefined) {
403
+ score += 250;
404
+ }
405
+ if (matcher.request_url?.host !== undefined) {
406
+ score += 250;
407
+ }
408
+ if (matcher.resource_url?.path_equals !== undefined || matcher.request_url?.path_equals !== undefined) {
409
+ score += 200;
410
+ }
411
+ if (matcher.status_codes !== undefined) {
412
+ score += 150;
413
+ }
414
+ if (matcher.browser_event_kind !== undefined) {
415
+ score += 100;
416
+ }
417
+ if (matcher.resource_url?.host_suffix !== undefined || matcher.request_url?.host_suffix !== undefined) {
418
+ score += 90;
419
+ }
420
+ if (matcher.resource_url?.path_prefix !== undefined || matcher.request_url?.path_prefix !== undefined) {
421
+ score += 80;
422
+ }
423
+ if (matcher.error_name !== undefined) {
424
+ score += 70;
425
+ }
426
+ if (matcher.message_equals !== undefined) {
427
+ score += 60;
428
+ }
429
+ if (matcher.message_contains !== undefined) {
430
+ score += 50;
431
+ }
432
+ if (matcher.first_party !== undefined) {
433
+ score += 40;
434
+ }
435
+ if (matcher.services !== undefined) {
436
+ score += 30;
437
+ }
438
+ if (matcher.environments !== undefined) {
439
+ score += 20;
440
+ }
441
+ if (matcher.runtime !== undefined) {
442
+ score += 10;
443
+ }
444
+ if (matcher.event_types !== undefined) {
445
+ score += 5;
446
+ }
447
+ return score;
448
+ }
449
+ function compareRules(left, right) {
450
+ const specificityDifference = getSpecificityScore(right) - getSpecificityScore(left);
451
+ if (specificityDifference !== 0) {
452
+ return specificityDifference;
453
+ }
454
+ const updatedDifference = Date.parse(right.updated_at) - Date.parse(left.updated_at);
455
+ if (updatedDifference !== 0) {
456
+ return updatedDifference;
457
+ }
458
+ return left.id.localeCompare(right.id);
459
+ }
460
+ function stableUnitFloat(seed) {
461
+ let hash = 2166136261;
462
+ for (let index = 0; index < seed.length; index += 1) {
463
+ hash ^= seed.charCodeAt(index);
464
+ hash = Math.imul(hash, 16777619);
465
+ }
466
+ return (hash >>> 0) / 0x100000000;
467
+ }
468
+ function shouldSample(projectId, ruleId, eventId, sampleRate) {
469
+ if (sampleRate <= 0) {
470
+ return false;
471
+ }
472
+ if (sampleRate >= 1) {
473
+ return true;
474
+ }
475
+ return stableUnitFloat(`${projectId}:${ruleId}:${eventId}`) < sampleRate;
476
+ }
477
+ export function evaluateBrowserCaptureRulesForEvent(rules, projectId, event, now) {
478
+ const context = buildEvaluationContext(projectId, event);
479
+ const activeRules = rules
480
+ .filter((rule) => rule.enabled && (rule.expires_at === null || Date.parse(rule.expires_at) > Date.parse(now)))
481
+ .sort(compareRules);
482
+ for (const rule of activeRules) {
483
+ if (!matchesRule(rule, context)) {
484
+ continue;
485
+ }
486
+ if (rule.action === "demote") {
487
+ return {
488
+ rule_id: rule.id,
489
+ action: "demote",
490
+ outcome: "demote",
491
+ sample_rate: null,
492
+ sample_event_class: null
493
+ };
494
+ }
495
+ if (rule.action === "drop") {
496
+ return {
497
+ rule_id: rule.id,
498
+ action: "drop",
499
+ outcome: "drop",
500
+ sample_rate: null,
501
+ sample_event_class: null
502
+ };
503
+ }
504
+ const sampledIn = shouldSample(projectId, rule.id, event.event_id, rule.sample_rate ?? 0);
505
+ return {
506
+ rule_id: rule.id,
507
+ action: "sample",
508
+ outcome: sampledIn ? "sampled_in" : "sampled_out",
509
+ sample_rate: rule.sample_rate,
510
+ sample_event_class: rule.sample_event_class
511
+ };
512
+ }
513
+ return null;
514
+ }
515
+ //# sourceMappingURL=capture-rules.js.map