@botim/mp-debug-sdk 0.3.1 → 0.5.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.cjs +160 -154
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -7
- package/dist/index.d.ts +16 -7
- package/dist/index.js +160 -154
- package/dist/index.js.map +1 -1
- package/dist/types.cjs.map +1 -1
- package/dist/types.d.cts +12 -0
- package/dist/types.d.ts +12 -0
- package/dist/types.js.map +1 -1
- package/package.json +4 -2
package/dist/index.d.cts
CHANGED
|
@@ -45,9 +45,11 @@ interface BuiltinHostHooks {
|
|
|
45
45
|
/**
|
|
46
46
|
* Capture a screenshot. Two return shapes are accepted:
|
|
47
47
|
* - a raw base64 string → assumed PNG (legacy form)
|
|
48
|
-
* - `{ data, format }` → format is one of
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* - `{ data, format }` → format is one of:
|
|
49
|
+
* 'png-base64' | 'jpeg-base64' — raw bitmap, viewer renders as <img>
|
|
50
|
+
* 'rrweb-snapshot' — `data` is JSON the viewer rebuilds
|
|
51
|
+
* 'html-snapshot' — legacy `data` JSON: { html, … }
|
|
52
|
+
* The SDK enforces a 1 MB base64/JSON cap regardless.
|
|
51
53
|
*/
|
|
52
54
|
screenshot?: () => ScreenshotResult | Promise<ScreenshotResult>;
|
|
53
55
|
}
|
|
@@ -55,10 +57,17 @@ type ScreenshotResult = string | {
|
|
|
55
57
|
data: string;
|
|
56
58
|
format?: 'png-base64' | 'jpeg-base64'
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* rrweb-snapshot tree, JSON-stringified. `data` parses to:
|
|
61
|
+
* { snapshot, viewport: { w, h }, url, capturedAt }
|
|
62
|
+
* The admin viewer calls `rrwebSnapshot.rebuild(snapshot, …)` into
|
|
63
|
+
* an iframe to re-materialize the page with full shadow DOM /
|
|
64
|
+
* adoptedStyleSheets / cross-origin CSS fidelity.
|
|
65
|
+
*/
|
|
66
|
+
| 'rrweb-snapshot'
|
|
67
|
+
/**
|
|
68
|
+
* Legacy self-contained HTML snapshot (pre-0.4). `data` JSON shape
|
|
69
|
+
* is `{ html, viewport, url, capturedAt }`. Kept on the wire for
|
|
70
|
+
* back-compat; admin viewers still know how to render it.
|
|
62
71
|
*/
|
|
63
72
|
| 'html-snapshot';
|
|
64
73
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -45,9 +45,11 @@ interface BuiltinHostHooks {
|
|
|
45
45
|
/**
|
|
46
46
|
* Capture a screenshot. Two return shapes are accepted:
|
|
47
47
|
* - a raw base64 string → assumed PNG (legacy form)
|
|
48
|
-
* - `{ data, format }` → format is one of
|
|
49
|
-
*
|
|
50
|
-
*
|
|
48
|
+
* - `{ data, format }` → format is one of:
|
|
49
|
+
* 'png-base64' | 'jpeg-base64' — raw bitmap, viewer renders as <img>
|
|
50
|
+
* 'rrweb-snapshot' — `data` is JSON the viewer rebuilds
|
|
51
|
+
* 'html-snapshot' — legacy `data` JSON: { html, … }
|
|
52
|
+
* The SDK enforces a 1 MB base64/JSON cap regardless.
|
|
51
53
|
*/
|
|
52
54
|
screenshot?: () => ScreenshotResult | Promise<ScreenshotResult>;
|
|
53
55
|
}
|
|
@@ -55,10 +57,17 @@ type ScreenshotResult = string | {
|
|
|
55
57
|
data: string;
|
|
56
58
|
format?: 'png-base64' | 'jpeg-base64'
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
60
|
+
* rrweb-snapshot tree, JSON-stringified. `data` parses to:
|
|
61
|
+
* { snapshot, viewport: { w, h }, url, capturedAt }
|
|
62
|
+
* The admin viewer calls `rrwebSnapshot.rebuild(snapshot, …)` into
|
|
63
|
+
* an iframe to re-materialize the page with full shadow DOM /
|
|
64
|
+
* adoptedStyleSheets / cross-origin CSS fidelity.
|
|
65
|
+
*/
|
|
66
|
+
| 'rrweb-snapshot'
|
|
67
|
+
/**
|
|
68
|
+
* Legacy self-contained HTML snapshot (pre-0.4). `data` JSON shape
|
|
69
|
+
* is `{ html, viewport, url, capturedAt }`. Kept on the wire for
|
|
70
|
+
* back-compat; admin viewers still know how to render it.
|
|
62
71
|
*/
|
|
63
72
|
| 'html-snapshot';
|
|
64
73
|
};
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { snapshot } from 'rrweb-snapshot';
|
|
2
|
+
|
|
1
3
|
// src/types.ts
|
|
2
4
|
var SCHEMA_VERSION = 2;
|
|
3
5
|
|
|
@@ -268,7 +270,7 @@ function resolveAgainstEndpoint(url, base) {
|
|
|
268
270
|
function detectDeviceInfo(app, override) {
|
|
269
271
|
const ua = typeof navigator !== "undefined" ? navigator.userAgent : void 0;
|
|
270
272
|
return {
|
|
271
|
-
deviceId: override?.deviceId ??
|
|
273
|
+
deviceId: override?.deviceId ?? loadOrCreateDeviceId(),
|
|
272
274
|
platform: override?.platform ?? detectPlatform(ua),
|
|
273
275
|
osVersion: override?.osVersion,
|
|
274
276
|
appName: override?.appName ?? app.name,
|
|
@@ -284,6 +286,21 @@ function detectPlatform(ua) {
|
|
|
284
286
|
if (/Mozilla|Chrome|Safari|Firefox/i.test(ua)) return "web";
|
|
285
287
|
return "unknown";
|
|
286
288
|
}
|
|
289
|
+
var DEVICE_ID_STORAGE_KEY = "botim-debug-sdk:device-id";
|
|
290
|
+
function loadOrCreateDeviceId() {
|
|
291
|
+
try {
|
|
292
|
+
const ls = typeof localStorage !== "undefined" ? localStorage : null;
|
|
293
|
+
if (ls) {
|
|
294
|
+
const stored = ls.getItem(DEVICE_ID_STORAGE_KEY);
|
|
295
|
+
if (stored && stored.length > 0) return stored;
|
|
296
|
+
const fresh = generateDeviceId();
|
|
297
|
+
ls.setItem(DEVICE_ID_STORAGE_KEY, fresh);
|
|
298
|
+
return fresh;
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
return generateDeviceId();
|
|
303
|
+
}
|
|
287
304
|
function generateDeviceId() {
|
|
288
305
|
const c = typeof crypto !== "undefined" ? crypto : void 0;
|
|
289
306
|
if (c?.randomUUID) return c.randomUUID();
|
|
@@ -558,6 +575,11 @@ function wrapFetch(opts) {
|
|
|
558
575
|
method,
|
|
559
576
|
url,
|
|
560
577
|
status: res.status,
|
|
578
|
+
// statusText carries the human label ("OK", "Not Found"). Pre-HTTP/2
|
|
579
|
+
// responses always have it; HTTP/2+ defines it as empty by spec but
|
|
580
|
+
// most browsers synthesize one from the code, so this is reliable
|
|
581
|
+
// enough to display alongside the status code.
|
|
582
|
+
statusText: res.statusText || void 0,
|
|
561
583
|
durationMs: Date.now() - start,
|
|
562
584
|
resHeaders: headersFromResponse(res),
|
|
563
585
|
resBody
|
|
@@ -571,7 +593,14 @@ function wrapFetch(opts) {
|
|
|
571
593
|
url,
|
|
572
594
|
durationMs: Date.now() - start,
|
|
573
595
|
errorMessage: err instanceof Error ? err.message : String(err),
|
|
574
|
-
errorName: err instanceof Error ? err.name : void 0
|
|
596
|
+
errorName: err instanceof Error ? err.name : void 0,
|
|
597
|
+
// Stack from the rejected promise — points into fetch internals
|
|
598
|
+
// and (when present) the call site that issued the request.
|
|
599
|
+
errorStack: err instanceof Error ? err.stack : void 0,
|
|
600
|
+
// undici frequently wraps the real reason in `cause` (e.g.
|
|
601
|
+
// `TypeError: fetch failed` outside, `Error: ECONNREFUSED` inside).
|
|
602
|
+
// Flatten the chain so operators don't have to dig.
|
|
603
|
+
errorCause: collectCauseChain(err)
|
|
575
604
|
});
|
|
576
605
|
throw err;
|
|
577
606
|
}
|
|
@@ -580,6 +609,24 @@ function wrapFetch(opts) {
|
|
|
580
609
|
target.fetch = original;
|
|
581
610
|
};
|
|
582
611
|
}
|
|
612
|
+
function collectCauseChain(err) {
|
|
613
|
+
if (!err || typeof err !== "object") return void 0;
|
|
614
|
+
const lines = [];
|
|
615
|
+
let cur = err.cause;
|
|
616
|
+
const seen = /* @__PURE__ */ new Set();
|
|
617
|
+
while (cur && lines.length < 5) {
|
|
618
|
+
if (seen.has(cur)) break;
|
|
619
|
+
seen.add(cur);
|
|
620
|
+
if (cur instanceof Error) {
|
|
621
|
+
lines.push(`${cur.name}: ${cur.message}`);
|
|
622
|
+
cur = cur.cause;
|
|
623
|
+
} else {
|
|
624
|
+
lines.push(String(cur));
|
|
625
|
+
cur = cur?.cause;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return lines.length ? lines.join("\n") : void 0;
|
|
629
|
+
}
|
|
583
630
|
function wrapXHR(opts) {
|
|
584
631
|
if (typeof XMLHttpRequest === "undefined") return () => {
|
|
585
632
|
};
|
|
@@ -615,6 +662,7 @@ function wrapXHR(opts) {
|
|
|
615
662
|
}
|
|
616
663
|
s.start = Date.now();
|
|
617
664
|
s.reqBody = typeof body === "string" ? body : void 0;
|
|
665
|
+
const sendSiteStack = captureCallSiteStack();
|
|
618
666
|
opts.emit({
|
|
619
667
|
phase: "request",
|
|
620
668
|
reqId: s.reqId,
|
|
@@ -632,25 +680,32 @@ function wrapXHR(opts) {
|
|
|
632
680
|
method: s.method,
|
|
633
681
|
url: s.url,
|
|
634
682
|
status: this.status,
|
|
683
|
+
// XHR exposes statusText directly; same display purpose as fetch.
|
|
684
|
+
statusText: this.statusText || void 0,
|
|
635
685
|
durationMs: Date.now() - s.start,
|
|
636
686
|
resHeaders: headers,
|
|
637
687
|
resBody
|
|
638
688
|
});
|
|
639
689
|
};
|
|
640
|
-
const onError = () => {
|
|
690
|
+
const onError = (kind) => () => {
|
|
641
691
|
opts.emit({
|
|
642
692
|
phase: "error",
|
|
643
693
|
reqId: s.reqId,
|
|
644
694
|
method: s.method,
|
|
645
695
|
url: s.url,
|
|
646
696
|
durationMs: Date.now() - s.start,
|
|
647
|
-
|
|
697
|
+
// Distinguish error/timeout/abort in the message — the standard
|
|
698
|
+
// XHR `statusText` is empty for `error` and unhelpful for the
|
|
699
|
+
// others, so we synthesise a clear label.
|
|
700
|
+
errorMessage: this.statusText || `xhr ${kind}`,
|
|
701
|
+
errorName: `XHR${kind[0].toUpperCase()}${kind.slice(1)}`,
|
|
702
|
+
errorStack: sendSiteStack
|
|
648
703
|
});
|
|
649
704
|
};
|
|
650
705
|
this.addEventListener("load", onLoad);
|
|
651
|
-
this.addEventListener("error", onError);
|
|
652
|
-
this.addEventListener("timeout", onError);
|
|
653
|
-
this.addEventListener("abort", onError);
|
|
706
|
+
this.addEventListener("error", onError("error"));
|
|
707
|
+
this.addEventListener("timeout", onError("timeout"));
|
|
708
|
+
this.addEventListener("abort", onError("abort"));
|
|
654
709
|
return origSend.apply(this, [body]);
|
|
655
710
|
};
|
|
656
711
|
return () => {
|
|
@@ -659,6 +714,15 @@ function wrapXHR(opts) {
|
|
|
659
714
|
proto.setRequestHeader = origSetReqHeader;
|
|
660
715
|
};
|
|
661
716
|
}
|
|
717
|
+
function captureCallSiteStack() {
|
|
718
|
+
try {
|
|
719
|
+
throw new Error("xhr-callsite");
|
|
720
|
+
} catch (err) {
|
|
721
|
+
if (!(err instanceof Error) || !err.stack) return void 0;
|
|
722
|
+
const lines = err.stack.split("\n");
|
|
723
|
+
return lines.slice(2).join("\n") || void 0;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
662
726
|
function parseXhrHeaders(raw) {
|
|
663
727
|
const out = {};
|
|
664
728
|
if (!raw) return out;
|
|
@@ -751,8 +815,6 @@ var CommandRegistry = class {
|
|
|
751
815
|
}
|
|
752
816
|
}
|
|
753
817
|
};
|
|
754
|
-
|
|
755
|
-
// src/commands/builtins.ts
|
|
756
818
|
var MAX_DUMP_BYTES = 64 * 1024;
|
|
757
819
|
var MAX_SCREENSHOT_BYTES = 1024 * 1024;
|
|
758
820
|
async function defaultDomScreenshot() {
|
|
@@ -761,163 +823,107 @@ async function defaultDomScreenshot() {
|
|
|
761
823
|
"[@botim/debug-sdk] default screenshot requires a DOM. Provide builtins.screenshot for non-browser runtimes (e.g. native bridge)."
|
|
762
824
|
);
|
|
763
825
|
}
|
|
826
|
+
const injected = injectAdoptedStyleSheets();
|
|
827
|
+
let tree;
|
|
764
828
|
try {
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
"link",
|
|
787
|
-
"meta",
|
|
788
|
-
"source",
|
|
789
|
-
"track",
|
|
790
|
-
"wbr"
|
|
791
|
-
]);
|
|
792
|
-
var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "noscript", "template"]);
|
|
793
|
-
var DEVTOOL_OVERLAY_PREFIXES = ["vite-", "astro-dev-", "next-route-"];
|
|
794
|
-
function isDevtoolOverlay(tag) {
|
|
795
|
-
return DEVTOOL_OVERLAY_PREFIXES.some((p) => tag.startsWith(p));
|
|
796
|
-
}
|
|
797
|
-
function escapeText(s) {
|
|
798
|
-
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
799
|
-
}
|
|
800
|
-
function escapeAttr(s) {
|
|
801
|
-
return s.replace(/&/g, "&").replace(/"/g, """);
|
|
802
|
-
}
|
|
803
|
-
function serializeComposed(node, opts, depth) {
|
|
804
|
-
if (depth > 256) return "";
|
|
805
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
806
|
-
return escapeText(node.textContent ?? "");
|
|
807
|
-
}
|
|
808
|
-
if (node.nodeType === Node.COMMENT_NODE) return "";
|
|
809
|
-
if (node.nodeType !== Node.ELEMENT_NODE) return "";
|
|
810
|
-
const el = node;
|
|
811
|
-
const tag = el.tagName.toLowerCase();
|
|
812
|
-
if (SKIP_TAGS.has(tag)) return "";
|
|
813
|
-
if (isDevtoolOverlay(tag)) return "";
|
|
814
|
-
if (tag === "slot") {
|
|
815
|
-
const slot = el;
|
|
816
|
-
let assigned = [];
|
|
817
|
-
try {
|
|
818
|
-
assigned = slot.assignedNodes({ flatten: true });
|
|
819
|
-
} catch {
|
|
829
|
+
tree = snapshot(document, {
|
|
830
|
+
inlineStylesheet: true,
|
|
831
|
+
inlineImages: true,
|
|
832
|
+
recordCanvas: true,
|
|
833
|
+
// Deliberately NOT slimming. SlimDOM drops <link rel="preload">,
|
|
834
|
+
// hidden form metadata, and other "noise" that's actually relevant
|
|
835
|
+
// when reproducing a layout bug.
|
|
836
|
+
slimDOM: false,
|
|
837
|
+
// Don't mask anything by default — debug-relay already runs a
|
|
838
|
+
// top-level redactor on console payloads, and on-screen text is the
|
|
839
|
+
// whole point of capturing a screenshot. Hosts that need PII masking
|
|
840
|
+
// can wire their own builtins.screenshot using rrweb-snapshot's
|
|
841
|
+
// `maskTextSelector` / `maskInputOptions`.
|
|
842
|
+
maskAllInputs: false
|
|
843
|
+
});
|
|
844
|
+
} finally {
|
|
845
|
+
for (const node of injected) {
|
|
846
|
+
try {
|
|
847
|
+
node.remove();
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
820
850
|
}
|
|
821
|
-
const source = assigned.length > 0 ? assigned : Array.from(el.childNodes);
|
|
822
|
-
return source.map((c) => serializeComposed(c, opts, depth + 1)).join("");
|
|
823
851
|
}
|
|
824
|
-
if (
|
|
825
|
-
|
|
852
|
+
if (!tree) {
|
|
853
|
+
throw new Error("[@botim/debug-sdk] rrweb-snapshot returned null tree");
|
|
826
854
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
855
|
+
const payload = {
|
|
856
|
+
snapshot: tree,
|
|
857
|
+
viewport: {
|
|
858
|
+
w: window.innerWidth || document.documentElement.clientWidth || 0,
|
|
859
|
+
h: window.innerHeight || document.documentElement.clientHeight || 0
|
|
860
|
+
},
|
|
861
|
+
url: location.href,
|
|
862
|
+
capturedAt: Date.now()
|
|
863
|
+
};
|
|
864
|
+
return {
|
|
865
|
+
data: JSON.stringify(payload),
|
|
866
|
+
format: "rrweb-snapshot"
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
function injectAdoptedStyleSheets() {
|
|
870
|
+
const injected = [];
|
|
871
|
+
const collect = (sheets) => {
|
|
872
|
+
if (!sheets || sheets.length === 0) return "";
|
|
873
|
+
const chunks = [];
|
|
874
|
+
for (const sheet of sheets) {
|
|
833
875
|
try {
|
|
834
|
-
const
|
|
835
|
-
|
|
876
|
+
const rules = sheet.cssRules;
|
|
877
|
+
for (const rule of Array.from(rules)) chunks.push(rule.cssText);
|
|
836
878
|
} catch {
|
|
837
|
-
return "";
|
|
838
879
|
}
|
|
839
880
|
}
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
for (const rule of Array.from(rules)) chunks.push(rule.cssText);
|
|
854
|
-
} catch {
|
|
881
|
+
return chunks.join("\n");
|
|
882
|
+
};
|
|
883
|
+
const inject = (parent, css) => {
|
|
884
|
+
if (!css) return;
|
|
885
|
+
const ownerDoc = parent instanceof Document ? parent : parent.ownerDocument;
|
|
886
|
+
if (!ownerDoc) return;
|
|
887
|
+
const style = ownerDoc.createElement("style");
|
|
888
|
+
style.setAttribute("data-botim-adopted", "1");
|
|
889
|
+
style.textContent = css;
|
|
890
|
+
const target = parent instanceof Document ? parent.head ?? parent.documentElement ?? parent.body : parent;
|
|
891
|
+
if (target) {
|
|
892
|
+
target.insertBefore(style, target.firstChild);
|
|
893
|
+
injected.push(style);
|
|
855
894
|
}
|
|
856
|
-
}
|
|
857
|
-
return chunks.join("\n");
|
|
858
|
-
}
|
|
859
|
-
async function captureViaSvgForeignObject() {
|
|
860
|
-
const dpr = Math.min(window.devicePixelRatio || 1, 2);
|
|
861
|
-
const w = Math.max(1, Math.min(window.innerWidth || document.documentElement.clientWidth || 1024, 2400));
|
|
862
|
-
const h = Math.max(1, Math.min(document.documentElement.scrollHeight, 4e3));
|
|
863
|
-
const inlinedCss = readInlineableStyles();
|
|
864
|
-
const bodyHtml = serializeComposed(document.body, { stripCrossOriginImages: true }, 0);
|
|
865
|
-
const htmlStr = `<html xmlns="http://www.w3.org/1999/xhtml"><body>${bodyHtml}</body></html>`;
|
|
866
|
-
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}"><foreignObject width="100%" height="100%">` + (inlinedCss ? `<style xmlns="http://www.w3.org/1999/xhtml">${escapeForXml(inlinedCss)}</style>` : "") + htmlStr + `</foreignObject></svg>`;
|
|
867
|
-
const blobUrl = URL.createObjectURL(
|
|
868
|
-
new Blob([svg], { type: "image/svg+xml;charset=utf-8" })
|
|
869
|
-
);
|
|
895
|
+
};
|
|
870
896
|
try {
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
if (!ctx) throw new Error("canvas 2D context unavailable");
|
|
877
|
-
const pageBg = getComputedStyle(document.body).backgroundColor || "#ffffff";
|
|
878
|
-
ctx.fillStyle = pageBg.startsWith("rgba(0, 0, 0, 0)") ? "#ffffff" : pageBg;
|
|
879
|
-
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
880
|
-
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
881
|
-
const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
|
|
882
|
-
return { data: dataUrl.split(",")[1], format: "jpeg-base64" };
|
|
883
|
-
} finally {
|
|
884
|
-
URL.revokeObjectURL(blobUrl);
|
|
897
|
+
inject(
|
|
898
|
+
document,
|
|
899
|
+
collect(document.adoptedStyleSheets)
|
|
900
|
+
);
|
|
901
|
+
} catch {
|
|
885
902
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
<head>
|
|
905
|
-
<meta charset="utf-8">
|
|
906
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
907
|
-
<base href="${escapeAttr(location.href)}">
|
|
908
|
-
` + (inlinedCss ? `<style data-botim-snapshot="1">${inlinedCss}</style>
|
|
909
|
-
` : "") + `</head>
|
|
910
|
-
<body>${bodyHtml}</body>
|
|
911
|
-
</html>`;
|
|
912
|
-
return {
|
|
913
|
-
data: JSON.stringify({
|
|
914
|
-
html,
|
|
915
|
-
viewport: { w: window.innerWidth, h: window.innerHeight },
|
|
916
|
-
url: location.href,
|
|
917
|
-
capturedAt: Date.now()
|
|
918
|
-
}),
|
|
919
|
-
format: "html-snapshot"
|
|
903
|
+
const walkRoot = (root) => {
|
|
904
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
905
|
+
let n = walker.currentNode;
|
|
906
|
+
while (n) {
|
|
907
|
+
const el = n;
|
|
908
|
+
const sr = el.shadowRoot;
|
|
909
|
+
if (sr) {
|
|
910
|
+
try {
|
|
911
|
+
inject(
|
|
912
|
+
sr,
|
|
913
|
+
collect(sr.adoptedStyleSheets)
|
|
914
|
+
);
|
|
915
|
+
} catch {
|
|
916
|
+
}
|
|
917
|
+
walkRoot(sr);
|
|
918
|
+
}
|
|
919
|
+
n = walker.nextNode();
|
|
920
|
+
}
|
|
920
921
|
};
|
|
922
|
+
try {
|
|
923
|
+
walkRoot(document);
|
|
924
|
+
} catch {
|
|
925
|
+
}
|
|
926
|
+
return injected;
|
|
921
927
|
}
|
|
922
928
|
function registerBuiltins(registry, hooks = {}) {
|
|
923
929
|
registry.register("ping", ping);
|