@observtech/rum 0.1.32 → 0.1.34

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 CHANGED
@@ -1,14 +1,15 @@
1
- import { ATTR_EXCEPTION_MESSAGE, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_EXCEPTION_TYPE, ATTR_EXCEPTION_STACKTRACE } from '@opentelemetry/semantic-conventions';
1
+ import { registerInstrumentations } from './chunk-P6VK4P6W.js';
2
+ import { SeverityNumber } from '@opentelemetry/api-logs';
2
3
  import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
3
4
  import { LoggerProvider, BatchLogRecordProcessor } from '@opentelemetry/sdk-logs';
4
5
  import { resourceFromAttributes } from '@opentelemetry/resources';
6
+ import { ATTR_EXCEPTION_MESSAGE, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION, ATTR_EXCEPTION_TYPE, ATTR_EXCEPTION_STACKTRACE } from '@opentelemetry/semantic-conventions';
5
7
  import { ATTR_SESSION_ID } from '@opentelemetry/semantic-conventions/incubating';
6
- import { trace, metrics } from '@opentelemetry/api';
8
+ import '@opentelemetry/api';
7
9
  import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
8
10
  import { PeriodicExportingMetricReader, MeterProvider } from '@opentelemetry/sdk-metrics';
9
11
  import { onLCP, onINP, onFCP, onTTFB, onCLS } from 'web-vitals';
10
12
  import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
11
- import { logs } from '@opentelemetry/api-logs';
12
13
  import { DocumentLoadInstrumentation } from '@opentelemetry/instrumentation-document-load';
13
14
  import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
14
15
  import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
@@ -63,6 +64,57 @@ function installBaggagePropagation(getId, backendUrls, ignoreUrls = []) {
63
64
  window.fetch = originalFetch;
64
65
  };
65
66
  }
67
+
68
+ // src/consent.ts
69
+ var DEFAULT_KEY = "observ.consent";
70
+ var DEFAULT_GRANTED = ["granted", "true", "1", "yes", "all"];
71
+ function readCookie(key) {
72
+ if (typeof document === "undefined" || !document.cookie) return void 0;
73
+ for (const part of document.cookie.split(";")) {
74
+ const idx = part.indexOf("=");
75
+ if (idx === -1) continue;
76
+ if (part.slice(0, idx).trim() === key) {
77
+ try {
78
+ return decodeURIComponent(part.slice(idx + 1).trim());
79
+ } catch {
80
+ return part.slice(idx + 1).trim();
81
+ }
82
+ }
83
+ }
84
+ return void 0;
85
+ }
86
+ function readStorage(key) {
87
+ try {
88
+ return globalThis.localStorage?.getItem(key) ?? void 0;
89
+ } catch {
90
+ return void 0;
91
+ }
92
+ }
93
+ function consentKey(opts) {
94
+ return opts.consentKey || DEFAULT_KEY;
95
+ }
96
+ function grantedValue(opts) {
97
+ return opts.consentGrantedValues?.[0] ?? "granted";
98
+ }
99
+ function hasAnalyticsConsent(opts) {
100
+ if (!opts.requireConsent) return true;
101
+ const key = consentKey(opts);
102
+ const granted = (opts.consentGrantedValues ?? DEFAULT_GRANTED).map((v) => v.toLowerCase());
103
+ const value = readCookie(key) ?? readStorage(key);
104
+ return value != null && granted.includes(value.toLowerCase());
105
+ }
106
+ function writeConsent(opts) {
107
+ try {
108
+ globalThis.localStorage?.setItem(consentKey(opts), grantedValue(opts));
109
+ } catch {
110
+ }
111
+ }
112
+ function clearConsent(opts) {
113
+ try {
114
+ globalThis.localStorage?.removeItem(consentKey(opts));
115
+ } catch {
116
+ }
117
+ }
66
118
  function buildResource(options) {
67
119
  const attrs = {
68
120
  "telemetry.sdk.language": "webjs"
@@ -81,11 +133,8 @@ function buildResource(options) {
81
133
  }
82
134
  var SESSION_ID_ATTRIBUTE = ATTR_SESSION_ID;
83
135
 
84
- // src/session.ts
85
- var SESSION_STORAGE_KEY = "observ.rum.session";
86
- var SESSION_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1e3;
87
- var current = null;
88
- function generateSessionId() {
136
+ // src/random-id.ts
137
+ function generateUuid() {
89
138
  const c = globalThis.crypto;
90
139
  if (c && typeof c.randomUUID === "function") {
91
140
  try {
@@ -106,16 +155,58 @@ function generateSessionId() {
106
155
  return v.toString(16);
107
156
  });
108
157
  }
158
+
159
+ // src/session.ts
160
+ var SESSION_STORAGE_KEY = "observ.rum.session";
161
+ var SESSION_INACTIVITY_TIMEOUT_MS = 30 * 60 * 1e3;
162
+ var SESSION_MAX_DURATION_MS = 4 * 60 * 60 * 1e3;
163
+ var current = null;
164
+ var inactivityTimeoutMs = SESSION_INACTIVITY_TIMEOUT_MS;
165
+ var maxDurationMs = SESSION_MAX_DURATION_MS;
166
+ var storageMode = "session";
167
+ var listeners = /* @__PURE__ */ new Set();
168
+ function configureSession(opts) {
169
+ if (typeof opts.inactivityTimeoutMs === "number" && opts.inactivityTimeoutMs > 0) {
170
+ inactivityTimeoutMs = opts.inactivityTimeoutMs;
171
+ }
172
+ if (typeof opts.maxDurationMs === "number" && opts.maxDurationMs > 0) {
173
+ maxDurationMs = opts.maxDurationMs;
174
+ }
175
+ if (typeof opts.crossTab === "boolean") {
176
+ storageMode = opts.crossTab ? "local" : "session";
177
+ }
178
+ }
179
+ function addSessionListener(fn) {
180
+ listeners.add(fn);
181
+ return () => {
182
+ listeners.delete(fn);
183
+ };
184
+ }
185
+ function notify(event) {
186
+ for (const fn of listeners) {
187
+ try {
188
+ fn(event);
189
+ } catch {
190
+ }
191
+ }
192
+ }
193
+ function backend() {
194
+ try {
195
+ return storageMode === "local" ? globalThis.localStorage : globalThis.sessionStorage;
196
+ } catch {
197
+ return void 0;
198
+ }
199
+ }
109
200
  function safeRead() {
110
201
  try {
111
- return globalThis.sessionStorage?.getItem(SESSION_STORAGE_KEY) ?? null;
202
+ return backend()?.getItem(SESSION_STORAGE_KEY) ?? null;
112
203
  } catch {
113
204
  return null;
114
205
  }
115
206
  }
116
207
  function safeWrite(value) {
117
208
  try {
118
- globalThis.sessionStorage?.setItem(SESSION_STORAGE_KEY, value);
209
+ backend()?.setItem(SESSION_STORAGE_KEY, value);
119
210
  } catch {
120
211
  }
121
212
  }
@@ -139,22 +230,110 @@ function save(session) {
139
230
  current = session;
140
231
  safeWrite(JSON.stringify(session));
141
232
  }
233
+ function isExpired(session, now) {
234
+ if (!session) return true;
235
+ const inactiveFor = Math.max(0, now - session.lastActivityAt);
236
+ const ageFor = Math.max(0, now - session.startedAt);
237
+ return inactiveFor > inactivityTimeoutMs || ageFor > maxDurationMs;
238
+ }
142
239
  function ensureSession(now = Date.now()) {
143
240
  const existing = load();
144
- if (existing) {
145
- const elapsed = Math.max(0, now - existing.lastActivityAt);
146
- if (elapsed <= SESSION_INACTIVITY_TIMEOUT_MS) {
147
- save({ ...existing, lastActivityAt: now });
148
- return existing.id;
149
- }
241
+ if (existing && !isExpired(existing, now)) {
242
+ save({ ...existing, lastActivityAt: now });
243
+ return existing.id;
244
+ }
245
+ if (existing && existing.endedAt == null) {
246
+ notify({ type: "end", id: existing.id, durationMs: Math.max(0, now - existing.startedAt) });
150
247
  }
151
- const fresh = { id: generateSessionId(), startedAt: now, lastActivityAt: now };
248
+ const fresh = {
249
+ id: generateUuid(),
250
+ startedAt: now,
251
+ lastActivityAt: now,
252
+ ...existing ? { previousId: existing.id } : {}
253
+ };
152
254
  save(fresh);
255
+ notify({ type: "start", id: fresh.id, ...existing ? { previousId: existing.id } : {} });
153
256
  return fresh.id;
154
257
  }
155
258
  function getSessionId(now = Date.now()) {
156
259
  return ensureSession(now);
157
260
  }
261
+ function touchSession(now = Date.now()) {
262
+ ensureSession(now);
263
+ }
264
+ function endSessionIfExpired(now = Date.now()) {
265
+ const session = load();
266
+ if (!session || session.endedAt != null || !isExpired(session, now)) return;
267
+ save({ ...session, endedAt: now });
268
+ notify({ type: "end", id: session.id, durationMs: Math.max(0, now - session.startedAt) });
269
+ }
270
+ function adoptStoredSession() {
271
+ const raw = safeRead();
272
+ if (raw === null) return;
273
+ try {
274
+ const parsed = JSON.parse(raw);
275
+ if (isPersistedSession(parsed)) current = parsed;
276
+ } catch {
277
+ }
278
+ }
279
+
280
+ // src/user.ts
281
+ var ATTR_ENDUSER_ID = "enduser.id";
282
+ var ATTR_ENDUSER_ROLE = "enduser.role";
283
+ var ATTR_ENDUSER_NAME = "enduser.name";
284
+ var ATTR_ENDUSER_PSEUDO_ID = "enduser.pseudo.id";
285
+ var PSEUDO_USER_STORAGE_KEY = "observ.rum.pseudo";
286
+ var currentUser = null;
287
+ var pseudoEnabled = true;
288
+ var pseudoId;
289
+ function configureUser(opts) {
290
+ if (typeof opts.disablePseudoUser === "boolean") {
291
+ pseudoEnabled = !opts.disablePseudoUser;
292
+ }
293
+ }
294
+ function setUser(user) {
295
+ currentUser = user;
296
+ }
297
+ function clearUser() {
298
+ currentUser = null;
299
+ }
300
+ function getPseudoUserId() {
301
+ if (!pseudoEnabled) return void 0;
302
+ if (pseudoId) return pseudoId;
303
+ try {
304
+ const existing = globalThis.localStorage?.getItem(PSEUDO_USER_STORAGE_KEY);
305
+ if (existing) {
306
+ pseudoId = existing;
307
+ return pseudoId;
308
+ }
309
+ } catch {
310
+ }
311
+ pseudoId = generateUuid();
312
+ try {
313
+ globalThis.localStorage?.setItem(PSEUDO_USER_STORAGE_KEY, pseudoId);
314
+ } catch {
315
+ }
316
+ return pseudoId;
317
+ }
318
+ function userAttributes() {
319
+ const attrs = {};
320
+ const pseudo = getPseudoUserId();
321
+ if (pseudo) attrs[ATTR_ENDUSER_PSEUDO_ID] = pseudo;
322
+ if (currentUser) {
323
+ attrs[ATTR_ENDUSER_ID] = String(currentUser.id);
324
+ if (currentUser.role) attrs[ATTR_ENDUSER_ROLE] = currentUser.role;
325
+ if (currentUser.name) attrs[ATTR_ENDUSER_NAME] = currentUser.name;
326
+ }
327
+ return attrs;
328
+ }
329
+ function applyUserAttributes(setAttribute) {
330
+ try {
331
+ for (const [key, value] of Object.entries(userAttributes())) {
332
+ setAttribute(key, value);
333
+ }
334
+ } catch {
335
+ }
336
+ }
158
337
 
159
338
  // src/otel-logs.ts
160
339
  function buildLogsUrl(endpoint) {
@@ -200,12 +379,17 @@ function currentSessionId() {
200
379
  }
201
380
  return cachedSessionId;
202
381
  }
382
+ function resetSessionIdCache() {
383
+ cachedSessionId = "";
384
+ cachedSessionIdAt = 0;
385
+ }
203
386
  function emitSessionEvent(logger, eventName, attrs = {}) {
204
387
  try {
205
388
  logger.emit({
206
389
  attributes: {
207
390
  "event.name": eventName,
208
391
  [SESSION_ID_ATTRIBUTE]: currentSessionId(),
392
+ ...userAttributes(),
209
393
  ...attrs
210
394
  }
211
395
  });
@@ -213,6 +397,20 @@ function emitSessionEvent(logger, eventName, attrs = {}) {
213
397
  console.warn("[observ] semantic event emit failed", err);
214
398
  }
215
399
  }
400
+ function emitLogRecord(logger, severityNumber, severityText, body) {
401
+ try {
402
+ logger.emit({
403
+ severityNumber,
404
+ severityText,
405
+ body,
406
+ attributes: {
407
+ [SESSION_ID_ATTRIBUTE]: currentSessionId(),
408
+ ...userAttributes()
409
+ }
410
+ });
411
+ } catch {
412
+ }
413
+ }
216
414
  async function shutdownOtelLogs() {
217
415
  const p = provider;
218
416
  provider = null;
@@ -220,7 +418,84 @@ async function shutdownOtelLogs() {
220
418
  await p?.shutdown();
221
419
  }
222
420
 
223
- // src/js-errors.ts
421
+ // src/console-forward.ts
422
+ var LEVELS = {
423
+ debug: { number: SeverityNumber.DEBUG, text: "DEBUG" },
424
+ log: { number: SeverityNumber.INFO, text: "INFO" },
425
+ info: { number: SeverityNumber.INFO, text: "INFO" },
426
+ warn: { number: SeverityNumber.WARN, text: "WARN" },
427
+ error: { number: SeverityNumber.ERROR, text: "ERROR" }
428
+ };
429
+ var ALL_LEVELS = Object.keys(LEVELS);
430
+ var MAX_BODY = 4096;
431
+ var NOOP = { stop: () => {
432
+ } };
433
+ var active = null;
434
+ function resolveLevels(forwardConsole) {
435
+ if (forwardConsole === true) return ALL_LEVELS;
436
+ if (Array.isArray(forwardConsole)) return forwardConsole.filter((l) => l in LEVELS);
437
+ return [];
438
+ }
439
+ function formatArg(arg) {
440
+ if (arg instanceof Error) return `${arg.name}: ${arg.message}`;
441
+ if (typeof arg === "object" && arg !== null) {
442
+ try {
443
+ return JSON.stringify(arg) ?? String(arg);
444
+ } catch {
445
+ return String(arg);
446
+ }
447
+ }
448
+ return String(arg);
449
+ }
450
+ function formatBody(args) {
451
+ const body = args.map(formatArg).join(" ");
452
+ return body.length > MAX_BODY ? body.slice(0, MAX_BODY) : body;
453
+ }
454
+ function startConsoleForward(options) {
455
+ if (active) return active;
456
+ const levels = resolveLevels(options.forwardConsole);
457
+ if (levels.length === 0 || typeof console === "undefined") return NOOP;
458
+ let logger;
459
+ try {
460
+ logger = setupOtelLogs(options);
461
+ } catch {
462
+ return NOOP;
463
+ }
464
+ let emitting = false;
465
+ const originals = /* @__PURE__ */ new Map();
466
+ for (const level of levels) {
467
+ const original = console[level];
468
+ if (typeof original !== "function") continue;
469
+ originals.set(level, original);
470
+ const { number, text } = LEVELS[level];
471
+ console[level] = (...args) => {
472
+ if (!emitting) {
473
+ emitting = true;
474
+ try {
475
+ emitLogRecord(logger, number, text, formatBody(args));
476
+ } catch {
477
+ } finally {
478
+ emitting = false;
479
+ }
480
+ }
481
+ original.apply(console, args);
482
+ };
483
+ }
484
+ let stopped = false;
485
+ const handle = {
486
+ stop() {
487
+ if (stopped) return;
488
+ stopped = true;
489
+ active = null;
490
+ for (const [level, original] of originals) {
491
+ console[level] = original;
492
+ }
493
+ originals.clear();
494
+ }
495
+ };
496
+ active = handle;
497
+ return handle;
498
+ }
224
499
  var MAX_JS_ERRORS = 50;
225
500
  var RATE_WINDOW_MS = 6e4;
226
501
  var MAX_MESSAGE = 1024;
@@ -236,21 +511,21 @@ function reasonToMessage(reason) {
236
511
  }
237
512
  return String(reason);
238
513
  }
239
- var NOOP = { stop: () => {
514
+ var NOOP2 = { stop: () => {
240
515
  } };
241
- var active = null;
516
+ var active2 = null;
242
517
  function truncate(s, max) {
243
518
  return s.length > max ? s.slice(0, max) : s;
244
519
  }
245
520
  function startJsErrorCapture(options) {
246
- if (active) return active;
247
- if (typeof window === "undefined") return NOOP;
521
+ if (active2) return active2;
522
+ if (typeof window === "undefined") return NOOP2;
248
523
  let logger;
249
524
  try {
250
525
  logger = setupOtelLogs(options);
251
526
  } catch (err) {
252
527
  console.warn("[observ] js-errors: logs setup failed", err);
253
- return NOOP;
528
+ return NOOP2;
254
529
  }
255
530
  let windowStart = 0;
256
531
  let inWindow = 0;
@@ -293,10 +568,10 @@ function startJsErrorCapture(options) {
293
568
  stop() {
294
569
  window.removeEventListener("error", onError);
295
570
  window.removeEventListener("unhandledrejection", onRejection);
296
- active = null;
571
+ active2 = null;
297
572
  }
298
573
  };
299
- active = handle;
574
+ active2 = handle;
300
575
  return handle;
301
576
  }
302
577
  var METRIC_EXPORT_INTERVAL_MS = 3e4;
@@ -378,40 +653,6 @@ async function shutdownOtelMetrics() {
378
653
  provider2 = null;
379
654
  await p?.shutdown();
380
655
  }
381
-
382
- // node_modules/@opentelemetry/instrumentation/build/esm/autoLoaderUtils.js
383
- function enableInstrumentations(instrumentations, tracerProvider, meterProvider, loggerProvider) {
384
- for (let i = 0, j = instrumentations.length; i < j; i++) {
385
- const instrumentation = instrumentations[i];
386
- if (tracerProvider) {
387
- instrumentation.setTracerProvider(tracerProvider);
388
- }
389
- if (meterProvider) {
390
- instrumentation.setMeterProvider(meterProvider);
391
- }
392
- if (loggerProvider && instrumentation.setLoggerProvider) {
393
- instrumentation.setLoggerProvider(loggerProvider);
394
- }
395
- if (!instrumentation.getConfig().enabled) {
396
- instrumentation.enable();
397
- }
398
- }
399
- }
400
- function disableInstrumentations(instrumentations) {
401
- instrumentations.forEach((instrumentation) => instrumentation.disable());
402
- }
403
-
404
- // node_modules/@opentelemetry/instrumentation/build/esm/autoLoader.js
405
- function registerInstrumentations(options) {
406
- const tracerProvider = options.tracerProvider || trace.getTracerProvider();
407
- const meterProvider = options.meterProvider || metrics.getMeterProvider();
408
- const loggerProvider = options.loggerProvider || logs.getLoggerProvider();
409
- const instrumentations = options.instrumentations?.flat() ?? [];
410
- enableInstrumentations(instrumentations, tracerProvider, meterProvider, loggerProvider);
411
- return () => {
412
- disableInstrumentations(instrumentations);
413
- };
414
- }
415
656
  function buildSampler(options) {
416
657
  const rate = options.sampleRate;
417
658
  if (typeof rate === "number" && Number.isFinite(rate) && rate >= 0 && rate < 1) {
@@ -450,9 +691,37 @@ var SessionAttributeSpanProcessor = class {
450
691
  return Promise.resolve();
451
692
  }
452
693
  };
694
+ var UserAttributeSpanProcessor = class {
695
+ onStart(span) {
696
+ applyUserAttributes((key, value) => span.setAttribute(key, value));
697
+ }
698
+ onEnd(_span) {
699
+ }
700
+ forceFlush() {
701
+ return Promise.resolve();
702
+ }
703
+ shutdown() {
704
+ return Promise.resolve();
705
+ }
706
+ };
453
707
  var provider3 = null;
454
- var disableInstrumentations2 = null;
708
+ var disableInstrumentations = null;
455
709
  var activeConfig = null;
710
+ var userInteractionReady = null;
711
+ function buildBaseInstrumentations(options, ignoreUrls) {
712
+ return [
713
+ // Page-load timing: documentLoad / documentFetch / resourceFetch spans.
714
+ new DocumentLoadInstrumentation(),
715
+ new FetchInstrumentation({
716
+ propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,
717
+ ignoreUrls
718
+ }),
719
+ new XMLHttpRequestInstrumentation({
720
+ propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,
721
+ ignoreUrls
722
+ })
723
+ ];
724
+ }
456
725
  function setupOtelRum(options) {
457
726
  if (provider3) {
458
727
  if (activeConfig && (activeConfig.endpoint !== options.endpoint || activeConfig.key !== options.key)) {
@@ -476,28 +745,38 @@ function setupOtelRum(options) {
476
745
  const p = new WebTracerProvider({
477
746
  resource: buildResource(options),
478
747
  sampler: buildSampler(options),
479
- spanProcessors: [new SessionAttributeSpanProcessor(), new BatchSpanProcessor(exporter)]
748
+ spanProcessors: [
749
+ new SessionAttributeSpanProcessor(),
750
+ new UserAttributeSpanProcessor(),
751
+ new BatchSpanProcessor(exporter)
752
+ ]
480
753
  });
754
+ const base = buildBaseInstrumentations(options, ignoreUrls);
755
+ if (options.userInteraction) {
756
+ provider3 = p;
757
+ activeConfig = { endpoint: options.endpoint, key: options.key };
758
+ userInteractionReady = import('./user-interaction-HZJLQRUN.js').then(({ setupUserInteraction }) => {
759
+ if (provider3 !== p) return;
760
+ disableInstrumentations = setupUserInteraction(p, base);
761
+ }).catch((err) => {
762
+ console.warn("[observ] user-interaction setup failed; tracing degraded.", err);
763
+ if (provider3 === p) {
764
+ provider3 = null;
765
+ activeConfig = null;
766
+ void p.shutdown();
767
+ }
768
+ });
769
+ return;
770
+ }
481
771
  try {
482
772
  p.register();
483
- disableInstrumentations2 = registerInstrumentations({
773
+ disableInstrumentations = registerInstrumentations({
484
774
  tracerProvider: p,
485
- instrumentations: [
486
- // Page-load timing: documentLoad / documentFetch / resourceFetch spans.
487
- new DocumentLoadInstrumentation(),
488
- new FetchInstrumentation({
489
- propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,
490
- ignoreUrls
491
- }),
492
- new XMLHttpRequestInstrumentation({
493
- propagateTraceHeaderCorsUrls: options.propagateTraceHeaderCorsUrls,
494
- ignoreUrls
495
- })
496
- ]
775
+ instrumentations: base
497
776
  });
498
777
  } catch (err) {
499
- disableInstrumentations2?.();
500
- disableInstrumentations2 = null;
778
+ disableInstrumentations?.();
779
+ disableInstrumentations = null;
501
780
  void p.shutdown();
502
781
  throw err;
503
782
  }
@@ -505,17 +784,147 @@ function setupOtelRum(options) {
505
784
  activeConfig = { endpoint: options.endpoint, key: options.key };
506
785
  }
507
786
  async function shutdownOtelRum() {
787
+ const pending = userInteractionReady;
788
+ userInteractionReady = null;
789
+ if (pending) {
790
+ try {
791
+ await pending;
792
+ } catch {
793
+ }
794
+ }
508
795
  const p = provider3;
509
796
  try {
510
- disableInstrumentations2?.();
797
+ disableInstrumentations?.();
511
798
  } finally {
512
- disableInstrumentations2 = null;
799
+ disableInstrumentations = null;
513
800
  provider3 = null;
514
801
  activeConfig = null;
515
802
  await p?.shutdown();
516
803
  }
517
804
  }
518
805
 
806
+ // src/history-nav.ts
807
+ var subscribers = /* @__PURE__ */ new Set();
808
+ var patched = false;
809
+ var originalPush = null;
810
+ var originalReplace = null;
811
+ var popStateHandler = null;
812
+ function notify2(type) {
813
+ for (const fn of subscribers) {
814
+ try {
815
+ fn({ type });
816
+ } catch {
817
+ }
818
+ }
819
+ }
820
+ function patch() {
821
+ if (patched || typeof window === "undefined" || typeof history === "undefined") return;
822
+ try {
823
+ originalPush = history.pushState;
824
+ originalReplace = history.replaceState;
825
+ history.pushState = function(...args) {
826
+ originalPush?.apply(this, args);
827
+ notify2("pushState");
828
+ };
829
+ history.replaceState = function(...args) {
830
+ originalReplace?.apply(this, args);
831
+ notify2("replaceState");
832
+ };
833
+ } catch {
834
+ if (originalPush) history.pushState = originalPush;
835
+ if (originalReplace) history.replaceState = originalReplace;
836
+ originalPush = null;
837
+ originalReplace = null;
838
+ }
839
+ popStateHandler = () => notify2("popstate");
840
+ window.addEventListener("popstate", popStateHandler);
841
+ patched = true;
842
+ }
843
+ function unpatch() {
844
+ if (!patched) return;
845
+ try {
846
+ if (originalPush) history.pushState = originalPush;
847
+ if (originalReplace) history.replaceState = originalReplace;
848
+ if (popStateHandler) window.removeEventListener("popstate", popStateHandler);
849
+ } catch {
850
+ }
851
+ originalPush = null;
852
+ originalReplace = null;
853
+ popStateHandler = null;
854
+ patched = false;
855
+ }
856
+ function onHistoryNavigation(listener) {
857
+ if (typeof window === "undefined") return () => {
858
+ };
859
+ subscribers.add(listener);
860
+ patch();
861
+ return () => {
862
+ subscribers.delete(listener);
863
+ if (subscribers.size === 0) unpatch();
864
+ };
865
+ }
866
+
867
+ // src/page-views.ts
868
+ var PAGE_VIEW_EVENT = "app.page_view";
869
+ var NOOP3 = { stop: () => {
870
+ } };
871
+ var active3 = null;
872
+ function sanitizePath(path) {
873
+ return (path.split("?")[0] ?? path).split("#")[0] ?? "";
874
+ }
875
+ function nowMs() {
876
+ return typeof performance !== "undefined" && typeof performance.now === "function" ? performance.now() : Date.now();
877
+ }
878
+ function startPageViews(options) {
879
+ if (active3) return active3;
880
+ if (typeof window === "undefined" || typeof document === "undefined") return NOOP3;
881
+ let logger;
882
+ try {
883
+ logger = setupOtelLogs(options);
884
+ } catch {
885
+ return NOOP3;
886
+ }
887
+ let currentPath = sanitizePath(typeof location !== "undefined" ? location.pathname : "/");
888
+ let enteredAt = nowMs();
889
+ try {
890
+ const loadAttrs = {
891
+ "url.path": currentPath,
892
+ "app.page_view.navigation_type": "load"
893
+ };
894
+ if (document.referrer) loadAttrs["app.page_view.referrer"] = document.referrer;
895
+ emitSessionEvent(logger, PAGE_VIEW_EVENT, loadAttrs);
896
+ } catch {
897
+ }
898
+ const unsubscribe = onHistoryNavigation(() => {
899
+ try {
900
+ const toPath = sanitizePath(typeof location !== "undefined" ? location.pathname : currentPath);
901
+ touchSession();
902
+ if (toPath === currentPath) return;
903
+ const now = nowMs();
904
+ emitSessionEvent(logger, PAGE_VIEW_EVENT, {
905
+ "url.path": toPath,
906
+ "app.page_view.referrer_path": currentPath,
907
+ "app.page_view.time_on_previous_ms": String(Math.round(now - enteredAt)),
908
+ "app.page_view.navigation_type": "spa"
909
+ });
910
+ currentPath = toPath;
911
+ enteredAt = now;
912
+ } catch {
913
+ }
914
+ });
915
+ let stopped = false;
916
+ const handle = {
917
+ stop() {
918
+ if (stopped) return;
919
+ stopped = true;
920
+ active3 = null;
921
+ unsubscribe();
922
+ }
923
+ };
924
+ active3 = handle;
925
+ return handle;
926
+ }
927
+
519
928
  // src/privacy.ts
520
929
  var DEFAULT_MASK_TEXT_CLASS = "observ-mask";
521
930
  var DEFAULT_BLOCK_CLASS = "observ-block";
@@ -704,22 +1113,22 @@ function elementText(el) {
704
1113
  var RAGE_CLICK_WINDOW_MS = 1e3;
705
1114
  var RAGE_CLICK_THRESHOLD = 3;
706
1115
  var MAX_TRACKED_SELECTORS = 100;
707
- var active2 = null;
708
- var NOOP2 = { stop: () => Promise.resolve() };
1116
+ var active4 = null;
1117
+ var NOOP4 = { stop: () => Promise.resolve() };
709
1118
  function toElement(target) {
710
1119
  if (target instanceof Element) return target;
711
1120
  const node = target;
712
1121
  return node && node.parentElement ? node.parentElement : null;
713
1122
  }
714
1123
  function startSemanticEvents(options) {
715
- if (active2) return active2;
716
- if (typeof document === "undefined" || typeof window === "undefined") return NOOP2;
1124
+ if (active4) return active4;
1125
+ if (typeof document === "undefined" || typeof window === "undefined") return NOOP4;
717
1126
  let logger;
718
1127
  try {
719
1128
  logger = setupOtelLogs(options);
720
1129
  } catch (err) {
721
1130
  console.warn("[observ] semantic events: logs setup failed", err);
722
- return NOOP2;
1131
+ return NOOP4;
723
1132
  }
724
1133
  const clickTimes = /* @__PURE__ */ new Map();
725
1134
  const onClick = (event) => {
@@ -744,45 +1153,124 @@ function startSemanticEvents(options) {
744
1153
  } catch {
745
1154
  }
746
1155
  };
747
- const emitNavigate = () => {
1156
+ document.addEventListener("click", onClick, { capture: true });
1157
+ const unsubscribeNav = onHistoryNavigation(() => {
748
1158
  emitSessionEvent(logger, "observ.session.navigate", { url: location.href });
1159
+ });
1160
+ let stopped = false;
1161
+ const handle = {
1162
+ async stop() {
1163
+ if (stopped) return;
1164
+ stopped = true;
1165
+ active4 = null;
1166
+ document.removeEventListener("click", onClick, { capture: true });
1167
+ unsubscribeNav();
1168
+ }
749
1169
  };
750
- const onPopState = () => emitNavigate();
751
- document.addEventListener("click", onClick, { capture: true });
752
- window.addEventListener("popstate", onPopState);
753
- const origPushState = history.pushState;
754
- const origReplaceState = history.replaceState;
755
- let historyPatched = false;
1170
+ active4 = handle;
1171
+ return handle;
1172
+ }
1173
+
1174
+ // src/session-lifecycle.ts
1175
+ var SESSION_START_EVENT = "session.start";
1176
+ var SESSION_END_EVENT = "session.end";
1177
+ var SWEEP_INTERVAL_MS = 6e4;
1178
+ var MOUSEMOVE_THROTTLE_MS = 1e3;
1179
+ var ACTIVITY_EVENTS = ["click", "keydown", "scroll"];
1180
+ var active5 = null;
1181
+ function emitLifecycle(logger, event) {
1182
+ if (event.type === "start") {
1183
+ resetSessionIdCache();
1184
+ const attrs = { [SESSION_ID_ATTRIBUTE]: event.id };
1185
+ if (event.previousId) attrs["session.previous_id"] = event.previousId;
1186
+ emitSessionEvent(logger, SESSION_START_EVENT, attrs);
1187
+ } else {
1188
+ emitSessionEvent(logger, SESSION_END_EVENT, {
1189
+ [SESSION_ID_ATTRIBUTE]: event.id,
1190
+ "session.duration_ms": String(event.durationMs)
1191
+ });
1192
+ }
1193
+ }
1194
+ function startSessionLifecycle(options) {
1195
+ if (active5) return active5;
1196
+ let logger = null;
756
1197
  try {
757
- history.pushState = function(...args) {
758
- origPushState.apply(this, args);
759
- emitNavigate();
760
- };
761
- history.replaceState = function(...args) {
762
- origReplaceState.apply(this, args);
763
- emitNavigate();
1198
+ logger = setupOtelLogs(options);
1199
+ } catch (err) {
1200
+ console.warn("[observ] session lifecycle: logs setup failed; events disabled.", err);
1201
+ }
1202
+ const unsubscribe = addSessionListener((event) => {
1203
+ if (logger) emitLifecycle(logger, event);
1204
+ });
1205
+ if (typeof window === "undefined" || typeof document === "undefined") {
1206
+ const handle2 = {
1207
+ stop() {
1208
+ unsubscribe();
1209
+ active5 = null;
1210
+ }
764
1211
  };
765
- historyPatched = true;
766
- } catch {
1212
+ active5 = handle2;
1213
+ return handle2;
767
1214
  }
1215
+ const onActivity = () => {
1216
+ try {
1217
+ touchSession();
1218
+ } catch {
1219
+ }
1220
+ };
1221
+ let lastMove = 0;
1222
+ const onMouseMove = () => {
1223
+ const now = Date.now();
1224
+ if (now - lastMove < MOUSEMOVE_THROTTLE_MS) return;
1225
+ lastMove = now;
1226
+ onActivity();
1227
+ };
1228
+ for (const evt of ACTIVITY_EVENTS) {
1229
+ window.addEventListener(evt, onActivity, { passive: true });
1230
+ }
1231
+ window.addEventListener("mousemove", onMouseMove, { passive: true });
1232
+ const onHide = () => {
1233
+ try {
1234
+ endSessionIfExpired();
1235
+ } catch {
1236
+ }
1237
+ };
1238
+ const onVisibility = () => {
1239
+ if (document.visibilityState === "hidden") onHide();
1240
+ };
1241
+ window.addEventListener("pagehide", onHide);
1242
+ document.addEventListener("visibilitychange", onVisibility);
1243
+ const onStorage = (e) => {
1244
+ if (e.key !== SESSION_STORAGE_KEY || e.newValue == null) return;
1245
+ try {
1246
+ adoptStoredSession();
1247
+ resetSessionIdCache();
1248
+ } catch {
1249
+ }
1250
+ };
1251
+ window.addEventListener("storage", onStorage);
1252
+ const sweepTimer = setInterval(() => {
1253
+ try {
1254
+ endSessionIfExpired();
1255
+ } catch {
1256
+ }
1257
+ }, SWEEP_INTERVAL_MS);
768
1258
  let stopped = false;
769
1259
  const handle = {
770
- async stop() {
1260
+ stop() {
771
1261
  if (stopped) return;
772
1262
  stopped = true;
773
- active2 = null;
774
- document.removeEventListener("click", onClick, { capture: true });
775
- window.removeEventListener("popstate", onPopState);
776
- if (historyPatched) {
777
- try {
778
- history.pushState = origPushState;
779
- history.replaceState = origReplaceState;
780
- } catch {
781
- }
782
- }
1263
+ active5 = null;
1264
+ unsubscribe();
1265
+ clearInterval(sweepTimer);
1266
+ for (const evt of ACTIVITY_EVENTS) window.removeEventListener(evt, onActivity);
1267
+ window.removeEventListener("mousemove", onMouseMove);
1268
+ window.removeEventListener("pagehide", onHide);
1269
+ document.removeEventListener("visibilitychange", onVisibility);
1270
+ window.removeEventListener("storage", onStorage);
783
1271
  }
784
1272
  };
785
- active2 = handle;
1273
+ active5 = handle;
786
1274
  return handle;
787
1275
  }
788
1276
 
@@ -790,10 +1278,40 @@ function startSemanticEvents(options) {
790
1278
  var replayRecorder = null;
791
1279
  var semanticEvents = null;
792
1280
  var jsErrors = null;
793
- var metrics2 = null;
1281
+ var metrics = null;
794
1282
  var baggageUninstall = null;
1283
+ var sessionLifecycle = null;
1284
+ var consoleForward = null;
1285
+ var pageViews = null;
1286
+ var pendingConsentOptions = null;
1287
+ var lastStartedOptions = null;
1288
+ var consentWatcherCleanup = null;
1289
+ var CONSENT_POLL_MS = 1500;
795
1290
  function init(options) {
796
1291
  try {
1292
+ if (options.requireConsent && !hasAnalyticsConsent(options)) {
1293
+ pendingConsentOptions = options;
1294
+ installConsentWatcher(options);
1295
+ return;
1296
+ }
1297
+ startSdk(options);
1298
+ } catch (err) {
1299
+ console.warn("[observ] RUM init failed; telemetry disabled.", err);
1300
+ }
1301
+ }
1302
+ function startSdk(options) {
1303
+ try {
1304
+ lastStartedOptions = options;
1305
+ configureSession({
1306
+ inactivityTimeoutMs: options.sessionInactivityMs,
1307
+ maxDurationMs: options.sessionMaxDurationMs,
1308
+ crossTab: options.crossTabSessions
1309
+ });
1310
+ configureUser({ disablePseudoUser: options.disablePseudoUser });
1311
+ getPseudoUserId();
1312
+ if (!sessionLifecycle) {
1313
+ sessionLifecycle = startSessionLifecycle(options);
1314
+ }
797
1315
  ensureSession();
798
1316
  setupOtelRum(options);
799
1317
  if (!options.disableReplay && !replayRecorder) {
@@ -802,11 +1320,17 @@ function init(options) {
802
1320
  if (!semanticEvents) {
803
1321
  semanticEvents = startSemanticEvents(options);
804
1322
  }
1323
+ if (!options.disablePageViews && !pageViews) {
1324
+ pageViews = startPageViews(options);
1325
+ }
805
1326
  if (!jsErrors) {
806
1327
  jsErrors = startJsErrorCapture(options);
807
1328
  }
808
- if (!options.disableMetrics && !metrics2) {
809
- metrics2 = setupOtelMetrics(options);
1329
+ if (options.forwardConsole && !consoleForward) {
1330
+ consoleForward = startConsoleForward(options);
1331
+ }
1332
+ if (!options.disableMetrics && !metrics) {
1333
+ metrics = setupOtelMetrics(options);
810
1334
  }
811
1335
  if (options.propagateBaggage && !baggageUninstall) {
812
1336
  const backends = [...options.propagateTraceHeaderCorsUrls ?? []];
@@ -821,17 +1345,79 @@ function init(options) {
821
1345
  console.warn("[observ] RUM setup failed; tracing/replay/events degraded.", err);
822
1346
  }
823
1347
  }
1348
+ function removeConsentWatcher() {
1349
+ const cleanup = consentWatcherCleanup;
1350
+ consentWatcherCleanup = null;
1351
+ try {
1352
+ cleanup?.();
1353
+ } catch {
1354
+ }
1355
+ }
1356
+ function startFromConsent(options) {
1357
+ removeConsentWatcher();
1358
+ pendingConsentOptions = null;
1359
+ startSdk(options);
1360
+ }
1361
+ function installConsentWatcher(options) {
1362
+ if (consentWatcherCleanup || typeof window === "undefined") return;
1363
+ const key = consentKey(options);
1364
+ const tryStart = () => {
1365
+ if (hasAnalyticsConsent(options)) startFromConsent(options);
1366
+ };
1367
+ const onStorage = (e) => {
1368
+ if (e.key === key) tryStart();
1369
+ };
1370
+ window.addEventListener("storage", onStorage);
1371
+ const interval = setInterval(tryStart, CONSENT_POLL_MS);
1372
+ consentWatcherCleanup = () => {
1373
+ window.removeEventListener("storage", onStorage);
1374
+ clearInterval(interval);
1375
+ };
1376
+ }
1377
+ function grantConsent() {
1378
+ const options = pendingConsentOptions;
1379
+ if (!options) return;
1380
+ writeConsent(options);
1381
+ startFromConsent(options);
1382
+ }
1383
+ function revokeConsent() {
1384
+ const options = pendingConsentOptions ?? lastStartedOptions;
1385
+ if (options) clearConsent(options);
1386
+ removeConsentWatcher();
1387
+ pendingConsentOptions = null;
1388
+ void shutdown();
1389
+ }
824
1390
  async function shutdown() {
1391
+ removeConsentWatcher();
1392
+ pendingConsentOptions = null;
825
1393
  const recorder = replayRecorder;
826
1394
  const events = semanticEvents;
827
1395
  const errors = jsErrors;
828
- const metricsHandle = metrics2;
1396
+ const metricsHandle = metrics;
829
1397
  const uninstallBaggage = baggageUninstall;
1398
+ const lifecycle = sessionLifecycle;
1399
+ const consoleHandle = consoleForward;
1400
+ const pageViewsHandle = pageViews;
830
1401
  replayRecorder = null;
831
1402
  semanticEvents = null;
832
1403
  jsErrors = null;
833
- metrics2 = null;
1404
+ metrics = null;
834
1405
  baggageUninstall = null;
1406
+ sessionLifecycle = null;
1407
+ consoleForward = null;
1408
+ pageViews = null;
1409
+ try {
1410
+ consoleHandle?.stop();
1411
+ } catch {
1412
+ }
1413
+ try {
1414
+ pageViewsHandle?.stop();
1415
+ } catch {
1416
+ }
1417
+ try {
1418
+ lifecycle?.stop();
1419
+ } catch {
1420
+ }
835
1421
  try {
836
1422
  uninstallBaggage?.();
837
1423
  } catch {
@@ -861,9 +1447,16 @@ async function shutdown() {
861
1447
  } catch {
862
1448
  }
863
1449
  }
864
- var observ = { init, shutdown };
1450
+ var observ = {
1451
+ init,
1452
+ shutdown,
1453
+ setUser,
1454
+ clearUser,
1455
+ grantConsent,
1456
+ revokeConsent
1457
+ };
865
1458
  var index_default = observ;
866
1459
 
867
- export { SESSION_ID_ATTRIBUTE, index_default as default, init, observ, shutdown };
1460
+ export { SESSION_ID_ATTRIBUTE, clearUser, index_default as default, grantConsent, init, observ, revokeConsent, setUser, shutdown };
868
1461
  //# sourceMappingURL=index.js.map
869
1462
  //# sourceMappingURL=index.js.map