@botim/mp-debug-sdk 0.3.0 → 0.4.1

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.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 'png-base64' | 'jpeg-base64'
49
- * so the admin viewer can pick the right MIME for the data URL.
50
- * The SDK enforces a 1 MB base64 cap regardless.
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
- * Self-contained HTML+CSS snapshot `data` is a JSON string with
59
- * the shape `{ html, viewport, url, capturedAt }`. Admin viewers
60
- * render `html` inside a sandboxed `<iframe srcdoc>` so the result
61
- * looks pixel-identical to the live page without any pixel encoding.
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 'png-base64' | 'jpeg-base64'
49
- * so the admin viewer can pick the right MIME for the data URL.
50
- * The SDK enforces a 1 MB base64 cap regardless.
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
- * Self-contained HTML+CSS snapshot `data` is a JSON string with
59
- * the shape `{ html, viewport, url, capturedAt }`. Admin viewers
60
- * render `html` inside a sandboxed `<iframe srcdoc>` so the result
61
- * looks pixel-identical to the live page without any pixel encoding.
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
 
@@ -105,8 +107,7 @@ var Transport = class {
105
107
  Authorization: `Bearer ${this.opts.deviceToken}`
106
108
  },
107
109
  body: JSON.stringify(batch),
108
- signal: this.inflightUpload.signal,
109
- keepalive: true
110
+ signal: this.inflightUpload.signal
110
111
  });
111
112
  if (!res.ok) {
112
113
  throw new Error(`ingest http ${res.status}`);
@@ -198,6 +199,12 @@ var Transport = class {
198
199
  }
199
200
  if (this.opts.buffer.size() > 0) {
200
201
  const events = this.opts.buffer.drain(this.opts.buffer.size());
202
+ const body = JSON.stringify({
203
+ sessionToken: this.opts.deviceToken,
204
+ events
205
+ });
206
+ const KEEPALIVE_BODY_LIMIT = 60 * 1024;
207
+ const useKeepalive = body.length <= KEEPALIVE_BODY_LIMIT;
201
208
  try {
202
209
  await this.internalFetch(this.opts.ingestUrl, {
203
210
  method: "POST",
@@ -205,11 +212,8 @@ var Transport = class {
205
212
  "Content-Type": "application/json",
206
213
  Authorization: `Bearer ${this.opts.deviceToken}`
207
214
  },
208
- body: JSON.stringify({
209
- sessionToken: this.opts.deviceToken,
210
- events
211
- }),
212
- keepalive: true
215
+ body,
216
+ ...useKeepalive ? { keepalive: true } : {}
213
217
  });
214
218
  } catch (err) {
215
219
  this.opts.onError?.(err);
@@ -749,8 +753,6 @@ var CommandRegistry = class {
749
753
  }
750
754
  }
751
755
  };
752
-
753
- // src/commands/builtins.ts
754
756
  var MAX_DUMP_BYTES = 64 * 1024;
755
757
  var MAX_SCREENSHOT_BYTES = 1024 * 1024;
756
758
  async function defaultDomScreenshot() {
@@ -759,163 +761,107 @@ async function defaultDomScreenshot() {
759
761
  "[@botim/debug-sdk] default screenshot requires a DOM. Provide builtins.screenshot for non-browser runtimes (e.g. native bridge)."
760
762
  );
761
763
  }
764
+ const injected = injectAdoptedStyleSheets();
765
+ let tree;
762
766
  try {
763
- return await captureViaSvgForeignObject();
764
- } catch (err) {
765
- try {
766
- console.warn(
767
- "[@botim/debug-sdk] SVG/canvas screenshot failed, falling back to HTML snapshot:",
768
- err instanceof Error ? err.message : err
769
- );
770
- } catch {
771
- }
772
- return await captureViaHtmlSnapshot();
773
- }
774
- }
775
- var VOID_ELEMENTS = /* @__PURE__ */ new Set([
776
- "area",
777
- "base",
778
- "br",
779
- "col",
780
- "embed",
781
- "hr",
782
- "img",
783
- "input",
784
- "link",
785
- "meta",
786
- "source",
787
- "track",
788
- "wbr"
789
- ]);
790
- var SKIP_TAGS = /* @__PURE__ */ new Set(["script", "noscript", "template"]);
791
- var DEVTOOL_OVERLAY_PREFIXES = ["vite-", "astro-dev-", "next-route-"];
792
- function isDevtoolOverlay(tag) {
793
- return DEVTOOL_OVERLAY_PREFIXES.some((p) => tag.startsWith(p));
794
- }
795
- function escapeText(s) {
796
- return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
797
- }
798
- function escapeAttr(s) {
799
- return s.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
800
- }
801
- function serializeComposed(node, opts, depth) {
802
- if (depth > 256) return "";
803
- if (node.nodeType === Node.TEXT_NODE) {
804
- return escapeText(node.textContent ?? "");
805
- }
806
- if (node.nodeType === Node.COMMENT_NODE) return "";
807
- if (node.nodeType !== Node.ELEMENT_NODE) return "";
808
- const el = node;
809
- const tag = el.tagName.toLowerCase();
810
- if (SKIP_TAGS.has(tag)) return "";
811
- if (isDevtoolOverlay(tag)) return "";
812
- if (tag === "slot") {
813
- const slot = el;
814
- let assigned = [];
815
- try {
816
- assigned = slot.assignedNodes({ flatten: true });
817
- } catch {
767
+ tree = snapshot(document, {
768
+ inlineStylesheet: true,
769
+ inlineImages: true,
770
+ recordCanvas: true,
771
+ // Deliberately NOT slimming. SlimDOM drops <link rel="preload">,
772
+ // hidden form metadata, and other "noise" that's actually relevant
773
+ // when reproducing a layout bug.
774
+ slimDOM: false,
775
+ // Don't mask anything by default — debug-relay already runs a
776
+ // top-level redactor on console payloads, and on-screen text is the
777
+ // whole point of capturing a screenshot. Hosts that need PII masking
778
+ // can wire their own builtins.screenshot using rrweb-snapshot's
779
+ // `maskTextSelector` / `maskInputOptions`.
780
+ maskAllInputs: false
781
+ });
782
+ } finally {
783
+ for (const node of injected) {
784
+ try {
785
+ node.remove();
786
+ } catch {
787
+ }
818
788
  }
819
- const source = assigned.length > 0 ? assigned : Array.from(el.childNodes);
820
- return source.map((c) => serializeComposed(c, opts, depth + 1)).join("");
821
789
  }
822
- if (tag === "link" && (el.getAttribute("rel") || "").toLowerCase() === "stylesheet") {
823
- return "";
790
+ if (!tree) {
791
+ throw new Error("[@botim/debug-sdk] rrweb-snapshot returned null tree");
824
792
  }
825
- if (tag === "style") {
826
- return `<style>${el.textContent ?? ""}</style>`;
827
- }
828
- if (tag === "img" && opts.stripCrossOriginImages) {
829
- const src = el.getAttribute("src") || "";
830
- if (/^(https?:|\/\/)/i.test(src)) {
793
+ const payload = {
794
+ snapshot: tree,
795
+ viewport: {
796
+ w: window.innerWidth || document.documentElement.clientWidth || 0,
797
+ h: window.innerHeight || document.documentElement.clientHeight || 0
798
+ },
799
+ url: location.href,
800
+ capturedAt: Date.now()
801
+ };
802
+ return {
803
+ data: JSON.stringify(payload),
804
+ format: "rrweb-snapshot"
805
+ };
806
+ }
807
+ function injectAdoptedStyleSheets() {
808
+ const injected = [];
809
+ const collect = (sheets) => {
810
+ if (!sheets || sheets.length === 0) return "";
811
+ const chunks = [];
812
+ for (const sheet of sheets) {
831
813
  try {
832
- const u = new URL(src, location.href);
833
- if (u.origin !== location.origin) return "";
814
+ const rules = sheet.cssRules;
815
+ for (const rule of Array.from(rules)) chunks.push(rule.cssText);
834
816
  } catch {
835
- return "";
836
817
  }
837
818
  }
838
- }
839
- if (tag === "canvas") return `<canvas></canvas>`;
840
- const attrs = Array.from(el.attributes).filter((a) => !a.name.startsWith("on")).map((a) => ` ${a.name}="${escapeAttr(a.value)}"`).join("");
841
- if (VOID_ELEMENTS.has(tag)) return `<${tag}${attrs}>`;
842
- const childSource = el.shadowRoot ?? el;
843
- const inner = Array.from(childSource.childNodes).map((c) => serializeComposed(c, opts, depth + 1)).join("");
844
- return `<${tag}${attrs}>${inner}</${tag}>`;
845
- }
846
- function readInlineableStyles() {
847
- const chunks = [];
848
- for (const sheet of Array.from(document.styleSheets)) {
849
- try {
850
- const rules = sheet.cssRules;
851
- for (const rule of Array.from(rules)) chunks.push(rule.cssText);
852
- } catch {
819
+ return chunks.join("\n");
820
+ };
821
+ const inject = (parent, css) => {
822
+ if (!css) return;
823
+ const ownerDoc = parent instanceof Document ? parent : parent.ownerDocument;
824
+ if (!ownerDoc) return;
825
+ const style = ownerDoc.createElement("style");
826
+ style.setAttribute("data-botim-adopted", "1");
827
+ style.textContent = css;
828
+ const target = parent instanceof Document ? parent.head ?? parent.documentElement ?? parent.body : parent;
829
+ if (target) {
830
+ target.insertBefore(style, target.firstChild);
831
+ injected.push(style);
853
832
  }
854
- }
855
- return chunks.join("\n");
856
- }
857
- async function captureViaSvgForeignObject() {
858
- const dpr = Math.min(window.devicePixelRatio || 1, 2);
859
- const w = Math.max(1, Math.min(window.innerWidth || document.documentElement.clientWidth || 1024, 2400));
860
- const h = Math.max(1, Math.min(document.documentElement.scrollHeight, 4e3));
861
- const inlinedCss = readInlineableStyles();
862
- const bodyHtml = serializeComposed(document.body, { stripCrossOriginImages: true }, 0);
863
- const htmlStr = `<html xmlns="http://www.w3.org/1999/xhtml"><body>${bodyHtml}</body></html>`;
864
- 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>`;
865
- const blobUrl = URL.createObjectURL(
866
- new Blob([svg], { type: "image/svg+xml;charset=utf-8" })
867
- );
833
+ };
868
834
  try {
869
- const img = await loadImage(blobUrl);
870
- const canvas = document.createElement("canvas");
871
- canvas.width = w * dpr;
872
- canvas.height = h * dpr;
873
- const ctx = canvas.getContext("2d");
874
- if (!ctx) throw new Error("canvas 2D context unavailable");
875
- const pageBg = getComputedStyle(document.body).backgroundColor || "#ffffff";
876
- ctx.fillStyle = pageBg.startsWith("rgba(0, 0, 0, 0)") ? "#ffffff" : pageBg;
877
- ctx.fillRect(0, 0, canvas.width, canvas.height);
878
- ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
879
- const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
880
- return { data: dataUrl.split(",")[1], format: "jpeg-base64" };
881
- } finally {
882
- URL.revokeObjectURL(blobUrl);
835
+ inject(
836
+ document,
837
+ collect(document.adoptedStyleSheets)
838
+ );
839
+ } catch {
883
840
  }
884
- }
885
- function loadImage(src) {
886
- return new Promise((resolve, reject) => {
887
- const img = new Image();
888
- img.crossOrigin = "anonymous";
889
- img.onload = () => resolve(img);
890
- img.onerror = () => reject(new Error("failed to render snapshot SVG"));
891
- img.src = src;
892
- });
893
- }
894
- function escapeForXml(s) {
895
- return s.replace(/]]>/g, "]]&gt;").replace(/<\//g, "&lt;/");
896
- }
897
- async function captureViaHtmlSnapshot() {
898
- const inlinedCss = readInlineableStyles();
899
- const bodyHtml = serializeComposed(document.body, { stripCrossOriginImages: false }, 0);
900
- const html = `<!DOCTYPE html>
901
- <html lang="en">
902
- <head>
903
- <meta charset="utf-8">
904
- <meta name="viewport" content="width=device-width,initial-scale=1">
905
- <base href="${escapeAttr(location.href)}">
906
- ` + (inlinedCss ? `<style data-botim-snapshot="1">${inlinedCss}</style>
907
- ` : "") + `</head>
908
- <body>${bodyHtml}</body>
909
- </html>`;
910
- return {
911
- data: JSON.stringify({
912
- html,
913
- viewport: { w: window.innerWidth, h: window.innerHeight },
914
- url: location.href,
915
- capturedAt: Date.now()
916
- }),
917
- format: "html-snapshot"
841
+ const walkRoot = (root) => {
842
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
843
+ let n = walker.currentNode;
844
+ while (n) {
845
+ const el = n;
846
+ const sr = el.shadowRoot;
847
+ if (sr) {
848
+ try {
849
+ inject(
850
+ sr,
851
+ collect(sr.adoptedStyleSheets)
852
+ );
853
+ } catch {
854
+ }
855
+ walkRoot(sr);
856
+ }
857
+ n = walker.nextNode();
858
+ }
918
859
  };
860
+ try {
861
+ walkRoot(document);
862
+ } catch {
863
+ }
864
+ return injected;
919
865
  }
920
866
  function registerBuiltins(registry, hooks = {}) {
921
867
  registry.register("ping", ping);