@debugbundle/sdk-browser 0.1.0-next.0

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/dist/index.js ADDED
@@ -0,0 +1,918 @@
1
+ import { redact } from "@debugbundle/redaction";
2
+ import { createEventEnvelope } from "@debugbundle/shared-types";
3
+ import { collectDeviceInfo, installConsoleHook, installNetworkHook } from "./hooks.js";
4
+ import { EventSuppressionTracker } from "./suppression.js";
5
+ import { validateBrowserTriggerToken } from "./trigger-token.js";
6
+ import { buildSelector, buildBrowserTransportRequestBody, createFetchTransport, deriveSdkConfigEndpoint, getConsoleSource, getDocumentSource, getFetchSource, getHistorySource, getLocationSource, getNavigatorSource, getWindowSource, matchesBrowserPattern, matchesStatusCodeFilter, normalizeBoolean, normalizeError, normalizeLogLevel, normalizeNetworkFilter, normalizePositiveNumber, normalizeSampleRate, normalizeTracePropagationTargets, normalizeUnknownRecord, parseIngestionProbeDirectives, parseRemoteProbeConfigPayload, resolveBrowserTransport, } from "./runtime.js";
7
+ import { DEFAULT_BATCH_SIZE, DEFAULT_FLUSH_INTERVAL_MS, DEFAULT_LOG_LEVEL, DEFAULT_MAX_BREADCRUMBS, DEFAULT_MAX_EVENTS_PER_SESSION, DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_SAMPLE_RATE, DEFAULT_SESSION_SAMPLE_RATE, LOG_LEVEL_ORDER, SDK_NAME, SDK_SCHEMA_VERSION, SDK_VERSION } from "./types.js";
8
+ export class BrowserSdk {
9
+ config = null;
10
+ bufferedEvents = [];
11
+ breadcrumbs = [];
12
+ persistentContext = {};
13
+ deviceInfo = null;
14
+ flushPromise = null;
15
+ flushTimer = null;
16
+ nextRetryAt = null;
17
+ _lastEventAt = null;
18
+ _consecutiveFailures = 0;
19
+ registeredListeners = [];
20
+ originalPushState = null;
21
+ originalReplaceState = null;
22
+ originalFetch = null;
23
+ originalXmlHttpRequest = null;
24
+ originalConsoleError = null;
25
+ originalConsoleWarn = null;
26
+ sessionSampledIn = true;
27
+ sessionEventCount = 0;
28
+ probeBuffers = new Map();
29
+ suppressionTracker = new EventSuppressionTracker();
30
+ remoteProbeState = {
31
+ probesEnabled: false,
32
+ remoteProbesEnabled: false,
33
+ directives: [],
34
+ triggerTokenKey: null
35
+ };
36
+ pendingTriggerToken = null;
37
+ activeTriggerDirective = null;
38
+ get status() {
39
+ if (this.config === null) {
40
+ return "disconnected";
41
+ }
42
+ if (this._consecutiveFailures >= 3) {
43
+ return "disconnected";
44
+ }
45
+ if (this.nextRetryAt !== null) {
46
+ return "degraded";
47
+ }
48
+ return "healthy";
49
+ }
50
+ get lastEventAt() {
51
+ return this._lastEventAt;
52
+ }
53
+ init(config) {
54
+ this.dispose();
55
+ const enabled = config.enabled ?? true;
56
+ const resolvedTransport = resolveBrowserTransport({
57
+ endpoint: config.endpoint,
58
+ projectToken: config.projectToken
59
+ });
60
+ if (!enabled || resolvedTransport.mode === "disabled" || resolvedTransport.endpoint === null) {
61
+ return;
62
+ }
63
+ this.config = {
64
+ projectToken: resolvedTransport.projectToken,
65
+ environment: config.environment?.trim() || "development",
66
+ service: config.service?.trim() || "browser-app",
67
+ enabled,
68
+ redactFields: config.redactFields ?? ["password", "secret", "token", "authorization", "cookie", "ssn", "credit_card"],
69
+ tracePropagationTargets: normalizeTracePropagationTargets(config.tracePropagationTargets),
70
+ sampleRate: normalizeSampleRate(config.sampleRate, DEFAULT_SAMPLE_RATE),
71
+ batchSize: normalizePositiveNumber(config.batchSize, DEFAULT_BATCH_SIZE),
72
+ flushInterval: normalizePositiveNumber(config.flushInterval, DEFAULT_FLUSH_INTERVAL_MS),
73
+ endpoint: resolvedTransport.endpoint,
74
+ logLevel: normalizeLogLevel(config.logLevel ?? DEFAULT_LOG_LEVEL),
75
+ maxBreadcrumbs: normalizePositiveNumber(config.maxBreadcrumbs, DEFAULT_MAX_BREADCRUMBS),
76
+ breadcrumbsOnErrorOnly: normalizeBoolean(config.breadcrumbsOnErrorOnly, true),
77
+ captureNetwork: normalizeBoolean(config.captureNetwork, true),
78
+ captureClicks: normalizeBoolean(config.captureClicks, true),
79
+ captureRouteChanges: normalizeBoolean(config.captureRouteChanges, true),
80
+ captureConsole: normalizeBoolean(config.captureConsole, false),
81
+ networkFilter: normalizeNetworkFilter(config.networkFilter),
82
+ sessionSampleRate: normalizeSampleRate(config.sessionSampleRate, DEFAULT_SESSION_SAMPLE_RATE),
83
+ maxEventsPerSession: normalizePositiveNumber(config.maxEventsPerSession, DEFAULT_MAX_EVENTS_PER_SESSION),
84
+ maxProbeLabels: normalizePositiveNumber(config.maxProbeLabels, 50),
85
+ maxProbeEntriesPerLabel: normalizePositiveNumber(config.maxProbeEntriesPerLabel, 10),
86
+ probeFlushOnError: normalizeBoolean(config.probeFlushOnError, true),
87
+ requestTimeoutMs: normalizePositiveNumber(config.requestTimeoutMs, DEFAULT_REQUEST_TIMEOUT_MS),
88
+ fetchImpl: getFetchSource(),
89
+ transport: config.transport ?? createFetchTransport(),
90
+ transportMode: resolvedTransport.mode
91
+ };
92
+ this.sessionSampledIn = this.config.sessionSampleRate >= 1 || Math.random() < this.config.sessionSampleRate;
93
+ this.sessionEventCount = 0;
94
+ this.deviceInfo = collectDeviceInfo();
95
+ this.pendingTriggerToken = this.consumeTriggerTokenFromLocation();
96
+ void this.refreshRemoteProbeConfig();
97
+ this.installBrowserHooks();
98
+ }
99
+ captureException(error, context = {}) {
100
+ const config = this.config;
101
+ if (config === null) {
102
+ return;
103
+ }
104
+ try {
105
+ const normalizedError = normalizeError(error);
106
+ const device = this.deviceInfo;
107
+ const browser = device?.browser ?? { name: "Unknown", version: "0" };
108
+ const breadcrumbs = this.consumeBreadcrumbs();
109
+ const probeData = config.probeFlushOnError ? this.consumeProbeData() : { version: 1, items: [] };
110
+ const domContext = typeof context.target?.outerHTML === "string" && context.target.outerHTML.length > 0
111
+ ? {
112
+ mode: "lightweight",
113
+ html_excerpt: context.target.outerHTML
114
+ }
115
+ : null;
116
+ const event = createEventEnvelope({
117
+ schema_version: SDK_SCHEMA_VERSION,
118
+ event_type: "frontend_exception",
119
+ ...this.getProjectTokenFields(config),
120
+ sdk_name: SDK_NAME,
121
+ sdk_version: SDK_VERSION,
122
+ service: {
123
+ name: config.service,
124
+ runtime: "browser",
125
+ framework: null,
126
+ environment: config.environment
127
+ },
128
+ occurred_at: new Date().toISOString(),
129
+ correlation: this.createCorrelation(),
130
+ payload: {
131
+ name: normalizedError.name,
132
+ message: normalizedError.message,
133
+ stack: normalizedError.stack,
134
+ route: context.route ?? this.getCurrentRoute(),
135
+ browser,
136
+ breadcrumbs,
137
+ probe_data: probeData,
138
+ device: device === null
139
+ ? null
140
+ : {
141
+ user_agent: device.user_agent,
142
+ os: device.os,
143
+ device_type: device.device_type,
144
+ screen: device.screen,
145
+ viewport: device.viewport,
146
+ device_pixel_ratio: device.device_pixel_ratio,
147
+ touch_capable: device.touch_capable,
148
+ language: device.language,
149
+ connection_type: device.connection_type,
150
+ color_scheme_preference: device.color_scheme_preference
151
+ },
152
+ dom_context: domContext
153
+ }
154
+ });
155
+ this.removeEmptyProjectToken(event, config);
156
+ this.enqueueEvent(event);
157
+ }
158
+ catch {
159
+ return;
160
+ }
161
+ }
162
+ captureError(error, context = {}) {
163
+ this.captureException(error, context);
164
+ }
165
+ captureLog(message, level, context = {}) {
166
+ const config = this.config;
167
+ if (config === null || !this.shouldCaptureNonExceptionEvent()) {
168
+ return;
169
+ }
170
+ if (LOG_LEVEL_ORDER[level] < LOG_LEVEL_ORDER[config.logLevel]) {
171
+ return;
172
+ }
173
+ try {
174
+ const attributes = redact({
175
+ ...this.persistentContext,
176
+ ...normalizeUnknownRecord(context)
177
+ }, {
178
+ sensitiveKeys: config.redactFields
179
+ }).redacted;
180
+ const event = createEventEnvelope({
181
+ schema_version: SDK_SCHEMA_VERSION,
182
+ event_type: "log_event",
183
+ ...this.getProjectTokenFields(config),
184
+ sdk_name: SDK_NAME,
185
+ sdk_version: SDK_VERSION,
186
+ service: {
187
+ name: config.service,
188
+ runtime: "browser",
189
+ framework: null,
190
+ environment: config.environment
191
+ },
192
+ occurred_at: new Date().toISOString(),
193
+ correlation: this.createCorrelation(),
194
+ payload: {
195
+ level,
196
+ message,
197
+ attributes
198
+ }
199
+ });
200
+ this.removeEmptyProjectToken(event, config);
201
+ this.enqueueEvent(event);
202
+ }
203
+ catch {
204
+ return;
205
+ }
206
+ }
207
+ captureRequest(request, response, context) {
208
+ void request;
209
+ void response;
210
+ void context;
211
+ }
212
+ captureMessage(message, level = "info", context = {}) {
213
+ this.captureLog(message, normalizeLogLevel(level), context);
214
+ }
215
+ setContext(key, value) {
216
+ const config = this.config;
217
+ if (config === null || key.trim().length === 0) {
218
+ return;
219
+ }
220
+ const redacted = redact({ [key]: value }, {
221
+ sensitiveKeys: config.redactFields
222
+ }).redacted;
223
+ this.persistentContext[key] = redacted[key] ?? null;
224
+ }
225
+ probe(label, data) {
226
+ const config = this.config;
227
+ const normalizedLabel = label.trim();
228
+ if (config === null || normalizedLabel.length === 0) {
229
+ return;
230
+ }
231
+ try {
232
+ const redacted = redact(this.normalizeProbeInput(data), {
233
+ sensitiveKeys: config.redactFields
234
+ }).redacted;
235
+ const probeData = normalizeUnknownRecord(redacted);
236
+ this.bufferProbe(normalizedLabel, probeData);
237
+ const matchingDirectives = this.getMatchingRemoteProbeDirectives(normalizedLabel, Date.now());
238
+ if (!this.sessionSampledIn || matchingDirectives.length === 0) {
239
+ return;
240
+ }
241
+ for (const directive of matchingDirectives) {
242
+ this.enqueueEvent(this.createSdkEventEnvelope(config, {
243
+ schema_version: SDK_SCHEMA_VERSION,
244
+ event_type: "probe_event",
245
+ ...this.getProjectTokenFields(config),
246
+ sdk_name: SDK_NAME,
247
+ sdk_version: SDK_VERSION,
248
+ service: {
249
+ name: config.service,
250
+ runtime: "browser",
251
+ framework: null,
252
+ environment: config.environment
253
+ },
254
+ occurred_at: new Date().toISOString(),
255
+ correlation: this.createCorrelation(),
256
+ payload: {
257
+ label: normalizedLabel,
258
+ data: probeData,
259
+ activation_id: directive.activationId,
260
+ probe_label_pattern: directive.labelPattern
261
+ }
262
+ }), false);
263
+ }
264
+ }
265
+ catch {
266
+ return;
267
+ }
268
+ }
269
+ async flush() {
270
+ const config = this.config;
271
+ if (config === null) {
272
+ return;
273
+ }
274
+ this.enqueueSuppressionAggregates();
275
+ if (this.bufferedEvents.length === 0) {
276
+ return;
277
+ }
278
+ if (this.flushPromise !== null) {
279
+ return this.flushPromise;
280
+ }
281
+ if (this.nextRetryAt !== null && Date.now() < this.nextRetryAt) {
282
+ return;
283
+ }
284
+ this.clearFlushTimer();
285
+ const events = [...this.bufferedEvents];
286
+ this.flushPromise = (async () => {
287
+ try {
288
+ const response = await config.transport({
289
+ endpoint: config.endpoint,
290
+ headers: this.getTransportHeaders(config),
291
+ events,
292
+ timeout_ms: config.requestTimeoutMs
293
+ });
294
+ if (response.status >= 200 && response.status < 300) {
295
+ this.updateRemoteProbeStateFromIngestionResponse(response.body);
296
+ this.nextRetryAt = null;
297
+ this._lastEventAt = Date.now();
298
+ this._consecutiveFailures = 0;
299
+ if (this.bufferedEvents === events || this.sameLeadingEvents(events)) {
300
+ this.bufferedEvents.splice(0, events.length);
301
+ }
302
+ return;
303
+ }
304
+ this._consecutiveFailures++;
305
+ if (response.status === 429) {
306
+ this.nextRetryAt = Date.now() + (response.retry_after_ms ?? 1_000);
307
+ }
308
+ }
309
+ catch {
310
+ this._consecutiveFailures++;
311
+ return;
312
+ }
313
+ finally {
314
+ this.flushPromise = null;
315
+ if (this.bufferedEvents.length > 0) {
316
+ const retryDelay = this.nextRetryAt === null ? undefined : Math.max(0, this.nextRetryAt - Date.now());
317
+ this.scheduleFlush(retryDelay);
318
+ }
319
+ }
320
+ })();
321
+ return this.flushPromise;
322
+ }
323
+ dispose() {
324
+ this.clearFlushTimer();
325
+ this.flushPromise = null;
326
+ this.bufferedEvents = [];
327
+ this.breadcrumbs = [];
328
+ this.probeBuffers = new Map();
329
+ this.persistentContext = {};
330
+ this.deviceInfo = null;
331
+ this.config = null;
332
+ this.sessionSampledIn = true;
333
+ this.sessionEventCount = 0;
334
+ this.nextRetryAt = null;
335
+ this._lastEventAt = null;
336
+ this._consecutiveFailures = 0;
337
+ this.suppressionTracker.reset();
338
+ this.remoteProbeState = {
339
+ probesEnabled: false,
340
+ remoteProbesEnabled: false,
341
+ directives: [],
342
+ triggerTokenKey: null
343
+ };
344
+ this.pendingTriggerToken = null;
345
+ this.activeTriggerDirective = null;
346
+ while (this.registeredListeners.length > 0) {
347
+ this.registeredListeners.pop()?.();
348
+ }
349
+ const historySource = getHistorySource();
350
+ if (this.originalPushState !== null && historySource !== null) {
351
+ historySource.pushState = this.originalPushState;
352
+ this.originalPushState = null;
353
+ }
354
+ if (this.originalReplaceState !== null && historySource !== null) {
355
+ historySource.replaceState = this.originalReplaceState;
356
+ this.originalReplaceState = null;
357
+ }
358
+ const consoleSource = getConsoleSource();
359
+ if (consoleSource !== null && this.originalConsoleError !== null) {
360
+ consoleSource.error = this.originalConsoleError;
361
+ this.originalConsoleError = null;
362
+ }
363
+ if (consoleSource !== null && this.originalConsoleWarn !== null) {
364
+ consoleSource.warn = this.originalConsoleWarn;
365
+ this.originalConsoleWarn = null;
366
+ }
367
+ if (this.originalFetch !== null) {
368
+ globalThis["fetch"] = this.originalFetch;
369
+ this.originalFetch = null;
370
+ }
371
+ if (this.originalXmlHttpRequest !== null) {
372
+ globalThis["XMLHttpRequest"] = this.originalXmlHttpRequest;
373
+ this.originalXmlHttpRequest = null;
374
+ }
375
+ }
376
+ installBrowserHooks() {
377
+ const windowSource = getWindowSource();
378
+ if (windowSource !== null) {
379
+ const onPageHide = () => {
380
+ this.flushViaBeacon();
381
+ };
382
+ const onError = (event) => {
383
+ const maybeError = normalizeUnknownRecord(event);
384
+ this.captureException(maybeError["error"] ?? maybeError["message"] ?? new Error("Window error"));
385
+ };
386
+ const onUnhandledRejection = (event) => {
387
+ const maybeError = normalizeUnknownRecord(event);
388
+ this.captureException(maybeError["reason"] ?? new Error("Unhandled promise rejection"));
389
+ };
390
+ windowSource.addEventListener("pagehide", onPageHide);
391
+ windowSource.addEventListener("error", onError);
392
+ windowSource.addEventListener("unhandledrejection", onUnhandledRejection);
393
+ this.registeredListeners.push(() => windowSource.removeEventListener("pagehide", onPageHide));
394
+ this.registeredListeners.push(() => windowSource.removeEventListener("error", onError));
395
+ this.registeredListeners.push(() => windowSource.removeEventListener("unhandledrejection", onUnhandledRejection));
396
+ }
397
+ const documentSource = getDocumentSource();
398
+ if (documentSource !== null) {
399
+ const onClick = (event) => {
400
+ if (this.config?.captureClicks !== true) {
401
+ return;
402
+ }
403
+ const target = normalizeUnknownRecord(normalizeUnknownRecord(event)["target"]);
404
+ const selector = buildSelector(target);
405
+ if (selector === null) {
406
+ return;
407
+ }
408
+ this.addBreadcrumb({
409
+ ts: new Date().toISOString(),
410
+ breadcrumb_type: "click",
411
+ data: {
412
+ selector
413
+ }
414
+ });
415
+ };
416
+ const onSubmit = (event) => {
417
+ const target = normalizeUnknownRecord(normalizeUnknownRecord(event)["target"]);
418
+ const selector = buildSelector(target) ?? "form";
419
+ const elements = Array.isArray(target["elements"]) ? target["elements"] : [];
420
+ const fieldCount = elements
421
+ .map((entry) => normalizeUnknownRecord(entry))
422
+ .filter((entry) => typeof entry["name"] === "string" && entry["name"].length > 0).length;
423
+ this.addBreadcrumb({
424
+ ts: new Date().toISOString(),
425
+ breadcrumb_type: "form_submit",
426
+ data: {
427
+ form: selector,
428
+ field_count: fieldCount
429
+ }
430
+ });
431
+ };
432
+ const onVisibilityChange = () => {
433
+ if (documentSource.visibilityState === "hidden") {
434
+ this.flushViaBeacon();
435
+ }
436
+ };
437
+ documentSource.addEventListener("click", onClick);
438
+ documentSource.addEventListener("submit", onSubmit);
439
+ documentSource.addEventListener("visibilitychange", onVisibilityChange);
440
+ this.registeredListeners.push(() => documentSource.removeEventListener("click", onClick));
441
+ this.registeredListeners.push(() => documentSource.removeEventListener("submit", onSubmit));
442
+ this.registeredListeners.push(() => documentSource.removeEventListener("visibilitychange", onVisibilityChange));
443
+ }
444
+ const historySource = getHistorySource();
445
+ if (historySource !== null) {
446
+ this.originalPushState = historySource.pushState.bind(historySource);
447
+ this.originalReplaceState = historySource.replaceState.bind(historySource);
448
+ historySource.pushState = (state, title, url) => {
449
+ this.originalPushState?.(state, title, url);
450
+ this.captureRouteChange(url);
451
+ };
452
+ historySource.replaceState = (state, title, url) => {
453
+ this.originalReplaceState?.(state, title, url);
454
+ this.captureRouteChange(url);
455
+ };
456
+ }
457
+ const consoleHooks = installConsoleHook(this.config, (breadcrumb) => {
458
+ this.addBreadcrumb(breadcrumb);
459
+ });
460
+ this.originalConsoleError = consoleHooks.originalConsoleError;
461
+ this.originalConsoleWarn = consoleHooks.originalConsoleWarn;
462
+ const networkHooks = installNetworkHook(this.config, (breadcrumb) => {
463
+ this.addBreadcrumb(breadcrumb);
464
+ }, (url, statusCode, durationMs) => this.shouldCaptureNetworkRequest(url, statusCode, durationMs), () => this.getCurrentRoute());
465
+ this.originalFetch = networkHooks.originalFetch;
466
+ this.originalXmlHttpRequest = networkHooks.originalXmlHttpRequest;
467
+ }
468
+ createCorrelation() {
469
+ return {
470
+ request_id: null,
471
+ trace_id: null,
472
+ session_id: null,
473
+ user_id_hash: null
474
+ };
475
+ }
476
+ addBreadcrumb(breadcrumb) {
477
+ const config = this.config;
478
+ if (config === null || !this.shouldCaptureBreadcrumb()) {
479
+ return;
480
+ }
481
+ if (config.breadcrumbsOnErrorOnly !== true) {
482
+ this.enqueueEvent(this.createBreadcrumbEvent(breadcrumb));
483
+ return;
484
+ }
485
+ this.breadcrumbs.push(breadcrumb);
486
+ this.sessionEventCount += 1;
487
+ while (this.breadcrumbs.length > config.maxBreadcrumbs) {
488
+ this.breadcrumbs.shift();
489
+ }
490
+ }
491
+ bufferProbe(label, data) {
492
+ const config = this.config;
493
+ if (config === null) {
494
+ return;
495
+ }
496
+ if (!this.probeBuffers.has(label) && this.probeBuffers.size >= config.maxProbeLabels) {
497
+ return;
498
+ }
499
+ const buffer = this.probeBuffers.get(label) ?? [];
500
+ buffer.push({
501
+ label,
502
+ data,
503
+ timestamp: new Date().toISOString(),
504
+ activation_id: null
505
+ });
506
+ while (buffer.length > config.maxProbeEntriesPerLabel) {
507
+ buffer.shift();
508
+ }
509
+ this.probeBuffers.set(label, buffer);
510
+ }
511
+ consumeProbeData() {
512
+ const items = Array.from(this.probeBuffers.values()).flatMap((buffer) => buffer);
513
+ this.probeBuffers.clear();
514
+ return {
515
+ version: 1,
516
+ items
517
+ };
518
+ }
519
+ consumeBreadcrumbs() {
520
+ const breadcrumbs = [...this.breadcrumbs];
521
+ this.breadcrumbs = [];
522
+ return breadcrumbs;
523
+ }
524
+ captureRouteChange(url) {
525
+ if (this.config?.captureRouteChanges !== true) {
526
+ return;
527
+ }
528
+ const route = typeof url === "string"
529
+ ? url
530
+ : url instanceof URL
531
+ ? url.pathname
532
+ : this.getCurrentRoute();
533
+ if (route === null) {
534
+ return;
535
+ }
536
+ this.addBreadcrumb({
537
+ ts: new Date().toISOString(),
538
+ breadcrumb_type: "route_change",
539
+ route,
540
+ data: {
541
+ route
542
+ }
543
+ });
544
+ }
545
+ createBreadcrumbEvent(breadcrumb) {
546
+ const config = this.config;
547
+ if (config === null) {
548
+ throw new Error("Browser SDK not initialized");
549
+ }
550
+ return this.createSdkEventEnvelope(config, {
551
+ schema_version: SDK_SCHEMA_VERSION,
552
+ event_type: "frontend_breadcrumb",
553
+ ...this.getProjectTokenFields(config),
554
+ sdk_name: SDK_NAME,
555
+ sdk_version: SDK_VERSION,
556
+ service: {
557
+ name: config.service,
558
+ runtime: "browser",
559
+ framework: null,
560
+ environment: config.environment
561
+ },
562
+ occurred_at: breadcrumb.ts,
563
+ correlation: this.createCorrelation(),
564
+ payload: {
565
+ breadcrumb_type: breadcrumb.breadcrumb_type,
566
+ route: breadcrumb.route ?? this.getCurrentRoute(),
567
+ data: breadcrumb.data
568
+ }
569
+ });
570
+ }
571
+ getCurrentRoute() {
572
+ const locationSource = getLocationSource();
573
+ if (locationSource === null) {
574
+ return null;
575
+ }
576
+ return typeof locationSource.pathname === "string" ? locationSource.pathname : null;
577
+ }
578
+ enqueueEvent(event, countTowardSession = true) {
579
+ if (!this.shouldCaptureBySampleRate(event)) {
580
+ return;
581
+ }
582
+ const suppressionKey = this.buildSuppressionKey(event);
583
+ if (suppressionKey !== null && !this.suppressionTracker.shouldCapture(suppressionKey, Date.now())) {
584
+ this.scheduleFlush();
585
+ return;
586
+ }
587
+ this.enqueueInternalEvent(event, countTowardSession);
588
+ }
589
+ enqueueInternalEvent(event, countTowardSession = true) {
590
+ const config = this.config;
591
+ if (config === null) {
592
+ return;
593
+ }
594
+ this.bufferedEvents.push(event);
595
+ if (countTowardSession && event.event_type !== "frontend_exception") {
596
+ this.sessionEventCount += 1;
597
+ }
598
+ if (this.bufferedEvents.length >= config.batchSize) {
599
+ queueMicrotask(() => {
600
+ void this.flush();
601
+ });
602
+ return;
603
+ }
604
+ this.scheduleFlush();
605
+ }
606
+ buildSuppressionKey(event) {
607
+ if (event.event_type === "frontend_exception") {
608
+ const stackFrame = event.payload.stack.split("\n")[1]?.trim() ?? null;
609
+ return JSON.stringify({
610
+ event_type: event.event_type,
611
+ name: event.payload.name,
612
+ message: event.payload.message,
613
+ stack_frame: stackFrame,
614
+ route: event.payload.route
615
+ });
616
+ }
617
+ if (event.event_type === "log_event") {
618
+ return JSON.stringify({
619
+ event_type: event.event_type,
620
+ level: event.payload.level,
621
+ message: event.payload.message,
622
+ attributes: event.payload.attributes
623
+ });
624
+ }
625
+ return null;
626
+ }
627
+ shouldCaptureBySampleRate(event) {
628
+ const config = this.config;
629
+ if (config === null) {
630
+ return false;
631
+ }
632
+ if (event.event_type === "frontend_exception" || event.event_type === "error_suppressed") {
633
+ return true;
634
+ }
635
+ return config.sampleRate >= 1 || Math.random() <= config.sampleRate;
636
+ }
637
+ scheduleFlush(delayMs) {
638
+ const config = this.config;
639
+ if (config === null) {
640
+ return;
641
+ }
642
+ if (this.flushTimer !== null) {
643
+ clearTimeout(this.flushTimer);
644
+ this.flushTimer = null;
645
+ }
646
+ this.flushTimer = setTimeout(() => {
647
+ this.flushTimer = null;
648
+ void this.flush();
649
+ }, delayMs ?? config.flushInterval);
650
+ }
651
+ clearFlushTimer() {
652
+ if (this.flushTimer !== null) {
653
+ clearTimeout(this.flushTimer);
654
+ this.flushTimer = null;
655
+ }
656
+ }
657
+ flushViaBeacon() {
658
+ const config = this.config;
659
+ const navigatorSource = getNavigatorSource();
660
+ if (config === null || this.bufferedEvents.length === 0 || navigatorSource === null) {
661
+ return;
662
+ }
663
+ const pendingEvents = [...this.bufferedEvents];
664
+ const body = buildBrowserTransportRequestBody(config.endpoint, pendingEvents);
665
+ const flushViaKeepalive = () => {
666
+ if (config.fetchImpl === null) {
667
+ void this.flush();
668
+ return;
669
+ }
670
+ void config
671
+ .fetchImpl(config.endpoint, {
672
+ method: "POST",
673
+ headers: this.getTransportHeaders(config),
674
+ body,
675
+ keepalive: true
676
+ })
677
+ .then(() => {
678
+ if (this.bufferedEvents === pendingEvents || this.sameLeadingEvents(pendingEvents)) {
679
+ this.bufferedEvents.splice(0, pendingEvents.length);
680
+ }
681
+ this.nextRetryAt = null;
682
+ this.clearFlushTimer();
683
+ })
684
+ .catch(() => {
685
+ return;
686
+ });
687
+ };
688
+ if (typeof navigatorSource.sendBeacon !== "function") {
689
+ flushViaKeepalive();
690
+ return;
691
+ }
692
+ const beaconBody = typeof Blob === "function"
693
+ ? new Blob([body], { type: "application/json" })
694
+ : body;
695
+ const accepted = navigatorSource.sendBeacon(config.endpoint, beaconBody);
696
+ if (accepted) {
697
+ this.bufferedEvents = [];
698
+ this.nextRetryAt = null;
699
+ this.clearFlushTimer();
700
+ return;
701
+ }
702
+ flushViaKeepalive();
703
+ }
704
+ sameLeadingEvents(events) {
705
+ if (this.bufferedEvents.length < events.length) {
706
+ return false;
707
+ }
708
+ return events.every((event, index) => this.bufferedEvents[index]?.event_id === event.event_id);
709
+ }
710
+ shouldCaptureNonExceptionEvent() {
711
+ const config = this.config;
712
+ if (config === null) {
713
+ return false;
714
+ }
715
+ return this.sessionSampledIn && this.sessionEventCount < config.maxEventsPerSession;
716
+ }
717
+ shouldCaptureBreadcrumb() {
718
+ return this.shouldCaptureNonExceptionEvent();
719
+ }
720
+ getProjectTokenFields(config) {
721
+ if (config.projectToken === null) {
722
+ return {};
723
+ }
724
+ return {
725
+ project_token: config.projectToken
726
+ };
727
+ }
728
+ getTransportHeaders(config) {
729
+ if (config.projectToken === null) {
730
+ return {
731
+ "content-type": "application/json"
732
+ };
733
+ }
734
+ return {
735
+ "content-type": "application/json",
736
+ authorization: `Bearer ${config.projectToken}`
737
+ };
738
+ }
739
+ createSdkEventEnvelope(config, input) {
740
+ const event = createEventEnvelope(input);
741
+ this.removeEmptyProjectToken(event, config);
742
+ return event;
743
+ }
744
+ removeEmptyProjectToken(event, config) {
745
+ if (config.projectToken !== null) {
746
+ return;
747
+ }
748
+ delete event["project_token"];
749
+ }
750
+ enqueueSuppressionAggregates() {
751
+ const config = this.config;
752
+ if (config === null) {
753
+ return;
754
+ }
755
+ for (const aggregate of this.suppressionTracker.drainAggregates(Date.now())) {
756
+ this.enqueueInternalEvent(this.createSdkEventEnvelope(config, {
757
+ schema_version: SDK_SCHEMA_VERSION,
758
+ event_type: "error_suppressed",
759
+ ...this.getProjectTokenFields(config),
760
+ sdk_name: SDK_NAME,
761
+ sdk_version: SDK_VERSION,
762
+ service: {
763
+ name: config.service,
764
+ runtime: "browser",
765
+ framework: null,
766
+ environment: config.environment
767
+ },
768
+ occurred_at: aggregate.lastSeen,
769
+ payload: {
770
+ fingerprint: aggregate.fingerprint,
771
+ suppressed_count: aggregate.suppressedCount,
772
+ window_seconds: aggregate.windowSeconds,
773
+ first_seen: aggregate.firstSeen,
774
+ last_seen: aggregate.lastSeen
775
+ }
776
+ }), false);
777
+ }
778
+ }
779
+ shouldCaptureNetworkRequest(url, statusCode, durationMs) {
780
+ const config = this.config;
781
+ if (config === null) {
782
+ return false;
783
+ }
784
+ const filter = config.networkFilter;
785
+ if (filter.urlPatterns.length > 0 && !filter.urlPatterns.some((pattern) => matchesBrowserPattern(url, pattern))) {
786
+ return false;
787
+ }
788
+ if (filter.urlDenyPatterns.some((pattern) => matchesBrowserPattern(url, pattern))) {
789
+ return false;
790
+ }
791
+ if (filter.minResponseTime !== null && durationMs < filter.minResponseTime) {
792
+ return false;
793
+ }
794
+ return matchesStatusCodeFilter(statusCode, filter.statusCodes);
795
+ }
796
+ pruneExpiredRemoteProbeDirectives(nowMs) {
797
+ const directives = this.remoteProbeState.directives.filter((directive) => Date.parse(directive.expiresAt) > nowMs);
798
+ if (this.activeTriggerDirective !== null && Date.parse(this.activeTriggerDirective.expiresAt) <= nowMs) {
799
+ this.activeTriggerDirective = null;
800
+ }
801
+ if (directives.length === this.remoteProbeState.directives.length) {
802
+ return;
803
+ }
804
+ this.remoteProbeState = {
805
+ ...this.remoteProbeState,
806
+ directives
807
+ };
808
+ }
809
+ async refreshRemoteProbeConfig() {
810
+ const config = this.config;
811
+ if (config === null || config.fetchImpl === null || config.transportMode !== "direct" || config.projectToken === null) {
812
+ return;
813
+ }
814
+ try {
815
+ const response = await config.fetchImpl(deriveSdkConfigEndpoint(config.endpoint), {
816
+ method: "GET",
817
+ headers: {
818
+ authorization: `Bearer ${config.projectToken}`
819
+ }
820
+ });
821
+ if (response.status === 304 || typeof response.json !== "function") {
822
+ return;
823
+ }
824
+ const payload = await response.json();
825
+ const parsed = parseRemoteProbeConfigPayload(payload, Date.now());
826
+ if (parsed !== null) {
827
+ this.remoteProbeState = parsed;
828
+ this.pruneExpiredRemoteProbeDirectives(Date.now());
829
+ await this.activatePendingTriggerTokenIfPossible();
830
+ }
831
+ }
832
+ catch {
833
+ return;
834
+ }
835
+ }
836
+ updateRemoteProbeStateFromIngestionResponse(payload) {
837
+ const directives = parseIngestionProbeDirectives(payload, Date.now());
838
+ if (directives === null) {
839
+ this.pruneExpiredRemoteProbeDirectives(Date.now());
840
+ return;
841
+ }
842
+ this.remoteProbeState = {
843
+ ...this.remoteProbeState,
844
+ directives
845
+ };
846
+ this.pruneExpiredRemoteProbeDirectives(Date.now());
847
+ }
848
+ consumeTriggerTokenFromLocation() {
849
+ const locationSource = getLocationSource();
850
+ const historySource = getHistorySource();
851
+ const search = typeof locationSource?.search === "string" ? locationSource.search : "";
852
+ if (search.length === 0) {
853
+ return null;
854
+ }
855
+ const params = new URLSearchParams(search.startsWith("?") ? search.slice(1) : search);
856
+ const token = params.get("_debug_probe");
857
+ if (token === null || token.length === 0) {
858
+ return null;
859
+ }
860
+ params.delete("_debug_probe");
861
+ const cleanedPath = `${locationSource?.pathname ?? ""}${params.toString().length > 0 ? `?${params.toString()}` : ""}`;
862
+ historySource?.replaceState({}, "", cleanedPath);
863
+ return token;
864
+ }
865
+ async activatePendingTriggerTokenIfPossible() {
866
+ if (this.pendingTriggerToken === null) {
867
+ return;
868
+ }
869
+ const directive = await validateBrowserTriggerToken({
870
+ token: this.pendingTriggerToken,
871
+ triggerTokenKey: this.remoteProbeState.triggerTokenKey,
872
+ nowMs: Date.now()
873
+ });
874
+ this.pendingTriggerToken = null;
875
+ this.activeTriggerDirective = directive;
876
+ }
877
+ normalizeProbeInput(data) {
878
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
879
+ return { value: data };
880
+ }
881
+ return data;
882
+ }
883
+ getMatchingRemoteProbeDirectives(label, nowMs) {
884
+ const config = this.config;
885
+ if (config === null ||
886
+ this.remoteProbeState.probesEnabled !== true ||
887
+ this.remoteProbeState.remoteProbesEnabled !== true) {
888
+ return [];
889
+ }
890
+ this.pruneExpiredRemoteProbeDirectives(nowMs);
891
+ const activeDirectives = this.activeTriggerDirective === null
892
+ ? this.remoteProbeState.directives
893
+ : [...this.remoteProbeState.directives, this.activeTriggerDirective];
894
+ return activeDirectives.filter((directive) => {
895
+ if (directive.service !== "*" && directive.service !== config.service) {
896
+ return false;
897
+ }
898
+ if (directive.environment !== "*" && directive.environment !== config.environment) {
899
+ return false;
900
+ }
901
+ return this.matchesProbeLabelPattern(directive.labelPattern, label);
902
+ });
903
+ }
904
+ matchesProbeLabelPattern(pattern, label) {
905
+ if (pattern === "*") {
906
+ return true;
907
+ }
908
+ if (pattern.endsWith(".*")) {
909
+ const prefix = pattern.slice(0, -2);
910
+ return label === prefix || label.startsWith(`${prefix}.`);
911
+ }
912
+ return pattern === label;
913
+ }
914
+ }
915
+ export function createDebugBundleBrowserSdk() {
916
+ return new BrowserSdk();
917
+ }
918
+ //# sourceMappingURL=index.js.map