@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 +136 -8
- package/dist/capture-rules.d.ts +4 -0
- package/dist/capture-rules.d.ts.map +1 -0
- package/dist/capture-rules.js +515 -0
- package/dist/capture-rules.js.map +1 -0
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +84 -37
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -8
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +61 -6
- package/dist/runtime.js.map +1 -1
- package/dist/types.d.ts +81 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -1
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,17 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Browser SDK for DebugBundle.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+

|
|
6
|
+

|
|
6
7
|
|
|
7
|
-
|
|
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
|
-
|
|
10
|
+
## Installation
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
13
|
npm install @debugbundle/sdk-browser
|
|
13
14
|
```
|
|
14
15
|
|
|
15
|
-
|
|
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: "
|
|
27
|
+
service: "web",
|
|
25
28
|
environment: "production"
|
|
26
29
|
});
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
|
|
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
|
-
-
|
|
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
|