@obsrviq/tracker 0.4.0 → 0.4.2

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
@@ -506,36 +506,144 @@ function bodyPreview(text, contentType) {
506
506
  redacted: masked !== text
507
507
  };
508
508
  }
509
- function enrichTiming(ctx, url, base) {
509
+ function perfNow() {
510
+ return typeof performance !== "undefined" ? performance.now() : 0;
511
+ }
512
+ function absUrl(raw) {
510
513
  try {
511
- const entries = performance.getEntriesByType("resource");
512
- const match = entries.filter((e) => e.name === url).pop();
513
- if (!match) return base;
514
- const t = { ...base };
515
- if (match.domainLookupEnd && match.domainLookupStart)
516
- t.dns = Math.max(0, match.domainLookupEnd - match.domainLookupStart);
517
- if (match.connectEnd && match.connectStart)
518
- t.tcp = Math.max(0, match.connectEnd - match.connectStart);
519
- if (match.secureConnectionStart)
520
- t.tls = Math.max(0, match.connectEnd - match.secureConnectionStart);
521
- if (match.responseStart && match.requestStart)
522
- t.ttfb = Math.max(0, match.responseStart - match.requestStart);
523
- if (match.responseEnd && match.responseStart)
524
- t.download = Math.max(0, match.responseEnd - match.responseStart);
525
- return t;
514
+ return new URL(raw, typeof location !== "undefined" ? location.href : void 0).href;
526
515
  } catch {
527
- return base;
516
+ return raw;
517
+ }
518
+ }
519
+ function phasesFrom(m, base) {
520
+ const t = { ...base };
521
+ const r = (n) => Math.round(n);
522
+ if (m.domainLookupStart > 0 && m.domainLookupEnd > m.domainLookupStart)
523
+ t.dns = r(m.domainLookupEnd - m.domainLookupStart);
524
+ if (m.connectStart > 0 && m.connectEnd > m.connectStart) t.tcp = r(m.connectEnd - m.connectStart);
525
+ if (m.secureConnectionStart > 0 && m.connectEnd > m.secureConnectionStart)
526
+ t.tls = r(m.connectEnd - m.secureConnectionStart);
527
+ if (m.requestStart > 0 && m.responseStart > m.requestStart)
528
+ t.ttfb = r(m.responseStart - m.requestStart);
529
+ if (m.responseStart > 0 && m.responseEnd > m.responseStart)
530
+ t.download = r(m.responseEnd - m.responseStart);
531
+ return t;
532
+ }
533
+ function matchEntry(name, perfStart, entries) {
534
+ let best;
535
+ let bestDelta = Infinity;
536
+ for (const e of entries) {
537
+ if (e.name !== name) continue;
538
+ if (e.startTime < perfStart - 4) continue;
539
+ const d = e.startTime - perfStart;
540
+ if (d < bestDelta) {
541
+ bestDelta = d;
542
+ best = e;
543
+ }
528
544
  }
545
+ return best;
546
+ }
547
+ var TIMING_WAIT_MS = 300;
548
+ function createTimingResolver() {
549
+ const pending = [];
550
+ let obs;
551
+ const stopObs = () => {
552
+ if (obs) {
553
+ try {
554
+ obs.disconnect();
555
+ } catch {
556
+ }
557
+ obs = void 0;
558
+ }
559
+ };
560
+ const settle = (p, t) => {
561
+ const i = pending.indexOf(p);
562
+ if (i === -1) return;
563
+ pending.splice(i, 1);
564
+ clearTimeout(p.timer);
565
+ p.resolve(t);
566
+ if (pending.length === 0) stopObs();
567
+ };
568
+ const consume = (entries) => {
569
+ for (const e of entries) {
570
+ let best;
571
+ let bestDelta = Infinity;
572
+ for (const p of pending) {
573
+ if (e.name !== p.name || e.startTime < p.perfStart - 4) continue;
574
+ const d = e.startTime - p.perfStart;
575
+ if (d < bestDelta) {
576
+ bestDelta = d;
577
+ best = p;
578
+ }
579
+ }
580
+ if (best) settle(best, phasesFrom(e, best.base));
581
+ }
582
+ };
583
+ const ensureObserver = () => {
584
+ if (obs || typeof PerformanceObserver === "undefined") return;
585
+ try {
586
+ obs = new PerformanceObserver((list) => consume(list.getEntries()));
587
+ obs.observe({ type: "resource", buffered: false });
588
+ } catch {
589
+ obs = void 0;
590
+ }
591
+ };
592
+ return {
593
+ resolve(rawUrl, perfStart, base) {
594
+ if (typeof performance === "undefined") return Promise.resolve(base);
595
+ const name = absUrl(rawUrl);
596
+ try {
597
+ const now = performance.getEntriesByType("resource");
598
+ const immediate = matchEntry(name, perfStart, now);
599
+ if (immediate) return Promise.resolve(phasesFrom(immediate, base));
600
+ } catch {
601
+ }
602
+ if (typeof PerformanceObserver === "undefined") return Promise.resolve(base);
603
+ return new Promise((resolve) => {
604
+ const p = {
605
+ name,
606
+ perfStart,
607
+ base,
608
+ resolve,
609
+ timer: setTimeout(() => settle(p, base), TIMING_WAIT_MS)
610
+ };
611
+ pending.push(p);
612
+ ensureObserver();
613
+ });
614
+ },
615
+ // Force-settle everything still in flight (with duration-only timing) so their
616
+ // events are emitted into the transport buffer before an unload beacon fires.
617
+ flushPending() {
618
+ for (const p of [...pending]) settle(p, p.base);
619
+ },
620
+ teardown() {
621
+ this.flushPending();
622
+ stopObs();
623
+ }
624
+ };
529
625
  }
530
626
  function instrumentNetwork(ctx) {
627
+ const timing = createTimingResolver();
628
+ const onPageHide = () => timing.flushPending();
629
+ const onVisibility = () => {
630
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") timing.flushPending();
631
+ };
632
+ if (typeof window !== "undefined") window.addEventListener("pagehide", onPageHide);
633
+ if (typeof document !== "undefined") document.addEventListener("visibilitychange", onVisibility);
531
634
  const teardowns = [];
532
- teardowns.push(wrapFetch(ctx));
533
- teardowns.push(wrapXhr(ctx));
635
+ teardowns.push(wrapFetch(ctx, timing));
636
+ teardowns.push(wrapXhr(ctx, timing));
534
637
  teardowns.push(wrapBeacon(ctx));
535
638
  teardowns.push(observeResources(ctx));
639
+ teardowns.push(() => {
640
+ if (typeof window !== "undefined") window.removeEventListener("pagehide", onPageHide);
641
+ if (typeof document !== "undefined") document.removeEventListener("visibilitychange", onVisibility);
642
+ timing.teardown();
643
+ });
536
644
  return () => teardowns.forEach((t) => t());
537
645
  }
538
- function wrapFetch(ctx) {
646
+ function wrapFetch(ctx, tr) {
539
647
  const orig = window.fetch;
540
648
  if (!orig) return () => {
541
649
  };
@@ -543,6 +651,7 @@ function wrapFetch(ctx) {
543
651
  const rawUrl = input instanceof Request ? input.url : String(input);
544
652
  if (rawUrl.includes("/v1/batch")) return orig.call(window, input, init);
545
653
  const startT = ctx.clock.now();
654
+ const perfStart = perfNow();
546
655
  const method = (init?.method || (input instanceof Request ? input.method : "GET")).toUpperCase();
547
656
  const url = redactUrl(rawUrl);
548
657
  const reqHeaders = pickHeaders(
@@ -551,25 +660,27 @@ function wrapFetch(ctx) {
551
660
  );
552
661
  const emit = (partial) => {
553
662
  const endT = ctx.clock.now();
554
- const timing = enrichTiming(ctx, rawUrl, { startT, endT, duration: endT - startT });
555
- const ev = {
556
- id: uuid(),
557
- sessionId: ctx.sessionId,
558
- type: "network",
559
- t: startT,
560
- ts: ctx.clock.wall(),
561
- initiator: "fetch",
562
- method,
563
- url,
564
- status: 0,
565
- ok: false,
566
- resourceType: "fetch",
567
- requestHeaders: reqHeaders,
568
- timing,
569
- ...partial
570
- };
571
- ctx.emit(ev);
663
+ const base = { startT, endT, duration: endT - startT };
572
664
  ctx.markActivity();
665
+ void tr.resolve(rawUrl, perfStart, base).then((timing) => {
666
+ const ev = {
667
+ id: uuid(),
668
+ sessionId: ctx.sessionId,
669
+ type: "network",
670
+ t: startT,
671
+ ts: ctx.clock.wall(),
672
+ initiator: "fetch",
673
+ method,
674
+ url,
675
+ status: 0,
676
+ ok: false,
677
+ resourceType: "fetch",
678
+ requestHeaders: reqHeaders,
679
+ timing,
680
+ ...partial
681
+ };
682
+ ctx.emit(ev);
683
+ });
573
684
  };
574
685
  try {
575
686
  const res = await orig.call(window, input, init);
@@ -611,7 +722,7 @@ function classifyFetchError(err) {
611
722
  if (msg.includes("timeout")) return "timeout";
612
723
  return "network";
613
724
  }
614
- function wrapXhr(ctx) {
725
+ function wrapXhr(ctx, tr) {
615
726
  const XHR = XMLHttpRequest.prototype;
616
727
  const origOpen = XHR.open;
617
728
  const origSend = XHR.send;
@@ -624,6 +735,7 @@ function wrapXhr(ctx) {
624
735
  rawUrl,
625
736
  url: redactUrl(rawUrl),
626
737
  startT: 0,
738
+ perfStart: 0,
627
739
  reqHeaders: {}
628
740
  });
629
741
  return origOpen.apply(this, [method, url, ...rest]);
@@ -642,14 +754,15 @@ function wrapXhr(ctx) {
642
754
  const meta = META.get(this);
643
755
  if (meta) {
644
756
  meta.startT = ctx.clock.now();
757
+ meta.perfStart = perfNow();
645
758
  if (ctx.config.captureNetworkBodies) meta.body = stringifyBody(body);
646
759
  const finish = (error) => {
647
760
  const endT = ctx.clock.now();
648
- const timing = enrichTiming(ctx, meta.rawUrl, {
761
+ const base = {
649
762
  startT: meta.startT,
650
763
  endT,
651
764
  duration: endT - meta.startT
652
- });
765
+ };
653
766
  const status = this.status;
654
767
  const respHeaders = parseRawHeaders(ctx, this.getAllResponseHeaders());
655
768
  let responseBody;
@@ -661,28 +774,31 @@ function wrapXhr(ctx) {
661
774
  } catch {
662
775
  }
663
776
  }
664
- const ev = {
665
- id: uuid(),
666
- sessionId: ctx.sessionId,
667
- type: "network",
668
- t: meta.startT,
669
- ts: ctx.clock.wall(),
670
- initiator: "xhr",
671
- method: meta.method,
672
- url: meta.url,
673
- status,
674
- ok: status >= 200 && status < 400,
675
- resourceType: "xhr",
676
- requestHeaders: meta.reqHeaders,
677
- responseHeaders: respHeaders,
678
- responseSize: safeLen(this),
679
- requestBody: ctx.config.captureNetworkBodies ? bodyPreview(meta.body, void 0) : void 0,
680
- responseBody,
681
- timing,
682
- error: error || classifyHttp(status)
683
- };
684
- ctx.emit(ev);
777
+ const responseSize = safeLen(this);
685
778
  ctx.markActivity();
779
+ void tr.resolve(meta.rawUrl, meta.perfStart, base).then((timing) => {
780
+ const ev = {
781
+ id: uuid(),
782
+ sessionId: ctx.sessionId,
783
+ type: "network",
784
+ t: meta.startT,
785
+ ts: ctx.clock.wall(),
786
+ initiator: "xhr",
787
+ method: meta.method,
788
+ url: meta.url,
789
+ status,
790
+ ok: status >= 200 && status < 400,
791
+ resourceType: "xhr",
792
+ requestHeaders: meta.reqHeaders,
793
+ responseHeaders: respHeaders,
794
+ responseSize,
795
+ requestBody: ctx.config.captureNetworkBodies ? bodyPreview(meta.body, void 0) : void 0,
796
+ responseBody,
797
+ timing,
798
+ error: error || classifyHttp(status)
799
+ };
800
+ ctx.emit(ev);
801
+ });
686
802
  };
687
803
  this.addEventListener("loadend", () => finish());
688
804
  this.addEventListener("error", () => finish("network"));