@insitue/sdk 0.1.7 → 0.1.9

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.
@@ -20,6 +20,17 @@ interface CaptureOnlyOptions {
20
20
  * `window.__insitu_capture__` (useful for prod validation).
21
21
  */
22
22
  onCapture?: (draft: IssueDraft, bundle: CaptureBundle) => void;
23
+ /**
24
+ * Force the pixel-perfect (`getDisplayMedia`) path for every
25
+ * capture from mount. Costs a one-time tab-share permission per
26
+ * session in exchange for screenshots that are guaranteed to
27
+ * match what the user actually saw — bypasses every html-to-image
28
+ * quirk (next/image srcset, video frames, canvas content,
29
+ * cross-origin assets). Use in dev/dogfood where capture quality
30
+ * matters more than permission UX; leave off for prod end-users
31
+ * who shouldn't see a permission dialog uninvited.
32
+ */
33
+ defaultPixelPerfect?: boolean;
23
34
  }
24
35
  declare function mountCaptureOnly(opts?: CaptureOnlyOptions): () => void;
25
36
 
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountCaptureOnly
3
- } from "./chunk-3ZUAZAJB.js";
4
- import "./chunk-AMC2RGMK.js";
3
+ } from "./chunk-BNREKX5C.js";
4
+ import "./chunk-CPPXBTE5.js";
5
5
  export {
6
6
  mountCaptureOnly
7
7
  };
@@ -8,9 +8,10 @@ import {
8
8
  k,
9
9
  onDisplayMediaChange,
10
10
  retryDisplayMedia,
11
+ setCaptureSettings,
11
12
  stopDisplayMedia,
12
13
  y
13
- } from "./chunk-AMC2RGMK.js";
14
+ } from "./chunk-CPPXBTE5.js";
14
15
 
15
16
  // src/capture-only.ts
16
17
  var DEFAULT_INGEST = "https://www.insitue.com/api/v1/capture";
@@ -353,8 +354,8 @@ function CaptureOnlyApp(props) {
353
354
  k("span", {}, "\u{1F512} Secrets scrubbed automatically"),
354
355
  k(
355
356
  "span",
356
- { title: `@insitue/sdk@${"0.1.7"}` },
357
- `InSitue \xB7 v${"0.1.7"}`
357
+ { title: `@insitue/sdk@${"0.1.9"}` },
358
+ `InSitue \xB7 v${"0.1.9"}`
358
359
  )
359
360
  ]
360
361
  )
@@ -362,6 +363,9 @@ function CaptureOnlyApp(props) {
362
363
  }
363
364
  function mountCaptureOnly(opts = {}) {
364
365
  installRuntimeCollectors();
366
+ if (opts.defaultPixelPerfect === true) {
367
+ setCaptureSettings({ alwaysPixelPerfect: true });
368
+ }
365
369
  const host = document.createElement("div");
366
370
  host.id = "insitu-capture-root";
367
371
  host.setAttribute("data-insitu", "");
@@ -1641,6 +1641,16 @@ async function toCanvas(node, options = {}) {
1641
1641
  }
1642
1642
 
1643
1643
  // src/capture.ts
1644
+ function crossOrigin(url) {
1645
+ if (!url || url.startsWith("data:") || url.startsWith("blob:")) {
1646
+ return false;
1647
+ }
1648
+ try {
1649
+ return new URL(url, location.href).origin !== location.origin;
1650
+ } catch {
1651
+ return false;
1652
+ }
1653
+ }
1644
1654
  var IMAGE_PLACEHOLDER = "data:image/svg+xml;base64," + (typeof btoa !== "undefined" ? btoa(
1645
1655
  '<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><rect width="32" height="32" fill="#e8e8e8"/><path d="M0 0 L32 32 M32 0 L0 32" stroke="#b0b0b0" stroke-width="1.5"/></svg>'
1646
1656
  ) : "");
@@ -1666,20 +1676,34 @@ async function renderViewportCrop(cropRect, pixelRatio) {
1666
1676
  const htmlBg = getComputedStyle(document.documentElement).backgroundColor;
1667
1677
  const backgroundColor = bodyBg && bodyBg !== "rgba(0, 0, 0, 0)" && bodyBg !== "transparent" ? bodyBg : htmlBg && htmlBg !== "rgba(0, 0, 0, 0)" && htmlBg !== "transparent" ? htmlBg : "#ffffff";
1668
1678
  const failedImages = /* @__PURE__ */ new Set();
1679
+ const out = document.createElement("canvas");
1680
+ out.width = Math.max(1, Math.round(cropRect.width * pixelRatio));
1681
+ out.height = Math.max(1, Math.round(cropRect.height * pixelRatio));
1682
+ const ctx = out.getContext("2d");
1683
+ if (!ctx) return { dataUrl: null, failedImages };
1684
+ const drawnImgs = drawAbsoluteImagesOnto(
1685
+ ctx,
1686
+ cropRect,
1687
+ pixelRatio,
1688
+ failedImages
1689
+ );
1669
1690
  const fullCanvas = await toCanvas(document.documentElement, {
1670
1691
  pixelRatio,
1671
1692
  cacheBust: true,
1672
1693
  backgroundColor,
1673
1694
  imagePlaceholder: IMAGE_PLACEHOLDER,
1674
- filter: (n2) => !(n2 instanceof Element && n2.closest?.("#insitu-root, [data-insitu-layer]"))
1695
+ filter: (n2) => {
1696
+ if (n2 instanceof Element && n2.closest?.("#insitu-root, [data-insitu-layer]")) {
1697
+ return false;
1698
+ }
1699
+ if (n2 instanceof HTMLImageElement && drawnImgs.has(n2)) {
1700
+ return false;
1701
+ }
1702
+ return true;
1703
+ }
1675
1704
  });
1676
1705
  const sx = window.scrollX;
1677
1706
  const sy = window.scrollY;
1678
- const out = document.createElement("canvas");
1679
- out.width = Math.max(1, Math.round(cropRect.width * pixelRatio));
1680
- out.height = Math.max(1, Math.round(cropRect.height * pixelRatio));
1681
- const ctx = out.getContext("2d");
1682
- if (!ctx) return { dataUrl: null, failedImages };
1683
1707
  ctx.drawImage(
1684
1708
  fullCanvas,
1685
1709
  Math.round((cropRect.x + sx) * pixelRatio),
@@ -1697,6 +1721,95 @@ async function renderViewportCrop(cropRect, pixelRatio) {
1697
1721
  detectUnrenderedImages(ctx, cropRect, out, pixelRatio, failedImages);
1698
1722
  return { dataUrl: out.toDataURL("image/png"), failedImages };
1699
1723
  }
1724
+ function drawAbsoluteImagesOnto(ctx, cropRect, pixelRatio, failedImages) {
1725
+ const drawn = /* @__PURE__ */ new Set();
1726
+ const imgs = Array.from(
1727
+ document.querySelectorAll("img")
1728
+ ).filter(
1729
+ (img) => !img.closest?.("#insitu-root, [data-insitu-layer]")
1730
+ );
1731
+ for (const img of imgs) {
1732
+ const r3 = img.getBoundingClientRect();
1733
+ if (r3.width <= 0 || r3.height <= 0) continue;
1734
+ const cs = getComputedStyle(img);
1735
+ if (cs.position !== "absolute" && cs.position !== "fixed") continue;
1736
+ if (r3.right < cropRect.x || r3.left > cropRect.x + cropRect.width || r3.bottom < cropRect.y || r3.top > cropRect.y + cropRect.height) {
1737
+ continue;
1738
+ }
1739
+ const src = img.currentSrc || img.src;
1740
+ if (!src) continue;
1741
+ if (crossOrigin(src) && img.crossOrigin !== "anonymous" && img.crossOrigin !== "use-credentials") {
1742
+ failedImages.add(img);
1743
+ continue;
1744
+ }
1745
+ if (!img.complete || img.naturalWidth === 0 || img.naturalHeight === 0) {
1746
+ failedImages.add(img);
1747
+ continue;
1748
+ }
1749
+ const dest = {
1750
+ x: (r3.left - cropRect.x) * pixelRatio,
1751
+ y: (r3.top - cropRect.y) * pixelRatio,
1752
+ w: r3.width * pixelRatio,
1753
+ h: r3.height * pixelRatio
1754
+ };
1755
+ const source = computeObjectFitSource(img, cs);
1756
+ try {
1757
+ ctx.drawImage(
1758
+ img,
1759
+ source.sx,
1760
+ source.sy,
1761
+ source.sw,
1762
+ source.sh,
1763
+ dest.x,
1764
+ dest.y,
1765
+ dest.w,
1766
+ dest.h
1767
+ );
1768
+ drawn.add(img);
1769
+ } catch {
1770
+ failedImages.add(img);
1771
+ }
1772
+ }
1773
+ return drawn;
1774
+ }
1775
+ function computeObjectFitSource(img, cs) {
1776
+ const nw = img.naturalWidth;
1777
+ const nh = img.naturalHeight;
1778
+ const r3 = img.getBoundingClientRect();
1779
+ const dw = r3.width;
1780
+ const dh = r3.height;
1781
+ if (!nw || !nh || !dw || !dh) {
1782
+ return { sx: 0, sy: 0, sw: nw || 1, sh: nh || 1 };
1783
+ }
1784
+ const fit = cs.objectFit || "fill";
1785
+ if (fit === "fill") {
1786
+ return { sx: 0, sy: 0, sw: nw, sh: nh };
1787
+ }
1788
+ const destAR = dw / dh;
1789
+ const srcAR = nw / nh;
1790
+ if (fit === "cover") {
1791
+ if (srcAR > destAR) {
1792
+ const sw = nh * destAR;
1793
+ return { sx: (nw - sw) / 2, sy: 0, sw, sh: nh };
1794
+ }
1795
+ const sh = nw / destAR;
1796
+ return { sx: 0, sy: (nh - sh) / 2, sw: nw, sh };
1797
+ }
1798
+ if (fit === "contain" || fit === "scale-down") {
1799
+ return { sx: 0, sy: 0, sw: nw, sh: nh };
1800
+ }
1801
+ if (fit === "none") {
1802
+ const sw = Math.min(nw, dw);
1803
+ const sh = Math.min(nh, dh);
1804
+ return {
1805
+ sx: (nw - sw) / 2,
1806
+ sy: (nh - sh) / 2,
1807
+ sw,
1808
+ sh
1809
+ };
1810
+ }
1811
+ return { sx: 0, sy: 0, sw: nw, sh: nh };
1812
+ }
1700
1813
  function detectUnrenderedImages(cropCtx, cropRect, cropCanvas, pixelRatio, failedImages) {
1701
1814
  const imgs = Array.from(
1702
1815
  document.querySelectorAll("img")
@@ -15,7 +15,7 @@ import {
15
15
  setCaptureSettings,
16
16
  stopDisplayMedia,
17
17
  y
18
- } from "./chunk-AMC2RGMK.js";
18
+ } from "./chunk-CPPXBTE5.js";
19
19
 
20
20
  // src/client.ts
21
21
  var CompanionClient = class {
package/dist/index.d.ts CHANGED
@@ -21,6 +21,18 @@ interface InSitueCaptureProps {
21
21
  * (neither set): console + JSON download + `window.__insitu_capture__`.
22
22
  */
23
23
  onCapture?: (draft: IssueDraft, bundle: CaptureBundle) => void;
24
+ /**
25
+ * Default the user's "Always pixel-perfect screenshots" setting
26
+ * to `true` on mount — every capture uses the `getDisplayMedia`
27
+ * OS-compositor path, paying a one-time tab-share permission per
28
+ * session in exchange for screenshots that are pixel-accurate
29
+ * across any content (next/image, video, canvas, cross-origin).
30
+ *
31
+ * Recommended for dev / dogfood, where capture quality matters
32
+ * more than the permission UX. Not the default — production
33
+ * end-users shouldn't see a permission dialog they didn't ask for.
34
+ */
35
+ defaultPixelPerfect?: boolean;
24
36
  }
25
37
  /**
26
38
  * `<InSitueCapture />` — the prod capture-only path. UNLIKE
@@ -31,7 +43,7 @@ interface InSitueCaptureProps {
31
43
  * The simplest path: set `projectKey` and the SDK POSTs captures to
32
44
  * the InSitue cloud automatically.
33
45
  */
34
- declare function InSitueCapture({ projectKey, endpoint, onCapture, }: InSitueCaptureProps): null;
46
+ declare function InSitueCapture({ projectKey, endpoint, onCapture, defaultPixelPerfect, }: InSitueCaptureProps): null;
35
47
 
36
48
  /** Build-time-inlined version of `@insitue/sdk` (from package.json).
37
49
  * Exposed so the host app can self-verify which SDK build is loaded
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  mountCaptureOnly
3
- } from "./chunk-3ZUAZAJB.js";
3
+ } from "./chunk-BNREKX5C.js";
4
4
  import {
5
5
  mountInSitue
6
- } from "./chunk-DBHURN5L.js";
7
- import "./chunk-AMC2RGMK.js";
6
+ } from "./chunk-DSHJX2LF.js";
7
+ import "./chunk-CPPXBTE5.js";
8
8
 
9
9
  // src/InSitue.tsx
10
10
  import { useEffect } from "react";
@@ -27,26 +27,32 @@ function InSitue({ port }) {
27
27
  function InSitueCapture({
28
28
  projectKey,
29
29
  endpoint,
30
- onCapture
30
+ onCapture,
31
+ defaultPixelPerfect
31
32
  }) {
32
33
  useEffect(() => {
33
34
  let active = true;
34
35
  let dispose;
35
36
  void import("./capture-only.js").then((m) => {
36
37
  if (active) {
37
- dispose = m.mountCaptureOnly({ projectKey, endpoint, onCapture });
38
+ dispose = m.mountCaptureOnly({
39
+ projectKey,
40
+ endpoint,
41
+ onCapture,
42
+ defaultPixelPerfect
43
+ });
38
44
  }
39
45
  });
40
46
  return () => {
41
47
  active = false;
42
48
  dispose?.();
43
49
  };
44
- }, [projectKey, endpoint, onCapture]);
50
+ }, [projectKey, endpoint, onCapture, defaultPixelPerfect]);
45
51
  return null;
46
52
  }
47
53
 
48
54
  // src/index.ts
49
- var SDK_VERSION = "0.1.7";
55
+ var SDK_VERSION = "0.1.9";
50
56
  export {
51
57
  InSitue,
52
58
  InSitueCapture,
package/dist/overlay.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountInSitue
3
- } from "./chunk-DBHURN5L.js";
4
- import "./chunk-AMC2RGMK.js";
3
+ } from "./chunk-DSHJX2LF.js";
4
+ import "./chunk-CPPXBTE5.js";
5
5
  export {
6
6
  mountInSitue
7
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@insitue/sdk",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "InSitue capture SDK — drop one snippet into your deployed app; your users point at a bug, InSitue opens a verified pull request.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -50,9 +50,12 @@
50
50
  "devDependencies": {
51
51
  "@types/node": "^22.9.0",
52
52
  "@types/react": "^19.0.0",
53
+ "@vitest/browser": "^3.2.4",
54
+ "playwright": "^1.60.0",
53
55
  "react": "^19.0.0",
54
56
  "tsup": "^8.3.5",
55
57
  "typescript": "^5.6.3",
58
+ "vitest": "^3.2.4",
56
59
  "@insitue/capture-core": "0.0.0"
57
60
  },
58
61
  "repository": {
@@ -84,6 +87,8 @@
84
87
  "build": "tsup",
85
88
  "dev": "tsup --watch",
86
89
  "typecheck": "tsc --noEmit",
87
- "lint": "tsc --noEmit"
90
+ "lint": "tsc --noEmit",
91
+ "test": "vitest run",
92
+ "test:watch": "vitest"
88
93
  }
89
94
  }