@getdevteam/analytics-web 0.3.0 → 0.3.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/README.md +2 -1
- package/dist/{feedback-W5PGVRNZ.js → feedback-SJHUYYJN.js} +3 -1
- package/dist/{feedback-W5PGVRNZ.js.map → feedback-SJHUYYJN.js.map} +1 -1
- package/dist/index.cjs +4 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,7 +34,8 @@ Set `allowUserFeedback: true` to render a floating feedback button (isolated in
|
|
|
34
34
|
`analytics.showFeedback()` opens the overlay programmatically; it is a no-op while the feature is off or unavailable.
|
|
35
35
|
Customize with `feedback: { colors, accentColor, buttonLabel, title, submitLabel, commentPlaceholder, successMessage, position, zIndex, pixelRatio }`.
|
|
36
36
|
|
|
37
|
-
The screenshot is a DOM-to-canvas render
|
|
37
|
+
The screenshot is a DOM-to-canvas render.
|
|
38
|
+
Individual images the browser cannot fetch are left blank so screenshot and drawing remain available; cross-origin iframes, tainted canvases, and video frames can still come out blank or make the capture fail, and a failed capture falls back to comment-only feedback instead of breaking the app.
|
|
38
39
|
See the [SDK README](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk#user-feedback) for the full option table and capture boundary.
|
|
39
40
|
|
|
40
41
|
You need a public ingest key (`dtp_...`) and the ingest host URL for your environment - see [Getting a key](https://github.com/getdevteam-ai/analytics/tree/main/src/sdk#getting-a-key) in the full SDK docs.
|
|
@@ -3,9 +3,11 @@ import { uuidV7 } from "@getdevteam/analytics-core";
|
|
|
3
3
|
|
|
4
4
|
// src/feedback/capture.ts
|
|
5
5
|
import { toCanvas } from "html-to-image";
|
|
6
|
+
var TRANSPARENT_PIXEL_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=";
|
|
6
7
|
async function captureViewport(exclude, pixelRatio) {
|
|
7
8
|
return toCanvas(document.documentElement, {
|
|
8
9
|
filter: (node) => node !== exclude,
|
|
10
|
+
imagePlaceholder: TRANSPARENT_PIXEL_DATA_URL,
|
|
9
11
|
pixelRatio,
|
|
10
12
|
width: window.innerWidth,
|
|
11
13
|
height: window.innerHeight,
|
|
@@ -648,4 +650,4 @@ export {
|
|
|
648
650
|
mapToImagePoint,
|
|
649
651
|
mountFeedback
|
|
650
652
|
};
|
|
651
|
-
//# sourceMappingURL=feedback-
|
|
653
|
+
//# sourceMappingURL=feedback-SJHUYYJN.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/feedback/index.ts","../src/feedback/capture.ts","../src/feedback/exportImage.ts","../src/feedback/strokes.ts"],"sourcesContent":["import { uuidV7, type EventContext, type FetchLike } from \"@getdevteam/analytics-core\";\nimport { captureViewport } from \"./capture\";\nimport { drawStrokes, exportAnnotatedImage, type ExportedImage } from \"./exportImage\";\nimport { createStrokeStore, mapToImagePoint, type StrokePoint } from \"./strokes\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nimport type { Stroke } from \"./strokes\";\n\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nexport { createStrokeStore, mapToImagePoint } from \"./strokes\";\nexport type { Stroke, StrokePoint, StrokeStore } from \"./strokes\";\nexport { exportAnnotatedImage } from \"./exportImage\";\nexport type { ExportedImage } from \"./exportImage\";\n\nexport interface FeedbackConfig {\n url: string;\n key: string;\n fetch: FetchLike | undefined;\n getDistinctId(): string;\n getSessionId(): string;\n getContext(): EventContext;\n options: WebFeedbackOptions;\n onError(error: unknown): void;\n}\n\nexport interface FeedbackDeps {\n capture(exclude: HTMLElement, pixelRatio: number): Promise<HTMLCanvasElement>;\n exportAnnotated(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: { maxBytes: number },\n ): Promise<ExportedImage | null>;\n}\n\nconst DEFAULT_COLORS = [\"#f44336\", \"#4caf50\", \"#2196f3\", \"#ffeb3b\"];\nconst DEFAULT_ACCENT = \"#5b9cf5\";\nconst DEFAULT_Z_INDEX = 2147483000;\nconst PEN_WIDTH = 5;\nconst MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;\nconst MAX_TEXT_LENGTH = 10000;\nconst MAX_PAGE_URL_LENGTH = 2048;\nconst SUCCESS_CLOSE_DELAY_MS = 1600;\n\nconst defaultDeps: FeedbackDeps = {\n capture: captureViewport,\n exportAnnotated: exportAnnotatedImage,\n};\n\nexport function mountFeedback(config: FeedbackConfig, deps: FeedbackDeps = defaultDeps): FeedbackHandle {\n const options = config.options;\n const colors = options.colors && options.colors.length > 0 ? options.colors : DEFAULT_COLORS;\n const accent = options.accentColor ?? DEFAULT_ACCENT;\n const pixelRatio =\n options.pixelRatio ??\n Math.min(typeof devicePixelRatio === \"number\" && devicePixelRatio > 0 ? devicePixelRatio : 1, 2);\n\n let destroyed = false;\n let overlayOpen = false;\n // Bumped on every open and close: a submit resolving for an earlier overlay\n // session (sent, then closed and reopened mid-flight) must not paint its\n // outcome into, or auto-close, the new draft.\n let overlaySession = 0;\n let capturing = false;\n let sending = false;\n // One idempotency key per draft (the contract dedupes on it), so retrying a\n // failed send cannot store the same feedback twice; re-minted on each open.\n let draftFeedbackId = uuidV7();\n let sourceCanvas: HTMLCanvasElement | undefined;\n let activeColor = colors[0] ?? DEFAULT_COLORS[0]!;\n let penWidth = PEN_WIDTH;\n let previousBodyOverflow: string | undefined;\n let closeTimer: ReturnType<typeof setTimeout> | undefined;\n let drawContext: CanvasRenderingContext2D | null | undefined;\n const strokes = createStrokeStore();\n\n const report = (error: unknown): void => {\n try {\n config.onError(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n const safely = (fn: () => void): void => {\n try {\n fn();\n } catch (error) {\n report(error);\n }\n };\n\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-devteam-feedback\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = buildStyles(options, accent);\n\n const container = element(\"div\", \"container\");\n const fab = element(\"button\", \"fab\");\n fab.type = \"button\";\n fab.textContent = options.buttonLabel ?? \"Feedback\";\n\n const overlay = element(\"div\", \"overlay\");\n overlay.hidden = true;\n const backdrop = element(\"div\", \"backdrop\");\n const panel = element(\"div\", \"panel\");\n panel.setAttribute(\"role\", \"dialog\");\n panel.setAttribute(\"aria-modal\", \"true\");\n\n const header = element(\"div\", \"header\");\n const title = element(\"div\", \"title\");\n title.textContent = options.title ?? \"Send feedback\";\n const closeButton = element(\"button\", \"close\");\n closeButton.type = \"button\";\n closeButton.setAttribute(\"aria-label\", \"Close\");\n closeButton.textContent = \"×\";\n header.append(title, closeButton);\n\n const preview = element(\"div\", \"preview\");\n const drawCanvas = document.createElement(\"canvas\");\n drawCanvas.className = \"draw\";\n preview.appendChild(drawCanvas);\n\n const tools = element(\"div\", \"tools\");\n const swatches = colors.map((color) => {\n const swatch = element(\"button\", \"swatch\");\n swatch.type = \"button\";\n swatch.style.background = color;\n swatch.setAttribute(\"aria-label\", `Pen color ${color}`);\n swatch.addEventListener(\"click\", () => safely(() => selectColor(color)));\n tools.appendChild(swatch);\n return { color, swatch };\n });\n const undoButton = element(\"button\", \"tool undo\");\n undoButton.type = \"button\";\n undoButton.textContent = \"Undo\";\n const clearButton = element(\"button\", \"tool clear\");\n clearButton.type = \"button\";\n clearButton.textContent = \"Clear\";\n tools.append(undoButton, clearButton);\n\n const comment = document.createElement(\"textarea\");\n comment.className = \"comment\";\n comment.placeholder = options.commentPlaceholder ?? \"What went wrong, or what could be better?\";\n comment.maxLength = MAX_TEXT_LENGTH;\n\n const error = element(\"div\", \"error\");\n error.hidden = true;\n const success = element(\"div\", \"success\");\n success.hidden = true;\n success.textContent = options.successMessage ?? \"Thanks for the feedback!\";\n\n const actions = element(\"div\", \"actions\");\n const cancelButton = element(\"button\", \"cancel\");\n cancelButton.type = \"button\";\n cancelButton.textContent = \"Cancel\";\n const submitButton = element(\"button\", \"submit\");\n submitButton.type = \"button\";\n submitButton.textContent = options.submitLabel ?? \"Send\";\n actions.append(cancelButton, submitButton);\n\n panel.append(header, preview, tools, comment, error, success, actions);\n overlay.append(backdrop, panel);\n container.append(fab, overlay);\n shadow.append(style, container);\n\n if (document.body) {\n document.body.appendChild(host);\n } else {\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n if (!destroyed) document.body?.appendChild(host);\n },\n { once: true },\n );\n }\n\n function selectColor(color: string): void {\n activeColor = color;\n for (const entry of swatches) {\n entry.swatch.classList.toggle(\"active\", entry.color === color);\n }\n }\n selectColor(activeColor);\n\n function context2d(): CanvasRenderingContext2D | null {\n if (drawContext === undefined) {\n try {\n drawContext = drawCanvas.getContext(\"2d\");\n } catch {\n drawContext = null;\n }\n }\n return drawContext;\n }\n\n function repaint(): void {\n const ctx = context2d();\n if (!ctx) return;\n ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);\n drawStrokes(ctx, strokes.list());\n }\n\n function pointFromEvent(event: Event): StrokePoint | undefined {\n const { clientX, clientY } = event as { clientX?: unknown; clientY?: unknown };\n if (typeof clientX !== \"number\" || typeof clientY !== \"number\") return undefined;\n const rect = drawCanvas.getBoundingClientRect();\n return mapToImagePoint(clientX, clientY, rect, drawCanvas.width, drawCanvas.height);\n }\n\n function withPointerCapture(event: Event, action: \"set\" | \"release\"): void {\n const pointerId = (event as { pointerId?: unknown }).pointerId;\n if (typeof pointerId !== \"number\") return;\n const method = action === \"set\" ? drawCanvas.setPointerCapture : drawCanvas.releasePointerCapture;\n if (typeof method !== \"function\") return;\n try {\n method.call(drawCanvas, pointerId);\n } catch {\n // pointer capture is a nicety; drawing works without it\n }\n }\n\n const onPointerDown = (event: Event): void =>\n safely(() => {\n if (!sourceCanvas || sending) return;\n const point = pointFromEvent(event);\n if (!point) return;\n if (typeof event.preventDefault === \"function\") event.preventDefault();\n withPointerCapture(event, \"set\");\n strokes.begin(activeColor, penWidth, point);\n repaint();\n });\n const onPointerMove = (event: Event): void =>\n safely(() => {\n if (!strokes.isDrawing()) return;\n const point = pointFromEvent(event);\n if (!point) return;\n strokes.extend(point);\n repaint();\n });\n const onPointerEnd = (event: Event): void =>\n safely(() => {\n withPointerCapture(event, \"release\");\n strokes.end();\n });\n drawCanvas.addEventListener(\"pointerdown\", onPointerDown);\n drawCanvas.addEventListener(\"pointermove\", onPointerMove);\n drawCanvas.addEventListener(\"pointerup\", onPointerEnd);\n drawCanvas.addEventListener(\"pointercancel\", onPointerEnd);\n // When pointer capture is unavailable a release outside the canvas never\n // reaches it, which would leave the stroke armed (hover keeps extending it\n // and Undo/Clear are mid-drag no-ops). Capture retargets events to the\n // canvas, so these fallbacks only fire on the uncaptured path: leaving the\n // canvas ends the stroke, and the overlay spans the viewport, so any\n // release lands somewhere in the shadow root.\n drawCanvas.addEventListener(\"pointerleave\", onPointerEnd);\n shadow.addEventListener(\"pointerup\", onPointerEnd);\n\n undoButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.undo();\n repaint();\n }),\n );\n clearButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.clear();\n repaint();\n }),\n );\n\n const onKeyDown = (event: Event): void => {\n if ((event as { key?: unknown }).key === \"Escape\") safely(close);\n };\n\n function lockScroll(): void {\n if (previousBodyOverflow !== undefined) return;\n previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n\n function unlockScroll(): void {\n if (previousBodyOverflow === undefined) return;\n document.body.style.overflow = previousBodyOverflow;\n previousBodyOverflow = undefined;\n }\n\n function setError(message: string | undefined): void {\n error.hidden = message === undefined;\n error.textContent = message ?? \"\";\n }\n\n function setFormVisible(visible: boolean): void {\n const hasPreview = sourceCanvas !== undefined;\n preview.hidden = !visible || !hasPreview;\n tools.hidden = !visible || !hasPreview;\n comment.hidden = !visible;\n actions.hidden = !visible;\n }\n\n function showOverlay(canvas: HTMLCanvasElement | undefined): void {\n overlaySession += 1;\n draftFeedbackId = uuidV7();\n sourceCanvas = canvas;\n strokes.end();\n strokes.clear();\n comment.value = \"\";\n setError(undefined);\n success.hidden = true;\n submitButton.disabled = false;\n if (canvas) {\n canvas.classList.add(\"shot\");\n preview.insertBefore(canvas, drawCanvas);\n drawCanvas.width = canvas.width;\n drawCanvas.height = canvas.height;\n penWidth = PEN_WIDTH * (pixelRatio > 0 ? pixelRatio : 1);\n repaint();\n }\n selectColor(colors[0] ?? DEFAULT_COLORS[0]!);\n setFormVisible(true);\n overlay.hidden = false;\n overlayOpen = true;\n lockScroll();\n document.addEventListener(\"keydown\", onKeyDown);\n }\n\n function close(): void {\n if (!overlayOpen) return;\n overlaySession += 1;\n overlayOpen = false;\n overlay.hidden = true;\n document.removeEventListener(\"keydown\", onKeyDown);\n unlockScroll();\n if (closeTimer !== undefined) {\n clearTimeout(closeTimer);\n closeTimer = undefined;\n }\n sending = false;\n if (sourceCanvas) {\n sourceCanvas.remove();\n sourceCanvas = undefined;\n }\n strokes.end();\n strokes.clear();\n }\n\n function open(): void {\n if (destroyed || overlayOpen || capturing) return;\n capturing = true;\n void Promise.resolve()\n .then(() => deps.capture(host, pixelRatio))\n .then(\n (canvas) => {\n capturing = false;\n if (destroyed) return;\n showOverlay(canvas);\n },\n (captureError: unknown) => {\n capturing = false;\n if (destroyed) return;\n report(captureError);\n showOverlay(undefined);\n },\n );\n }\n\n function buildBody(text: string, screenshot: ExportedImage | null): string {\n const body: Record<string, unknown> = {\n feedback_id: draftFeedbackId,\n text: text.slice(0, MAX_TEXT_LENGTH),\n };\n if (screenshot) {\n body.screenshot = screenshot.base64;\n body.screenshot_content_type = screenshot.contentType;\n }\n const distinctId = config.getDistinctId();\n if (distinctId) body.distinct_id = distinctId;\n const sessionId = config.getSessionId();\n if (sessionId) body.session_id = sessionId;\n if (typeof location !== \"undefined\" && location.href) {\n body.page_url = location.href.slice(0, MAX_PAGE_URL_LENGTH);\n }\n body.context = config.getContext();\n return JSON.stringify(body);\n }\n\n async function submit(): Promise<void> {\n if (sending || !overlayOpen) return;\n const text = comment.value.trim();\n if (text.length === 0) {\n setError(\"Please add a short comment before sending.\");\n return;\n }\n const session = overlaySession;\n const sameSession = (): boolean => !destroyed && overlayOpen && overlaySession === session;\n sending = true;\n submitButton.disabled = true;\n setError(undefined);\n try {\n let screenshot: ExportedImage | null = null;\n if (sourceCanvas) {\n screenshot = await deps.exportAnnotated(sourceCanvas, strokes.list(), {\n maxBytes: MAX_SCREENSHOT_BYTES,\n });\n }\n if (!config.fetch) throw new Error(\"fetch is not available in this environment\");\n const response = await config.fetch(config.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-DevTeam-Key\": config.key },\n body: buildBody(text, screenshot),\n });\n if (!sameSession()) return;\n if (response.ok) {\n setFormVisible(false);\n success.hidden = false;\n closeTimer = setTimeout(() => safely(close), SUCCESS_CLOSE_DELAY_MS);\n } else {\n setError(\"Could not send feedback. Please try again.\");\n }\n } catch (submitError) {\n report(submitError);\n if (sameSession()) setError(\"Could not send feedback. Please try again.\");\n } finally {\n if (overlaySession === session) {\n sending = false;\n submitButton.disabled = false;\n }\n }\n }\n\n fab.addEventListener(\"click\", () => safely(open));\n closeButton.addEventListener(\"click\", () => safely(close));\n cancelButton.addEventListener(\"click\", () => safely(close));\n backdrop.addEventListener(\"click\", () => safely(close));\n submitButton.addEventListener(\"click\", () => safely(() => void submit()));\n\n return {\n open: () => safely(open),\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n safely(() => {\n close();\n document.removeEventListener(\"keydown\", onKeyDown);\n host.remove();\n });\n },\n };\n}\n\nfunction element<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n node.className = className;\n return node;\n}\n\nfunction buildStyles(options: WebFeedbackOptions, accent: string): string {\n const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;\n const side = options.position === \"bottom-left\" ? \"left\" : \"right\";\n return `\n:host { all: initial; }\n* { box-sizing: border-box; }\n[hidden] { display: none !important; }\n.container {\n position: fixed;\n inset: 0;\n z-index: ${zIndex};\n pointer-events: none;\n font-family: system-ui, -apple-system, sans-serif;\n color: #1f2933;\n}\n.fab {\n position: absolute;\n bottom: 20px;\n ${side}: 20px;\n pointer-events: auto;\n border: none;\n border-radius: 999px;\n padding: 10px 18px;\n background: ${accent};\n color: #fff;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n}\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgba(15, 23, 42, 0.55);\n}\n.panel {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: min(560px, calc(100vw - 32px));\n max-height: calc(100vh - 32px);\n overflow: auto;\n background: #fff;\n border-radius: 12px;\n padding: 16px;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);\n}\n.header { display: flex; align-items: center; justify-content: space-between; }\n.title { font-size: 16px; font-weight: 600; }\n.close {\n border: none;\n background: none;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n color: #52606d;\n padding: 4px 8px;\n}\n.preview {\n position: relative;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n overflow: hidden;\n background: #f5f7fa;\n}\n.preview .shot { display: block; width: 100%; height: auto; }\n.preview .draw {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n cursor: crosshair;\n touch-action: none;\n}\n.tools { display: flex; align-items: center; gap: 8px; }\n.swatch {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n border: 2px solid transparent;\n cursor: pointer;\n padding: 0;\n}\n.swatch.active { border-color: #1f2933; }\n.tool {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 6px;\n padding: 4px 10px;\n font-size: 13px;\n cursor: pointer;\n}\n.comment {\n width: 100%;\n min-height: 72px;\n resize: vertical;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n}\n.error { color: #b91c1c; font-size: 13px; }\n.success { color: #15803d; font-size: 14px; font-weight: 600; }\n.actions { display: flex; justify-content: flex-end; gap: 8px; }\n.cancel {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 8px;\n padding: 8px 14px;\n font-size: 14px;\n cursor: pointer;\n}\n.submit {\n border: none;\n background: ${accent};\n color: #fff;\n border-radius: 8px;\n padding: 8px 16px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n.submit:disabled { opacity: 0.6; cursor: default; }\n`;\n}\n","import { toCanvas } from \"html-to-image\";\n\n// html-to-image has no clip option, so the visible viewport is captured by\n// sizing the render to the window and translating the cloned document up and\n// left by the current scroll offset.\nexport async function captureViewport(exclude: Node, pixelRatio: number): Promise<HTMLCanvasElement> {\n return toCanvas(document.documentElement, {\n filter: (node) => node !== exclude,\n pixelRatio,\n width: window.innerWidth,\n height: window.innerHeight,\n style: {\n transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`,\n },\n });\n}\n","import type { Stroke } from \"./strokes\";\n\nexport interface ExportedImage {\n base64: string;\n contentType: \"image/png\" | \"image/jpeg\";\n}\n\nexport interface ExportImageOptions {\n maxBytes: number;\n}\n\nconst JPEG_QUALITY = 0.85;\nconst DOWNSCALE_FACTOR = 0.7;\nconst MIN_EXPORT_DIMENSION = 200;\n\nexport function drawStrokes(ctx: CanvasRenderingContext2D, strokes: readonly Stroke[]): void {\n for (const stroke of strokes) {\n const [first, ...rest] = stroke.points;\n if (!first) continue;\n ctx.strokeStyle = stroke.color;\n ctx.lineWidth = stroke.width;\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n ctx.beginPath();\n ctx.moveTo(first.x, first.y);\n if (rest.length === 0) ctx.lineTo(first.x, first.y);\n for (const point of rest) ctx.lineTo(point.x, point.y);\n ctx.stroke();\n }\n}\n\n// Composites the full-resolution screenshot with the strokes, then encodes to\n// PNG; when the PNG is over the byte budget it falls back to JPEG and, if\n// still too large, downscales in 0.7 steps until it fits. Returns null when no\n// encoding fits or the environment has no working 2d canvas.\nexport async function exportAnnotatedImage(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: ExportImageOptions,\n): Promise<ExportedImage | null> {\n try {\n const composed = compose(source, strokes);\n if (!composed) return null;\n\n const png = await encode(composed, \"image/png\");\n if (png && png.size <= options.maxBytes) {\n return { base64: await blobToBase64(png), contentType: \"image/png\" };\n }\n\n let canvas = composed;\n let jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n while (\n jpeg &&\n jpeg.size > options.maxBytes &&\n Math.min(canvas.width, canvas.height) * DOWNSCALE_FACTOR >= MIN_EXPORT_DIMENSION\n ) {\n const smaller = downscale(canvas, DOWNSCALE_FACTOR);\n if (!smaller) break;\n canvas = smaller;\n jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n }\n if (jpeg && jpeg.size <= options.maxBytes) {\n return { base64: await blobToBase64(jpeg), contentType: \"image/jpeg\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction compose(source: HTMLCanvasElement, strokes: readonly Stroke[]): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = source.width;\n canvas.height = source.height;\n const ctx = get2dContext(canvas);\n if (!ctx || canvas.width === 0 || canvas.height === 0) return null;\n // JPEG has no alpha channel and would composite transparency onto black.\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(source, 0, 0);\n drawStrokes(ctx, strokes);\n return canvas;\n}\n\nfunction downscale(source: HTMLCanvasElement, factor: number): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(source.width * factor));\n canvas.height = Math.max(1, Math.round(source.height * factor));\n const ctx = get2dContext(canvas);\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\nfunction get2dContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {\n try {\n return canvas.getContext(\"2d\");\n } catch {\n return null;\n }\n}\n\nfunction encode(canvas: HTMLCanvasElement, type: string, quality?: number): Promise<Blob | null> {\n return new Promise((resolve) => {\n try {\n canvas.toBlob((blob) => resolve(blob), type, quality);\n } catch {\n resolve(null);\n }\n });\n}\n\nexport async function blobToBase64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n const chunkSize = 0x8000;\n for (let i = 0; i < bytes.length; i += chunkSize) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));\n }\n return btoa(binary);\n}\n","export interface StrokePoint {\n x: number;\n y: number;\n}\n\nexport interface Stroke {\n points: StrokePoint[];\n color: string;\n width: number;\n}\n\nexport interface StrokeStore {\n begin(color: string, width: number, point: StrokePoint): void;\n extend(point: StrokePoint): void;\n end(): void;\n undo(): void;\n clear(): void;\n list(): readonly Stroke[];\n isDrawing(): boolean;\n}\n\n// Each stroke snapshots its color and width when it starts, so a later color\n// change never restyles strokes already on the canvas. Undo and clear are\n// no-ops mid-drag to keep the active stroke consistent.\nexport function createStrokeStore(): StrokeStore {\n const strokes: Stroke[] = [];\n let active: Stroke | undefined;\n return {\n begin(color, width, point) {\n if (active) return;\n active = { points: [point], color, width };\n strokes.push(active);\n },\n extend(point) {\n active?.points.push(point);\n },\n end() {\n active = undefined;\n },\n undo() {\n if (active) return;\n strokes.pop();\n },\n clear() {\n if (active) return;\n strokes.length = 0;\n },\n list: () => strokes,\n isDrawing: () => active !== undefined,\n };\n}\n\nexport interface DisplayRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n// Strokes live in the screenshot's own pixel space so they land at the same\n// spot in the full-resolution export as they did on the scaled-down preview.\n// A degenerate rect (jsdom reports zero sizes) maps 1:1 instead of dividing by zero.\nexport function mapToImagePoint(\n clientX: number,\n clientY: number,\n rect: DisplayRect,\n imageWidth: number,\n imageHeight: number,\n): StrokePoint {\n const scaleX = rect.width > 0 && imageWidth > 0 ? imageWidth / rect.width : 1;\n const scaleY = rect.height > 0 && imageHeight > 0 ? imageHeight / rect.height : 1;\n return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };\n}\n"],"mappings":";AAAA,SAAS,cAAiD;;;ACA1D,SAAS,gBAAgB;AAKzB,eAAsB,gBAAgB,SAAe,YAAgD;AACnG,SAAO,SAAS,SAAS,iBAAiB;AAAA,IACxC,QAAQ,CAAC,SAAS,SAAS;AAAA,IAC3B;AAAA,IACA,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,OAAO;AAAA,MACL,WAAW,aAAa,CAAC,OAAO,OAAO,OAAO,CAAC,OAAO,OAAO;AAAA,IAC/D;AAAA,EACF,CAAC;AACH;;;ACJA,IAAM,eAAe;AACrB,IAAM,mBAAmB;AACzB,IAAM,uBAAuB;AAEtB,SAAS,YAAY,KAA+B,SAAkC;AAC3F,aAAW,UAAU,SAAS;AAC5B,UAAM,CAAC,OAAO,GAAG,IAAI,IAAI,OAAO;AAChC,QAAI,CAAC,MAAO;AACZ,QAAI,cAAc,OAAO;AACzB,QAAI,YAAY,OAAO;AACvB,QAAI,UAAU;AACd,QAAI,WAAW;AACf,QAAI,UAAU;AACd,QAAI,OAAO,MAAM,GAAG,MAAM,CAAC;AAC3B,QAAI,KAAK,WAAW,EAAG,KAAI,OAAO,MAAM,GAAG,MAAM,CAAC;AAClD,eAAW,SAAS,KAAM,KAAI,OAAO,MAAM,GAAG,MAAM,CAAC;AACrD,QAAI,OAAO;AAAA,EACb;AACF;AAMA,eAAsB,qBACpB,QACA,SACA,SAC+B;AAC/B,MAAI;AACF,UAAM,WAAW,QAAQ,QAAQ,OAAO;AACxC,QAAI,CAAC,SAAU,QAAO;AAEtB,UAAM,MAAM,MAAM,OAAO,UAAU,WAAW;AAC9C,QAAI,OAAO,IAAI,QAAQ,QAAQ,UAAU;AACvC,aAAO,EAAE,QAAQ,MAAM,aAAa,GAAG,GAAG,aAAa,YAAY;AAAA,IACrE;AAEA,QAAI,SAAS;AACb,QAAI,OAAO,MAAM,OAAO,QAAQ,cAAc,YAAY;AAC1D,WACE,QACA,KAAK,OAAO,QAAQ,YACpB,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,IAAI,oBAAoB,sBAC5D;AACA,YAAM,UAAU,UAAU,QAAQ,gBAAgB;AAClD,UAAI,CAAC,QAAS;AACd,eAAS;AACT,aAAO,MAAM,OAAO,QAAQ,cAAc,YAAY;AAAA,IACxD;AACA,QAAI,QAAQ,KAAK,QAAQ,QAAQ,UAAU;AACzC,aAAO,EAAE,QAAQ,MAAM,aAAa,IAAI,GAAG,aAAa,aAAa;AAAA,IACvE;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,QAA2B,SAAsD;AAChG,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ,OAAO;AACtB,SAAO,SAAS,OAAO;AACvB,QAAM,MAAM,aAAa,MAAM;AAC/B,MAAI,CAAC,OAAO,OAAO,UAAU,KAAK,OAAO,WAAW,EAAG,QAAO;AAE9D,MAAI,YAAY;AAChB,MAAI,SAAS,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAC9C,MAAI,UAAU,QAAQ,GAAG,CAAC;AAC1B,cAAY,KAAK,OAAO;AACxB,SAAO;AACT;AAEA,SAAS,UAAU,QAA2B,QAA0C;AACtF,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,MAAM,CAAC;AAC5D,SAAO,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,SAAS,MAAM,CAAC;AAC9D,QAAM,MAAM,aAAa,MAAM;AAC/B,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,UAAU,QAAQ,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AACvD,SAAO;AACT;AAEA,SAAS,aAAa,QAA4D;AAChF,MAAI;AACF,WAAO,OAAO,WAAW,IAAI;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,QAA2B,MAAc,SAAwC;AAC/F,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI;AACF,aAAO,OAAO,CAAC,SAAS,QAAQ,IAAI,GAAG,MAAM,OAAO;AAAA,IACtD,QAAQ;AACN,cAAQ,IAAI;AAAA,IACd;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,aAAa,MAA6B;AAC9D,QAAM,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AACrD,MAAI,SAAS;AACb,QAAM,YAAY;AAClB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,cAAU,OAAO,aAAa,GAAG,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;AAAA,EACnE;AACA,SAAO,KAAK,MAAM;AACpB;;;AChGO,SAAS,oBAAiC;AAC/C,QAAM,UAAoB,CAAC;AAC3B,MAAI;AACJ,SAAO;AAAA,IACL,MAAM,OAAO,OAAO,OAAO;AACzB,UAAI,OAAQ;AACZ,eAAS,EAAE,QAAQ,CAAC,KAAK,GAAG,OAAO,MAAM;AACzC,cAAQ,KAAK,MAAM;AAAA,IACrB;AAAA,IACA,OAAO,OAAO;AACZ,cAAQ,OAAO,KAAK,KAAK;AAAA,IAC3B;AAAA,IACA,MAAM;AACJ,eAAS;AAAA,IACX;AAAA,IACA,OAAO;AACL,UAAI,OAAQ;AACZ,cAAQ,IAAI;AAAA,IACd;AAAA,IACA,QAAQ;AACN,UAAI,OAAQ;AACZ,cAAQ,SAAS;AAAA,IACnB;AAAA,IACA,MAAM,MAAM;AAAA,IACZ,WAAW,MAAM,WAAW;AAAA,EAC9B;AACF;AAYO,SAAS,gBACd,SACA,SACA,MACA,YACA,aACa;AACb,QAAM,SAAS,KAAK,QAAQ,KAAK,aAAa,IAAI,aAAa,KAAK,QAAQ;AAC5E,QAAM,SAAS,KAAK,SAAS,KAAK,cAAc,IAAI,cAAc,KAAK,SAAS;AAChF,SAAO,EAAE,IAAI,UAAU,KAAK,QAAQ,QAAQ,IAAI,UAAU,KAAK,OAAO,OAAO;AAC/E;;;AHvCA,IAAM,iBAAiB,CAAC,WAAW,WAAW,WAAW,SAAS;AAClE,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,YAAY;AAClB,IAAM,uBAAuB,IAAI,OAAO;AACxC,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,yBAAyB;AAE/B,IAAM,cAA4B;AAAA,EAChC,SAAS;AAAA,EACT,iBAAiB;AACnB;AAEO,SAAS,cAAc,QAAwB,OAAqB,aAA6B;AACtG,QAAM,UAAU,OAAO;AACvB,QAAM,SAAS,QAAQ,UAAU,QAAQ,OAAO,SAAS,IAAI,QAAQ,SAAS;AAC9E,QAAM,SAAS,QAAQ,eAAe;AACtC,QAAM,aACJ,QAAQ,cACR,KAAK,IAAI,OAAO,qBAAqB,YAAY,mBAAmB,IAAI,mBAAmB,GAAG,CAAC;AAEjG,MAAI,YAAY;AAChB,MAAI,cAAc;AAIlB,MAAI,iBAAiB;AACrB,MAAI,YAAY;AAChB,MAAI,UAAU;AAGd,MAAI,kBAAkB,OAAO;AAC7B,MAAI;AACJ,MAAI,cAAc,OAAO,CAAC,KAAK,eAAe,CAAC;AAC/C,MAAI,WAAW;AACf,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,kBAAkB;AAElC,QAAM,SAAS,CAACA,WAAyB;AACvC,QAAI;AACF,aAAO,QAAQA,MAAK;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,SAAS,CAAC,OAAyB;AACvC,QAAI;AACF,SAAG;AAAA,IACL,SAASA,QAAO;AACd,aAAOA,MAAK;AAAA,IACd;AAAA,EACF;AAEA,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,aAAa,yBAAyB,EAAE;AAC7C,QAAM,SAAS,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAEjD,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,cAAc,YAAY,SAAS,MAAM;AAE/C,QAAM,YAAY,QAAQ,OAAO,WAAW;AAC5C,QAAM,MAAM,QAAQ,UAAU,KAAK;AACnC,MAAI,OAAO;AACX,MAAI,cAAc,QAAQ,eAAe;AAEzC,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,UAAQ,SAAS;AACjB,QAAM,WAAW,QAAQ,OAAO,UAAU;AAC1C,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,aAAa,QAAQ,QAAQ;AACnC,QAAM,aAAa,cAAc,MAAM;AAEvC,QAAM,SAAS,QAAQ,OAAO,QAAQ;AACtC,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,cAAc,QAAQ,SAAS;AACrC,QAAM,cAAc,QAAQ,UAAU,OAAO;AAC7C,cAAY,OAAO;AACnB,cAAY,aAAa,cAAc,OAAO;AAC9C,cAAY,cAAc;AAC1B,SAAO,OAAO,OAAO,WAAW;AAEhC,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,QAAM,aAAa,SAAS,cAAc,QAAQ;AAClD,aAAW,YAAY;AACvB,UAAQ,YAAY,UAAU;AAE9B,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,WAAW,OAAO,IAAI,CAAC,UAAU;AACrC,UAAM,SAAS,QAAQ,UAAU,QAAQ;AACzC,WAAO,OAAO;AACd,WAAO,MAAM,aAAa;AAC1B,WAAO,aAAa,cAAc,aAAa,KAAK,EAAE;AACtD,WAAO,iBAAiB,SAAS,MAAM,OAAO,MAAM,YAAY,KAAK,CAAC,CAAC;AACvE,UAAM,YAAY,MAAM;AACxB,WAAO,EAAE,OAAO,OAAO;AAAA,EACzB,CAAC;AACD,QAAM,aAAa,QAAQ,UAAU,WAAW;AAChD,aAAW,OAAO;AAClB,aAAW,cAAc;AACzB,QAAM,cAAc,QAAQ,UAAU,YAAY;AAClD,cAAY,OAAO;AACnB,cAAY,cAAc;AAC1B,QAAM,OAAO,YAAY,WAAW;AAEpC,QAAM,UAAU,SAAS,cAAc,UAAU;AACjD,UAAQ,YAAY;AACpB,UAAQ,cAAc,QAAQ,sBAAsB;AACpD,UAAQ,YAAY;AAEpB,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,SAAS;AACf,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,UAAQ,SAAS;AACjB,UAAQ,cAAc,QAAQ,kBAAkB;AAEhD,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,QAAM,eAAe,QAAQ,UAAU,QAAQ;AAC/C,eAAa,OAAO;AACpB,eAAa,cAAc;AAC3B,QAAM,eAAe,QAAQ,UAAU,QAAQ;AAC/C,eAAa,OAAO;AACpB,eAAa,cAAc,QAAQ,eAAe;AAClD,UAAQ,OAAO,cAAc,YAAY;AAEzC,QAAM,OAAO,QAAQ,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO;AACrE,UAAQ,OAAO,UAAU,KAAK;AAC9B,YAAU,OAAO,KAAK,OAAO;AAC7B,SAAO,OAAO,OAAO,SAAS;AAE9B,MAAI,SAAS,MAAM;AACjB,aAAS,KAAK,YAAY,IAAI;AAAA,EAChC,OAAO;AACL,aAAS;AAAA,MACP;AAAA,MACA,MAAM;AACJ,YAAI,CAAC,UAAW,UAAS,MAAM,YAAY,IAAI;AAAA,MACjD;AAAA,MACA,EAAE,MAAM,KAAK;AAAA,IACf;AAAA,EACF;AAEA,WAAS,YAAY,OAAqB;AACxC,kBAAc;AACd,eAAW,SAAS,UAAU;AAC5B,YAAM,OAAO,UAAU,OAAO,UAAU,MAAM,UAAU,KAAK;AAAA,IAC/D;AAAA,EACF;AACA,cAAY,WAAW;AAEvB,WAAS,YAA6C;AACpD,QAAI,gBAAgB,QAAW;AAC7B,UAAI;AACF,sBAAc,WAAW,WAAW,IAAI;AAAA,MAC1C,QAAQ;AACN,sBAAc;AAAA,MAChB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,WAAS,UAAgB;AACvB,UAAM,MAAM,UAAU;AACtB,QAAI,CAAC,IAAK;AACV,QAAI,UAAU,GAAG,GAAG,WAAW,OAAO,WAAW,MAAM;AACvD,gBAAY,KAAK,QAAQ,KAAK,CAAC;AAAA,EACjC;AAEA,WAAS,eAAe,OAAuC;AAC7D,UAAM,EAAE,SAAS,QAAQ,IAAI;AAC7B,QAAI,OAAO,YAAY,YAAY,OAAO,YAAY,SAAU,QAAO;AACvE,UAAM,OAAO,WAAW,sBAAsB;AAC9C,WAAO,gBAAgB,SAAS,SAAS,MAAM,WAAW,OAAO,WAAW,MAAM;AAAA,EACpF;AAEA,WAAS,mBAAmB,OAAc,QAAiC;AACzE,UAAM,YAAa,MAAkC;AACrD,QAAI,OAAO,cAAc,SAAU;AACnC,UAAM,SAAS,WAAW,QAAQ,WAAW,oBAAoB,WAAW;AAC5E,QAAI,OAAO,WAAW,WAAY;AAClC,QAAI;AACF,aAAO,KAAK,YAAY,SAAS;AAAA,IACnC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,gBAAgB,CAAC,UACrB,OAAO,MAAM;AACX,QAAI,CAAC,gBAAgB,QAAS;AAC9B,UAAM,QAAQ,eAAe,KAAK;AAClC,QAAI,CAAC,MAAO;AACZ,QAAI,OAAO,MAAM,mBAAmB,WAAY,OAAM,eAAe;AACrE,uBAAmB,OAAO,KAAK;AAC/B,YAAQ,MAAM,aAAa,UAAU,KAAK;AAC1C,YAAQ;AAAA,EACV,CAAC;AACH,QAAM,gBAAgB,CAAC,UACrB,OAAO,MAAM;AACX,QAAI,CAAC,QAAQ,UAAU,EAAG;AAC1B,UAAM,QAAQ,eAAe,KAAK;AAClC,QAAI,CAAC,MAAO;AACZ,YAAQ,OAAO,KAAK;AACpB,YAAQ;AAAA,EACV,CAAC;AACH,QAAM,eAAe,CAAC,UACpB,OAAO,MAAM;AACX,uBAAmB,OAAO,SAAS;AACnC,YAAQ,IAAI;AAAA,EACd,CAAC;AACH,aAAW,iBAAiB,eAAe,aAAa;AACxD,aAAW,iBAAiB,eAAe,aAAa;AACxD,aAAW,iBAAiB,aAAa,YAAY;AACrD,aAAW,iBAAiB,iBAAiB,YAAY;AAOzD,aAAW,iBAAiB,gBAAgB,YAAY;AACxD,SAAO,iBAAiB,aAAa,YAAY;AAEjD,aAAW;AAAA,IAAiB;AAAA,IAAS,MACnC,OAAO,MAAM;AACX,cAAQ,KAAK;AACb,cAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,cAAY;AAAA,IAAiB;AAAA,IAAS,MACpC,OAAO,MAAM;AACX,cAAQ,MAAM;AACd,cAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,QAAM,YAAY,CAAC,UAAuB;AACxC,QAAK,MAA4B,QAAQ,SAAU,QAAO,KAAK;AAAA,EACjE;AAEA,WAAS,aAAmB;AAC1B,QAAI,yBAAyB,OAAW;AACxC,2BAAuB,SAAS,KAAK,MAAM;AAC3C,aAAS,KAAK,MAAM,WAAW;AAAA,EACjC;AAEA,WAAS,eAAqB;AAC5B,QAAI,yBAAyB,OAAW;AACxC,aAAS,KAAK,MAAM,WAAW;AAC/B,2BAAuB;AAAA,EACzB;AAEA,WAAS,SAAS,SAAmC;AACnD,UAAM,SAAS,YAAY;AAC3B,UAAM,cAAc,WAAW;AAAA,EACjC;AAEA,WAAS,eAAe,SAAwB;AAC9C,UAAM,aAAa,iBAAiB;AACpC,YAAQ,SAAS,CAAC,WAAW,CAAC;AAC9B,UAAM,SAAS,CAAC,WAAW,CAAC;AAC5B,YAAQ,SAAS,CAAC;AAClB,YAAQ,SAAS,CAAC;AAAA,EACpB;AAEA,WAAS,YAAY,QAA6C;AAChE,sBAAkB;AAClB,sBAAkB,OAAO;AACzB,mBAAe;AACf,YAAQ,IAAI;AACZ,YAAQ,MAAM;AACd,YAAQ,QAAQ;AAChB,aAAS,MAAS;AAClB,YAAQ,SAAS;AACjB,iBAAa,WAAW;AACxB,QAAI,QAAQ;AACV,aAAO,UAAU,IAAI,MAAM;AAC3B,cAAQ,aAAa,QAAQ,UAAU;AACvC,iBAAW,QAAQ,OAAO;AAC1B,iBAAW,SAAS,OAAO;AAC3B,iBAAW,aAAa,aAAa,IAAI,aAAa;AACtD,cAAQ;AAAA,IACV;AACA,gBAAY,OAAO,CAAC,KAAK,eAAe,CAAC,CAAE;AAC3C,mBAAe,IAAI;AACnB,YAAQ,SAAS;AACjB,kBAAc;AACd,eAAW;AACX,aAAS,iBAAiB,WAAW,SAAS;AAAA,EAChD;AAEA,WAAS,QAAc;AACrB,QAAI,CAAC,YAAa;AAClB,sBAAkB;AAClB,kBAAc;AACd,YAAQ,SAAS;AACjB,aAAS,oBAAoB,WAAW,SAAS;AACjD,iBAAa;AACb,QAAI,eAAe,QAAW;AAC5B,mBAAa,UAAU;AACvB,mBAAa;AAAA,IACf;AACA,cAAU;AACV,QAAI,cAAc;AAChB,mBAAa,OAAO;AACpB,qBAAe;AAAA,IACjB;AACA,YAAQ,IAAI;AACZ,YAAQ,MAAM;AAAA,EAChB;AAEA,WAAS,OAAa;AACpB,QAAI,aAAa,eAAe,UAAW;AAC3C,gBAAY;AACZ,SAAK,QAAQ,QAAQ,EAClB,KAAK,MAAM,KAAK,QAAQ,MAAM,UAAU,CAAC,EACzC;AAAA,MACC,CAAC,WAAW;AACV,oBAAY;AACZ,YAAI,UAAW;AACf,oBAAY,MAAM;AAAA,MACpB;AAAA,MACA,CAAC,iBAA0B;AACzB,oBAAY;AACZ,YAAI,UAAW;AACf,eAAO,YAAY;AACnB,oBAAY,MAAS;AAAA,MACvB;AAAA,IACF;AAAA,EACJ;AAEA,WAAS,UAAU,MAAc,YAA0C;AACzE,UAAM,OAAgC;AAAA,MACpC,aAAa;AAAA,MACb,MAAM,KAAK,MAAM,GAAG,eAAe;AAAA,IACrC;AACA,QAAI,YAAY;AACd,WAAK,aAAa,WAAW;AAC7B,WAAK,0BAA0B,WAAW;AAAA,IAC5C;AACA,UAAM,aAAa,OAAO,cAAc;AACxC,QAAI,WAAY,MAAK,cAAc;AACnC,UAAM,YAAY,OAAO,aAAa;AACtC,QAAI,UAAW,MAAK,aAAa;AACjC,QAAI,OAAO,aAAa,eAAe,SAAS,MAAM;AACpD,WAAK,WAAW,SAAS,KAAK,MAAM,GAAG,mBAAmB;AAAA,IAC5D;AACA,SAAK,UAAU,OAAO,WAAW;AACjC,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B;AAEA,iBAAe,SAAwB;AACrC,QAAI,WAAW,CAAC,YAAa;AAC7B,UAAM,OAAO,QAAQ,MAAM,KAAK;AAChC,QAAI,KAAK,WAAW,GAAG;AACrB,eAAS,4CAA4C;AACrD;AAAA,IACF;AACA,UAAM,UAAU;AAChB,UAAM,cAAc,MAAe,CAAC,aAAa,eAAe,mBAAmB;AACnF,cAAU;AACV,iBAAa,WAAW;AACxB,aAAS,MAAS;AAClB,QAAI;AACF,UAAI,aAAmC;AACvC,UAAI,cAAc;AAChB,qBAAa,MAAM,KAAK,gBAAgB,cAAc,QAAQ,KAAK,GAAG;AAAA,UACpE,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AACA,UAAI,CAAC,OAAO,MAAO,OAAM,IAAI,MAAM,4CAA4C;AAC/E,YAAM,WAAW,MAAM,OAAO,MAAM,OAAO,KAAK;AAAA,QAC9C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,iBAAiB,OAAO,IAAI;AAAA,QAC3E,MAAM,UAAU,MAAM,UAAU;AAAA,MAClC,CAAC;AACD,UAAI,CAAC,YAAY,EAAG;AACpB,UAAI,SAAS,IAAI;AACf,uBAAe,KAAK;AACpB,gBAAQ,SAAS;AACjB,qBAAa,WAAW,MAAM,OAAO,KAAK,GAAG,sBAAsB;AAAA,MACrE,OAAO;AACL,iBAAS,4CAA4C;AAAA,MACvD;AAAA,IACF,SAAS,aAAa;AACpB,aAAO,WAAW;AAClB,UAAI,YAAY,EAAG,UAAS,4CAA4C;AAAA,IAC1E,UAAE;AACA,UAAI,mBAAmB,SAAS;AAC9B,kBAAU;AACV,qBAAa,WAAW;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,MAAI,iBAAiB,SAAS,MAAM,OAAO,IAAI,CAAC;AAChD,cAAY,iBAAiB,SAAS,MAAM,OAAO,KAAK,CAAC;AACzD,eAAa,iBAAiB,SAAS,MAAM,OAAO,KAAK,CAAC;AAC1D,WAAS,iBAAiB,SAAS,MAAM,OAAO,KAAK,CAAC;AACtD,eAAa,iBAAiB,SAAS,MAAM,OAAO,MAAM,KAAK,OAAO,CAAC,CAAC;AAExE,SAAO;AAAA,IACL,MAAM,MAAM,OAAO,IAAI;AAAA,IACvB,SAAS,MAAM;AACb,UAAI,UAAW;AACf,kBAAY;AACZ,aAAO,MAAM;AACX,cAAM;AACN,iBAAS,oBAAoB,WAAW,SAAS;AACjD,aAAK,OAAO;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,SAAS,QACP,KACA,WAC0B;AAC1B,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,YAAY;AACjB,SAAO;AACT;AAEA,SAAS,YAAY,SAA6B,QAAwB;AACxE,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,OAAO,QAAQ,aAAa,gBAAgB,SAAS;AAC3D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAOI,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQf,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,gBAKQ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAoGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUtB;","names":["error"]}
|
|
1
|
+
{"version":3,"sources":["../src/feedback/index.ts","../src/feedback/capture.ts","../src/feedback/exportImage.ts","../src/feedback/strokes.ts"],"sourcesContent":["import { uuidV7, type EventContext, type FetchLike } from \"@getdevteam/analytics-core\";\nimport { captureViewport } from \"./capture\";\nimport { drawStrokes, exportAnnotatedImage, type ExportedImage } from \"./exportImage\";\nimport { createStrokeStore, mapToImagePoint, type StrokePoint } from \"./strokes\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nimport type { Stroke } from \"./strokes\";\n\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nexport { createStrokeStore, mapToImagePoint } from \"./strokes\";\nexport type { Stroke, StrokePoint, StrokeStore } from \"./strokes\";\nexport { exportAnnotatedImage } from \"./exportImage\";\nexport type { ExportedImage } from \"./exportImage\";\n\nexport interface FeedbackConfig {\n url: string;\n key: string;\n fetch: FetchLike | undefined;\n getDistinctId(): string;\n getSessionId(): string;\n getContext(): EventContext;\n options: WebFeedbackOptions;\n onError(error: unknown): void;\n}\n\nexport interface FeedbackDeps {\n capture(exclude: HTMLElement, pixelRatio: number): Promise<HTMLCanvasElement>;\n exportAnnotated(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: { maxBytes: number },\n ): Promise<ExportedImage | null>;\n}\n\nconst DEFAULT_COLORS = [\"#f44336\", \"#4caf50\", \"#2196f3\", \"#ffeb3b\"];\nconst DEFAULT_ACCENT = \"#5b9cf5\";\nconst DEFAULT_Z_INDEX = 2147483000;\nconst PEN_WIDTH = 5;\nconst MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;\nconst MAX_TEXT_LENGTH = 10000;\nconst MAX_PAGE_URL_LENGTH = 2048;\nconst SUCCESS_CLOSE_DELAY_MS = 1600;\n\nconst defaultDeps: FeedbackDeps = {\n capture: captureViewport,\n exportAnnotated: exportAnnotatedImage,\n};\n\nexport function mountFeedback(config: FeedbackConfig, deps: FeedbackDeps = defaultDeps): FeedbackHandle {\n const options = config.options;\n const colors = options.colors && options.colors.length > 0 ? options.colors : DEFAULT_COLORS;\n const accent = options.accentColor ?? DEFAULT_ACCENT;\n const pixelRatio =\n options.pixelRatio ??\n Math.min(typeof devicePixelRatio === \"number\" && devicePixelRatio > 0 ? devicePixelRatio : 1, 2);\n\n let destroyed = false;\n let overlayOpen = false;\n // Bumped on every open and close: a submit resolving for an earlier overlay\n // session (sent, then closed and reopened mid-flight) must not paint its\n // outcome into, or auto-close, the new draft.\n let overlaySession = 0;\n let capturing = false;\n let sending = false;\n // One idempotency key per draft (the contract dedupes on it), so retrying a\n // failed send cannot store the same feedback twice; re-minted on each open.\n let draftFeedbackId = uuidV7();\n let sourceCanvas: HTMLCanvasElement | undefined;\n let activeColor = colors[0] ?? DEFAULT_COLORS[0]!;\n let penWidth = PEN_WIDTH;\n let previousBodyOverflow: string | undefined;\n let closeTimer: ReturnType<typeof setTimeout> | undefined;\n let drawContext: CanvasRenderingContext2D | null | undefined;\n const strokes = createStrokeStore();\n\n const report = (error: unknown): void => {\n try {\n config.onError(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n const safely = (fn: () => void): void => {\n try {\n fn();\n } catch (error) {\n report(error);\n }\n };\n\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-devteam-feedback\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = buildStyles(options, accent);\n\n const container = element(\"div\", \"container\");\n const fab = element(\"button\", \"fab\");\n fab.type = \"button\";\n fab.textContent = options.buttonLabel ?? \"Feedback\";\n\n const overlay = element(\"div\", \"overlay\");\n overlay.hidden = true;\n const backdrop = element(\"div\", \"backdrop\");\n const panel = element(\"div\", \"panel\");\n panel.setAttribute(\"role\", \"dialog\");\n panel.setAttribute(\"aria-modal\", \"true\");\n\n const header = element(\"div\", \"header\");\n const title = element(\"div\", \"title\");\n title.textContent = options.title ?? \"Send feedback\";\n const closeButton = element(\"button\", \"close\");\n closeButton.type = \"button\";\n closeButton.setAttribute(\"aria-label\", \"Close\");\n closeButton.textContent = \"×\";\n header.append(title, closeButton);\n\n const preview = element(\"div\", \"preview\");\n const drawCanvas = document.createElement(\"canvas\");\n drawCanvas.className = \"draw\";\n preview.appendChild(drawCanvas);\n\n const tools = element(\"div\", \"tools\");\n const swatches = colors.map((color) => {\n const swatch = element(\"button\", \"swatch\");\n swatch.type = \"button\";\n swatch.style.background = color;\n swatch.setAttribute(\"aria-label\", `Pen color ${color}`);\n swatch.addEventListener(\"click\", () => safely(() => selectColor(color)));\n tools.appendChild(swatch);\n return { color, swatch };\n });\n const undoButton = element(\"button\", \"tool undo\");\n undoButton.type = \"button\";\n undoButton.textContent = \"Undo\";\n const clearButton = element(\"button\", \"tool clear\");\n clearButton.type = \"button\";\n clearButton.textContent = \"Clear\";\n tools.append(undoButton, clearButton);\n\n const comment = document.createElement(\"textarea\");\n comment.className = \"comment\";\n comment.placeholder = options.commentPlaceholder ?? \"What went wrong, or what could be better?\";\n comment.maxLength = MAX_TEXT_LENGTH;\n\n const error = element(\"div\", \"error\");\n error.hidden = true;\n const success = element(\"div\", \"success\");\n success.hidden = true;\n success.textContent = options.successMessage ?? \"Thanks for the feedback!\";\n\n const actions = element(\"div\", \"actions\");\n const cancelButton = element(\"button\", \"cancel\");\n cancelButton.type = \"button\";\n cancelButton.textContent = \"Cancel\";\n const submitButton = element(\"button\", \"submit\");\n submitButton.type = \"button\";\n submitButton.textContent = options.submitLabel ?? \"Send\";\n actions.append(cancelButton, submitButton);\n\n panel.append(header, preview, tools, comment, error, success, actions);\n overlay.append(backdrop, panel);\n container.append(fab, overlay);\n shadow.append(style, container);\n\n if (document.body) {\n document.body.appendChild(host);\n } else {\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n if (!destroyed) document.body?.appendChild(host);\n },\n { once: true },\n );\n }\n\n function selectColor(color: string): void {\n activeColor = color;\n for (const entry of swatches) {\n entry.swatch.classList.toggle(\"active\", entry.color === color);\n }\n }\n selectColor(activeColor);\n\n function context2d(): CanvasRenderingContext2D | null {\n if (drawContext === undefined) {\n try {\n drawContext = drawCanvas.getContext(\"2d\");\n } catch {\n drawContext = null;\n }\n }\n return drawContext;\n }\n\n function repaint(): void {\n const ctx = context2d();\n if (!ctx) return;\n ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);\n drawStrokes(ctx, strokes.list());\n }\n\n function pointFromEvent(event: Event): StrokePoint | undefined {\n const { clientX, clientY } = event as { clientX?: unknown; clientY?: unknown };\n if (typeof clientX !== \"number\" || typeof clientY !== \"number\") return undefined;\n const rect = drawCanvas.getBoundingClientRect();\n return mapToImagePoint(clientX, clientY, rect, drawCanvas.width, drawCanvas.height);\n }\n\n function withPointerCapture(event: Event, action: \"set\" | \"release\"): void {\n const pointerId = (event as { pointerId?: unknown }).pointerId;\n if (typeof pointerId !== \"number\") return;\n const method = action === \"set\" ? drawCanvas.setPointerCapture : drawCanvas.releasePointerCapture;\n if (typeof method !== \"function\") return;\n try {\n method.call(drawCanvas, pointerId);\n } catch {\n // pointer capture is a nicety; drawing works without it\n }\n }\n\n const onPointerDown = (event: Event): void =>\n safely(() => {\n if (!sourceCanvas || sending) return;\n const point = pointFromEvent(event);\n if (!point) return;\n if (typeof event.preventDefault === \"function\") event.preventDefault();\n withPointerCapture(event, \"set\");\n strokes.begin(activeColor, penWidth, point);\n repaint();\n });\n const onPointerMove = (event: Event): void =>\n safely(() => {\n if (!strokes.isDrawing()) return;\n const point = pointFromEvent(event);\n if (!point) return;\n strokes.extend(point);\n repaint();\n });\n const onPointerEnd = (event: Event): void =>\n safely(() => {\n withPointerCapture(event, \"release\");\n strokes.end();\n });\n drawCanvas.addEventListener(\"pointerdown\", onPointerDown);\n drawCanvas.addEventListener(\"pointermove\", onPointerMove);\n drawCanvas.addEventListener(\"pointerup\", onPointerEnd);\n drawCanvas.addEventListener(\"pointercancel\", onPointerEnd);\n // When pointer capture is unavailable a release outside the canvas never\n // reaches it, which would leave the stroke armed (hover keeps extending it\n // and Undo/Clear are mid-drag no-ops). Capture retargets events to the\n // canvas, so these fallbacks only fire on the uncaptured path: leaving the\n // canvas ends the stroke, and the overlay spans the viewport, so any\n // release lands somewhere in the shadow root.\n drawCanvas.addEventListener(\"pointerleave\", onPointerEnd);\n shadow.addEventListener(\"pointerup\", onPointerEnd);\n\n undoButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.undo();\n repaint();\n }),\n );\n clearButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.clear();\n repaint();\n }),\n );\n\n const onKeyDown = (event: Event): void => {\n if ((event as { key?: unknown }).key === \"Escape\") safely(close);\n };\n\n function lockScroll(): void {\n if (previousBodyOverflow !== undefined) return;\n previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n\n function unlockScroll(): void {\n if (previousBodyOverflow === undefined) return;\n document.body.style.overflow = previousBodyOverflow;\n previousBodyOverflow = undefined;\n }\n\n function setError(message: string | undefined): void {\n error.hidden = message === undefined;\n error.textContent = message ?? \"\";\n }\n\n function setFormVisible(visible: boolean): void {\n const hasPreview = sourceCanvas !== undefined;\n preview.hidden = !visible || !hasPreview;\n tools.hidden = !visible || !hasPreview;\n comment.hidden = !visible;\n actions.hidden = !visible;\n }\n\n function showOverlay(canvas: HTMLCanvasElement | undefined): void {\n overlaySession += 1;\n draftFeedbackId = uuidV7();\n sourceCanvas = canvas;\n strokes.end();\n strokes.clear();\n comment.value = \"\";\n setError(undefined);\n success.hidden = true;\n submitButton.disabled = false;\n if (canvas) {\n canvas.classList.add(\"shot\");\n preview.insertBefore(canvas, drawCanvas);\n drawCanvas.width = canvas.width;\n drawCanvas.height = canvas.height;\n penWidth = PEN_WIDTH * (pixelRatio > 0 ? pixelRatio : 1);\n repaint();\n }\n selectColor(colors[0] ?? DEFAULT_COLORS[0]!);\n setFormVisible(true);\n overlay.hidden = false;\n overlayOpen = true;\n lockScroll();\n document.addEventListener(\"keydown\", onKeyDown);\n }\n\n function close(): void {\n if (!overlayOpen) return;\n overlaySession += 1;\n overlayOpen = false;\n overlay.hidden = true;\n document.removeEventListener(\"keydown\", onKeyDown);\n unlockScroll();\n if (closeTimer !== undefined) {\n clearTimeout(closeTimer);\n closeTimer = undefined;\n }\n sending = false;\n if (sourceCanvas) {\n sourceCanvas.remove();\n sourceCanvas = undefined;\n }\n strokes.end();\n strokes.clear();\n }\n\n function open(): void {\n if (destroyed || overlayOpen || capturing) return;\n capturing = true;\n void Promise.resolve()\n .then(() => deps.capture(host, pixelRatio))\n .then(\n (canvas) => {\n capturing = false;\n if (destroyed) return;\n showOverlay(canvas);\n },\n (captureError: unknown) => {\n capturing = false;\n if (destroyed) return;\n report(captureError);\n showOverlay(undefined);\n },\n );\n }\n\n function buildBody(text: string, screenshot: ExportedImage | null): string {\n const body: Record<string, unknown> = {\n feedback_id: draftFeedbackId,\n text: text.slice(0, MAX_TEXT_LENGTH),\n };\n if (screenshot) {\n body.screenshot = screenshot.base64;\n body.screenshot_content_type = screenshot.contentType;\n }\n const distinctId = config.getDistinctId();\n if (distinctId) body.distinct_id = distinctId;\n const sessionId = config.getSessionId();\n if (sessionId) body.session_id = sessionId;\n if (typeof location !== \"undefined\" && location.href) {\n body.page_url = location.href.slice(0, MAX_PAGE_URL_LENGTH);\n }\n body.context = config.getContext();\n return JSON.stringify(body);\n }\n\n async function submit(): Promise<void> {\n if (sending || !overlayOpen) return;\n const text = comment.value.trim();\n if (text.length === 0) {\n setError(\"Please add a short comment before sending.\");\n return;\n }\n const session = overlaySession;\n const sameSession = (): boolean => !destroyed && overlayOpen && overlaySession === session;\n sending = true;\n submitButton.disabled = true;\n setError(undefined);\n try {\n let screenshot: ExportedImage | null = null;\n if (sourceCanvas) {\n screenshot = await deps.exportAnnotated(sourceCanvas, strokes.list(), {\n maxBytes: MAX_SCREENSHOT_BYTES,\n });\n }\n if (!config.fetch) throw new Error(\"fetch is not available in this environment\");\n const response = await config.fetch(config.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-DevTeam-Key\": config.key },\n body: buildBody(text, screenshot),\n });\n if (!sameSession()) return;\n if (response.ok) {\n setFormVisible(false);\n success.hidden = false;\n closeTimer = setTimeout(() => safely(close), SUCCESS_CLOSE_DELAY_MS);\n } else {\n setError(\"Could not send feedback. Please try again.\");\n }\n } catch (submitError) {\n report(submitError);\n if (sameSession()) setError(\"Could not send feedback. Please try again.\");\n } finally {\n if (overlaySession === session) {\n sending = false;\n submitButton.disabled = false;\n }\n }\n }\n\n fab.addEventListener(\"click\", () => safely(open));\n closeButton.addEventListener(\"click\", () => safely(close));\n cancelButton.addEventListener(\"click\", () => safely(close));\n backdrop.addEventListener(\"click\", () => safely(close));\n submitButton.addEventListener(\"click\", () => safely(() => void submit()));\n\n return {\n open: () => safely(open),\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n safely(() => {\n close();\n document.removeEventListener(\"keydown\", onKeyDown);\n host.remove();\n });\n },\n };\n}\n\nfunction element<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n node.className = className;\n return node;\n}\n\nfunction buildStyles(options: WebFeedbackOptions, accent: string): string {\n const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;\n const side = options.position === \"bottom-left\" ? \"left\" : \"right\";\n return `\n:host { all: initial; }\n* { box-sizing: border-box; }\n[hidden] { display: none !important; }\n.container {\n position: fixed;\n inset: 0;\n z-index: ${zIndex};\n pointer-events: none;\n font-family: system-ui, -apple-system, sans-serif;\n color: #1f2933;\n}\n.fab {\n position: absolute;\n bottom: 20px;\n ${side}: 20px;\n pointer-events: auto;\n border: none;\n border-radius: 999px;\n padding: 10px 18px;\n background: ${accent};\n color: #fff;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n}\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgba(15, 23, 42, 0.55);\n}\n.panel {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: min(560px, calc(100vw - 32px));\n max-height: calc(100vh - 32px);\n overflow: auto;\n background: #fff;\n border-radius: 12px;\n padding: 16px;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);\n}\n.header { display: flex; align-items: center; justify-content: space-between; }\n.title { font-size: 16px; font-weight: 600; }\n.close {\n border: none;\n background: none;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n color: #52606d;\n padding: 4px 8px;\n}\n.preview {\n position: relative;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n overflow: hidden;\n background: #f5f7fa;\n}\n.preview .shot { display: block; width: 100%; height: auto; }\n.preview .draw {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n cursor: crosshair;\n touch-action: none;\n}\n.tools { display: flex; align-items: center; gap: 8px; }\n.swatch {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n border: 2px solid transparent;\n cursor: pointer;\n padding: 0;\n}\n.swatch.active { border-color: #1f2933; }\n.tool {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 6px;\n padding: 4px 10px;\n font-size: 13px;\n cursor: pointer;\n}\n.comment {\n width: 100%;\n min-height: 72px;\n resize: vertical;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n}\n.error { color: #b91c1c; font-size: 13px; }\n.success { color: #15803d; font-size: 14px; font-weight: 600; }\n.actions { display: flex; justify-content: flex-end; gap: 8px; }\n.cancel {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 8px;\n padding: 8px 14px;\n font-size: 14px;\n cursor: pointer;\n}\n.submit {\n border: none;\n background: ${accent};\n color: #fff;\n border-radius: 8px;\n padding: 8px 16px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n.submit:disabled { opacity: 0.6; cursor: default; }\n`;\n}\n","import { toCanvas } from \"html-to-image\";\n\nconst TRANSPARENT_PIXEL_DATA_URL =\n \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=\";\n\n// html-to-image has no clip option, so the visible viewport is captured by\n// sizing the render to the window and translating the cloned document up and\n// left by the current scroll offset.\nexport async function captureViewport(exclude: Node, pixelRatio: number): Promise<HTMLCanvasElement> {\n return toCanvas(document.documentElement, {\n filter: (node) => node !== exclude,\n imagePlaceholder: TRANSPARENT_PIXEL_DATA_URL,\n pixelRatio,\n width: window.innerWidth,\n height: window.innerHeight,\n style: {\n transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`,\n },\n });\n}\n","import type { Stroke } from \"./strokes\";\n\nexport interface ExportedImage {\n base64: string;\n contentType: \"image/png\" | \"image/jpeg\";\n}\n\nexport interface ExportImageOptions {\n maxBytes: number;\n}\n\nconst JPEG_QUALITY = 0.85;\nconst DOWNSCALE_FACTOR = 0.7;\nconst MIN_EXPORT_DIMENSION = 200;\n\nexport function drawStrokes(ctx: CanvasRenderingContext2D, strokes: readonly Stroke[]): void {\n for (const stroke of strokes) {\n const [first, ...rest] = stroke.points;\n if (!first) continue;\n ctx.strokeStyle = stroke.color;\n ctx.lineWidth = stroke.width;\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n ctx.beginPath();\n ctx.moveTo(first.x, first.y);\n if (rest.length === 0) ctx.lineTo(first.x, first.y);\n for (const point of rest) ctx.lineTo(point.x, point.y);\n ctx.stroke();\n }\n}\n\n// Composites the full-resolution screenshot with the strokes, then encodes to\n// PNG; when the PNG is over the byte budget it falls back to JPEG and, if\n// still too large, downscales in 0.7 steps until it fits. Returns null when no\n// encoding fits or the environment has no working 2d canvas.\nexport async function exportAnnotatedImage(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: ExportImageOptions,\n): Promise<ExportedImage | null> {\n try {\n const composed = compose(source, strokes);\n if (!composed) return null;\n\n const png = await encode(composed, \"image/png\");\n if (png && png.size <= options.maxBytes) {\n return { base64: await blobToBase64(png), contentType: \"image/png\" };\n }\n\n let canvas = composed;\n let jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n while (\n jpeg &&\n jpeg.size > options.maxBytes &&\n Math.min(canvas.width, canvas.height) * DOWNSCALE_FACTOR >= MIN_EXPORT_DIMENSION\n ) {\n const smaller = downscale(canvas, DOWNSCALE_FACTOR);\n if (!smaller) break;\n canvas = smaller;\n jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n }\n if (jpeg && jpeg.size <= options.maxBytes) {\n return { base64: await blobToBase64(jpeg), contentType: \"image/jpeg\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction compose(source: HTMLCanvasElement, strokes: readonly Stroke[]): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = source.width;\n canvas.height = source.height;\n const ctx = get2dContext(canvas);\n if (!ctx || canvas.width === 0 || canvas.height === 0) return null;\n // JPEG has no alpha channel and would composite transparency onto black.\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(source, 0, 0);\n drawStrokes(ctx, strokes);\n return canvas;\n}\n\nfunction downscale(source: HTMLCanvasElement, factor: number): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(source.width * factor));\n canvas.height = Math.max(1, Math.round(source.height * factor));\n const ctx = get2dContext(canvas);\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\nfunction get2dContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {\n try {\n return canvas.getContext(\"2d\");\n } catch {\n return null;\n }\n}\n\nfunction encode(canvas: HTMLCanvasElement, type: string, quality?: number): Promise<Blob | null> {\n return new Promise((resolve) => {\n try {\n canvas.toBlob((blob) => resolve(blob), type, quality);\n } catch {\n resolve(null);\n }\n });\n}\n\nexport async function blobToBase64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n const chunkSize = 0x8000;\n for (let i = 0; i < bytes.length; i += chunkSize) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));\n }\n return btoa(binary);\n}\n","export interface StrokePoint {\n x: number;\n y: number;\n}\n\nexport interface Stroke {\n points: StrokePoint[];\n color: string;\n width: number;\n}\n\nexport interface StrokeStore {\n begin(color: string, width: number, point: StrokePoint): void;\n extend(point: StrokePoint): void;\n end(): void;\n undo(): void;\n clear(): void;\n list(): readonly Stroke[];\n isDrawing(): boolean;\n}\n\n// Each stroke snapshots its color and width when it starts, so a later color\n// change never restyles strokes already on the canvas. Undo and clear are\n// no-ops mid-drag to keep the active stroke consistent.\nexport function createStrokeStore(): StrokeStore {\n const strokes: Stroke[] = [];\n let active: Stroke | undefined;\n return {\n begin(color, width, point) {\n if (active) return;\n active = { points: [point], color, width };\n strokes.push(active);\n },\n extend(point) {\n active?.points.push(point);\n },\n end() {\n active = undefined;\n },\n undo() {\n if (active) return;\n strokes.pop();\n },\n clear() {\n if (active) return;\n strokes.length = 0;\n },\n list: () => strokes,\n isDrawing: () => active !== undefined,\n };\n}\n\nexport interface DisplayRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n// Strokes live in the screenshot's own pixel space so they land at the same\n// spot in the full-resolution export as they did on the scaled-down preview.\n// A degenerate rect (jsdom reports zero sizes) maps 1:1 instead of dividing by zero.\nexport function mapToImagePoint(\n clientX: number,\n clientY: number,\n rect: DisplayRect,\n imageWidth: number,\n imageHeight: number,\n): StrokePoint {\n const scaleX = rect.width > 0 && imageWidth > 0 ? imageWidth / rect.width : 1;\n const scaleY = rect.height > 0 && imageHeight > 0 ? imageHeight / rect.height : 1;\n return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };\n}\n"],"mappings":";AAAA,SAAS,cAAiD;;;ACA1D,SAAS,gBAAgB;AAEzB,IAAM,6BACJ;AAKF,eAAsB,gBAAgB,SAAe,YAAgD;AACnG,SAAO,SAAS,SAAS,iBAAiB;AAAA,IACxC,QAAQ,CAAC,SAAS,SAAS;AAAA,IAC3B,kBAAkB;AAAA,IAClB;AAAA,IACA,OAAO,OAAO;AAAA,IACd,QAAQ,OAAO;AAAA,IACf,OAAO;AAAA,MACL,WAAW,aAAa,CAAC,OAAO,OAAO,OAAO,CAAC,OAAO,OAAO;AAAA,IAC/D;AAAA,EACF,CAAC;AACH;;;ACRA,IAAM,eAAe;AACrB,IAAM,mBAAmB;AACzB,IAAM,uBAAuB;AAEtB,SAAS,YAAY,KAA+B,SAAkC;AAC3F,aAAW,UAAU,SAAS;AAC5B,UAAM,CAAC,OAAO,GAAG,IAAI,IAAI,OAAO;AAChC,QAAI,CAAC,MAAO;AACZ,QAAI,cAAc,OAAO;AACzB,QAAI,YAAY,OAAO;AACvB,QAAI,UAAU;AACd,QAAI,WAAW;AACf,QAAI,UAAU;AACd,QAAI,OAAO,MAAM,GAAG,MAAM,CAAC;AAC3B,QAAI,KAAK,WAAW,EAAG,KAAI,OAAO,MAAM,GAAG,MAAM,CAAC;AAClD,eAAW,SAAS,KAAM,KAAI,OAAO,MAAM,GAAG,MAAM,CAAC;AACrD,QAAI,OAAO;AAAA,EACb;AACF;AAMA,eAAsB,qBACpB,QACA,SACA,SAC+B;AAC/B,MAAI;AACF,UAAM,WAAW,QAAQ,QAAQ,OAAO;AACxC,QAAI,CAAC,SAAU,QAAO;AAEtB,UAAM,MAAM,MAAM,OAAO,UAAU,WAAW;AAC9C,QAAI,OAAO,IAAI,QAAQ,QAAQ,UAAU;AACvC,aAAO,EAAE,QAAQ,MAAM,aAAa,GAAG,GAAG,aAAa,YAAY;AAAA,IACrE;AAEA,QAAI,SAAS;AACb,QAAI,OAAO,MAAM,OAAO,QAAQ,cAAc,YAAY;AAC1D,WACE,QACA,KAAK,OAAO,QAAQ,YACpB,KAAK,IAAI,OAAO,OAAO,OAAO,MAAM,IAAI,oBAAoB,sBAC5D;AACA,YAAM,UAAU,UAAU,QAAQ,gBAAgB;AAClD,UAAI,CAAC,QAAS;AACd,eAAS;AACT,aAAO,MAAM,OAAO,QAAQ,cAAc,YAAY;AAAA,IACxD;AACA,QAAI,QAAQ,KAAK,QAAQ,QAAQ,UAAU;AACzC,aAAO,EAAE,QAAQ,MAAM,aAAa,IAAI,GAAG,aAAa,aAAa;AAAA,IACvE;AACA,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,QAAQ,QAA2B,SAAsD;AAChG,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ,OAAO;AACtB,SAAO,SAAS,OAAO;AACvB,QAAM,MAAM,aAAa,MAAM;AAC/B,MAAI,CAAC,OAAO,OAAO,UAAU,KAAK,OAAO,WAAW,EAAG,QAAO;AAE9D,MAAI,YAAY;AAChB,MAAI,SAAS,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AAC9C,MAAI,UAAU,QAAQ,GAAG,CAAC;AAC1B,cAAY,KAAK,OAAO;AACxB,SAAO;AACT;AAEA,SAAS,UAAU,QAA2B,QAA0C;AACtF,QAAM,SAAS,SAAS,cAAc,QAAQ;AAC9C,SAAO,QAAQ,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,QAAQ,MAAM,CAAC;AAC5D,SAAO,SAAS,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,SAAS,MAAM,CAAC;AAC9D,QAAM,MAAM,aAAa,MAAM;AAC/B,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,UAAU,QAAQ,GAAG,GAAG,OAAO,OAAO,OAAO,MAAM;AACvD,SAAO;AACT;AAEA,SAAS,aAAa,QAA4D;AAChF,MAAI;AACF,WAAO,OAAO,WAAW,IAAI;AAAA,EAC/B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,OAAO,QAA2B,MAAc,SAAwC;AAC/F,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,QAAI;AACF,aAAO,OAAO,CAAC,SAAS,QAAQ,IAAI,GAAG,MAAM,OAAO;AAAA,IACtD,QAAQ;AACN,cAAQ,IAAI;AAAA,IACd;AAAA,EACF,CAAC;AACH;AAEA,eAAsB,aAAa,MAA6B;AAC9D,QAAM,QAAQ,IAAI,WAAW,MAAM,KAAK,YAAY,CAAC;AACrD,MAAI,SAAS;AACb,QAAM,YAAY;AAClB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK,WAAW;AAChD,cAAU,OAAO,aAAa,GAAG,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC;AAAA,EACnE;AACA,SAAO,KAAK,MAAM;AACpB;;;AChGO,SAAS,oBAAiC;AAC/C,QAAM,UAAoB,CAAC;AAC3B,MAAI;AACJ,SAAO;AAAA,IACL,MAAM,OAAO,OAAO,OAAO;AACzB,UAAI,OAAQ;AACZ,eAAS,EAAE,QAAQ,CAAC,KAAK,GAAG,OAAO,MAAM;AACzC,cAAQ,KAAK,MAAM;AAAA,IACrB;AAAA,IACA,OAAO,OAAO;AACZ,cAAQ,OAAO,KAAK,KAAK;AAAA,IAC3B;AAAA,IACA,MAAM;AACJ,eAAS;AAAA,IACX;AAAA,IACA,OAAO;AACL,UAAI,OAAQ;AACZ,cAAQ,IAAI;AAAA,IACd;AAAA,IACA,QAAQ;AACN,UAAI,OAAQ;AACZ,cAAQ,SAAS;AAAA,IACnB;AAAA,IACA,MAAM,MAAM;AAAA,IACZ,WAAW,MAAM,WAAW;AAAA,EAC9B;AACF;AAYO,SAAS,gBACd,SACA,SACA,MACA,YACA,aACa;AACb,QAAM,SAAS,KAAK,QAAQ,KAAK,aAAa,IAAI,aAAa,KAAK,QAAQ;AAC5E,QAAM,SAAS,KAAK,SAAS,KAAK,cAAc,IAAI,cAAc,KAAK,SAAS;AAChF,SAAO,EAAE,IAAI,UAAU,KAAK,QAAQ,QAAQ,IAAI,UAAU,KAAK,OAAO,OAAO;AAC/E;;;AHvCA,IAAM,iBAAiB,CAAC,WAAW,WAAW,WAAW,SAAS;AAClE,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,YAAY;AAClB,IAAM,uBAAuB,IAAI,OAAO;AACxC,IAAM,kBAAkB;AACxB,IAAM,sBAAsB;AAC5B,IAAM,yBAAyB;AAE/B,IAAM,cAA4B;AAAA,EAChC,SAAS;AAAA,EACT,iBAAiB;AACnB;AAEO,SAAS,cAAc,QAAwB,OAAqB,aAA6B;AACtG,QAAM,UAAU,OAAO;AACvB,QAAM,SAAS,QAAQ,UAAU,QAAQ,OAAO,SAAS,IAAI,QAAQ,SAAS;AAC9E,QAAM,SAAS,QAAQ,eAAe;AACtC,QAAM,aACJ,QAAQ,cACR,KAAK,IAAI,OAAO,qBAAqB,YAAY,mBAAmB,IAAI,mBAAmB,GAAG,CAAC;AAEjG,MAAI,YAAY;AAChB,MAAI,cAAc;AAIlB,MAAI,iBAAiB;AACrB,MAAI,YAAY;AAChB,MAAI,UAAU;AAGd,MAAI,kBAAkB,OAAO;AAC7B,MAAI;AACJ,MAAI,cAAc,OAAO,CAAC,KAAK,eAAe,CAAC;AAC/C,MAAI,WAAW;AACf,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,QAAM,UAAU,kBAAkB;AAElC,QAAM,SAAS,CAACA,WAAyB;AACvC,QAAI;AACF,aAAO,QAAQA,MAAK;AAAA,IACtB,QAAQ;AAAA,IAER;AAAA,EACF;AACA,QAAM,SAAS,CAAC,OAAyB;AACvC,QAAI;AACF,SAAG;AAAA,IACL,SAASA,QAAO;AACd,aAAOA,MAAK;AAAA,IACd;AAAA,EACF;AAEA,QAAM,OAAO,SAAS,cAAc,KAAK;AACzC,OAAK,aAAa,yBAAyB,EAAE;AAC7C,QAAM,SAAS,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAEjD,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,cAAc,YAAY,SAAS,MAAM;AAE/C,QAAM,YAAY,QAAQ,OAAO,WAAW;AAC5C,QAAM,MAAM,QAAQ,UAAU,KAAK;AACnC,MAAI,OAAO;AACX,MAAI,cAAc,QAAQ,eAAe;AAEzC,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,UAAQ,SAAS;AACjB,QAAM,WAAW,QAAQ,OAAO,UAAU;AAC1C,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,aAAa,QAAQ,QAAQ;AACnC,QAAM,aAAa,cAAc,MAAM;AAEvC,QAAM,SAAS,QAAQ,OAAO,QAAQ;AACtC,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,cAAc,QAAQ,SAAS;AACrC,QAAM,cAAc,QAAQ,UAAU,OAAO;AAC7C,cAAY,OAAO;AACnB,cAAY,aAAa,cAAc,OAAO;AAC9C,cAAY,cAAc;AAC1B,SAAO,OAAO,OAAO,WAAW;AAEhC,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,QAAM,aAAa,SAAS,cAAc,QAAQ;AAClD,aAAW,YAAY;AACvB,UAAQ,YAAY,UAAU;AAE9B,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,WAAW,OAAO,IAAI,CAAC,UAAU;AACrC,UAAM,SAAS,QAAQ,UAAU,QAAQ;AACzC,WAAO,OAAO;AACd,WAAO,MAAM,aAAa;AAC1B,WAAO,aAAa,cAAc,aAAa,KAAK,EAAE;AACtD,WAAO,iBAAiB,SAAS,MAAM,OAAO,MAAM,YAAY,KAAK,CAAC,CAAC;AACvE,UAAM,YAAY,MAAM;AACxB,WAAO,EAAE,OAAO,OAAO;AAAA,EACzB,CAAC;AACD,QAAM,aAAa,QAAQ,UAAU,WAAW;AAChD,aAAW,OAAO;AAClB,aAAW,cAAc;AACzB,QAAM,cAAc,QAAQ,UAAU,YAAY;AAClD,cAAY,OAAO;AACnB,cAAY,cAAc;AAC1B,QAAM,OAAO,YAAY,WAAW;AAEpC,QAAM,UAAU,SAAS,cAAc,UAAU;AACjD,UAAQ,YAAY;AACpB,UAAQ,cAAc,QAAQ,sBAAsB;AACpD,UAAQ,YAAY;AAEpB,QAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,QAAM,SAAS;AACf,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,UAAQ,SAAS;AACjB,UAAQ,cAAc,QAAQ,kBAAkB;AAEhD,QAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,QAAM,eAAe,QAAQ,UAAU,QAAQ;AAC/C,eAAa,OAAO;AACpB,eAAa,cAAc;AAC3B,QAAM,eAAe,QAAQ,UAAU,QAAQ;AAC/C,eAAa,OAAO;AACpB,eAAa,cAAc,QAAQ,eAAe;AAClD,UAAQ,OAAO,cAAc,YAAY;AAEzC,QAAM,OAAO,QAAQ,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO;AACrE,UAAQ,OAAO,UAAU,KAAK;AAC9B,YAAU,OAAO,KAAK,OAAO;AAC7B,SAAO,OAAO,OAAO,SAAS;AAE9B,MAAI,SAAS,MAAM;AACjB,aAAS,KAAK,YAAY,IAAI;AAAA,EAChC,OAAO;AACL,aAAS;AAAA,MACP;AAAA,MACA,MAAM;AACJ,YAAI,CAAC,UAAW,UAAS,MAAM,YAAY,IAAI;AAAA,MACjD;AAAA,MACA,EAAE,MAAM,KAAK;AAAA,IACf;AAAA,EACF;AAEA,WAAS,YAAY,OAAqB;AACxC,kBAAc;AACd,eAAW,SAAS,UAAU;AAC5B,YAAM,OAAO,UAAU,OAAO,UAAU,MAAM,UAAU,KAAK;AAAA,IAC/D;AAAA,EACF;AACA,cAAY,WAAW;AAEvB,WAAS,YAA6C;AACpD,QAAI,gBAAgB,QAAW;AAC7B,UAAI;AACF,sBAAc,WAAW,WAAW,IAAI;AAAA,MAC1C,QAAQ;AACN,sBAAc;AAAA,MAChB;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,WAAS,UAAgB;AACvB,UAAM,MAAM,UAAU;AACtB,QAAI,CAAC,IAAK;AACV,QAAI,UAAU,GAAG,GAAG,WAAW,OAAO,WAAW,MAAM;AACvD,gBAAY,KAAK,QAAQ,KAAK,CAAC;AAAA,EACjC;AAEA,WAAS,eAAe,OAAuC;AAC7D,UAAM,EAAE,SAAS,QAAQ,IAAI;AAC7B,QAAI,OAAO,YAAY,YAAY,OAAO,YAAY,SAAU,QAAO;AACvE,UAAM,OAAO,WAAW,sBAAsB;AAC9C,WAAO,gBAAgB,SAAS,SAAS,MAAM,WAAW,OAAO,WAAW,MAAM;AAAA,EACpF;AAEA,WAAS,mBAAmB,OAAc,QAAiC;AACzE,UAAM,YAAa,MAAkC;AACrD,QAAI,OAAO,cAAc,SAAU;AACnC,UAAM,SAAS,WAAW,QAAQ,WAAW,oBAAoB,WAAW;AAC5E,QAAI,OAAO,WAAW,WAAY;AAClC,QAAI;AACF,aAAO,KAAK,YAAY,SAAS;AAAA,IACnC,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,gBAAgB,CAAC,UACrB,OAAO,MAAM;AACX,QAAI,CAAC,gBAAgB,QAAS;AAC9B,UAAM,QAAQ,eAAe,KAAK;AAClC,QAAI,CAAC,MAAO;AACZ,QAAI,OAAO,MAAM,mBAAmB,WAAY,OAAM,eAAe;AACrE,uBAAmB,OAAO,KAAK;AAC/B,YAAQ,MAAM,aAAa,UAAU,KAAK;AAC1C,YAAQ;AAAA,EACV,CAAC;AACH,QAAM,gBAAgB,CAAC,UACrB,OAAO,MAAM;AACX,QAAI,CAAC,QAAQ,UAAU,EAAG;AAC1B,UAAM,QAAQ,eAAe,KAAK;AAClC,QAAI,CAAC,MAAO;AACZ,YAAQ,OAAO,KAAK;AACpB,YAAQ;AAAA,EACV,CAAC;AACH,QAAM,eAAe,CAAC,UACpB,OAAO,MAAM;AACX,uBAAmB,OAAO,SAAS;AACnC,YAAQ,IAAI;AAAA,EACd,CAAC;AACH,aAAW,iBAAiB,eAAe,aAAa;AACxD,aAAW,iBAAiB,eAAe,aAAa;AACxD,aAAW,iBAAiB,aAAa,YAAY;AACrD,aAAW,iBAAiB,iBAAiB,YAAY;AAOzD,aAAW,iBAAiB,gBAAgB,YAAY;AACxD,SAAO,iBAAiB,aAAa,YAAY;AAEjD,aAAW;AAAA,IAAiB;AAAA,IAAS,MACnC,OAAO,MAAM;AACX,cAAQ,KAAK;AACb,cAAQ;AAAA,IACV,CAAC;AAAA,EACH;AACA,cAAY;AAAA,IAAiB;AAAA,IAAS,MACpC,OAAO,MAAM;AACX,cAAQ,MAAM;AACd,cAAQ;AAAA,IACV,CAAC;AAAA,EACH;AAEA,QAAM,YAAY,CAAC,UAAuB;AACxC,QAAK,MAA4B,QAAQ,SAAU,QAAO,KAAK;AAAA,EACjE;AAEA,WAAS,aAAmB;AAC1B,QAAI,yBAAyB,OAAW;AACxC,2BAAuB,SAAS,KAAK,MAAM;AAC3C,aAAS,KAAK,MAAM,WAAW;AAAA,EACjC;AAEA,WAAS,eAAqB;AAC5B,QAAI,yBAAyB,OAAW;AACxC,aAAS,KAAK,MAAM,WAAW;AAC/B,2BAAuB;AAAA,EACzB;AAEA,WAAS,SAAS,SAAmC;AACnD,UAAM,SAAS,YAAY;AAC3B,UAAM,cAAc,WAAW;AAAA,EACjC;AAEA,WAAS,eAAe,SAAwB;AAC9C,UAAM,aAAa,iBAAiB;AACpC,YAAQ,SAAS,CAAC,WAAW,CAAC;AAC9B,UAAM,SAAS,CAAC,WAAW,CAAC;AAC5B,YAAQ,SAAS,CAAC;AAClB,YAAQ,SAAS,CAAC;AAAA,EACpB;AAEA,WAAS,YAAY,QAA6C;AAChE,sBAAkB;AAClB,sBAAkB,OAAO;AACzB,mBAAe;AACf,YAAQ,IAAI;AACZ,YAAQ,MAAM;AACd,YAAQ,QAAQ;AAChB,aAAS,MAAS;AAClB,YAAQ,SAAS;AACjB,iBAAa,WAAW;AACxB,QAAI,QAAQ;AACV,aAAO,UAAU,IAAI,MAAM;AAC3B,cAAQ,aAAa,QAAQ,UAAU;AACvC,iBAAW,QAAQ,OAAO;AAC1B,iBAAW,SAAS,OAAO;AAC3B,iBAAW,aAAa,aAAa,IAAI,aAAa;AACtD,cAAQ;AAAA,IACV;AACA,gBAAY,OAAO,CAAC,KAAK,eAAe,CAAC,CAAE;AAC3C,mBAAe,IAAI;AACnB,YAAQ,SAAS;AACjB,kBAAc;AACd,eAAW;AACX,aAAS,iBAAiB,WAAW,SAAS;AAAA,EAChD;AAEA,WAAS,QAAc;AACrB,QAAI,CAAC,YAAa;AAClB,sBAAkB;AAClB,kBAAc;AACd,YAAQ,SAAS;AACjB,aAAS,oBAAoB,WAAW,SAAS;AACjD,iBAAa;AACb,QAAI,eAAe,QAAW;AAC5B,mBAAa,UAAU;AACvB,mBAAa;AAAA,IACf;AACA,cAAU;AACV,QAAI,cAAc;AAChB,mBAAa,OAAO;AACpB,qBAAe;AAAA,IACjB;AACA,YAAQ,IAAI;AACZ,YAAQ,MAAM;AAAA,EAChB;AAEA,WAAS,OAAa;AACpB,QAAI,aAAa,eAAe,UAAW;AAC3C,gBAAY;AACZ,SAAK,QAAQ,QAAQ,EAClB,KAAK,MAAM,KAAK,QAAQ,MAAM,UAAU,CAAC,EACzC;AAAA,MACC,CAAC,WAAW;AACV,oBAAY;AACZ,YAAI,UAAW;AACf,oBAAY,MAAM;AAAA,MACpB;AAAA,MACA,CAAC,iBAA0B;AACzB,oBAAY;AACZ,YAAI,UAAW;AACf,eAAO,YAAY;AACnB,oBAAY,MAAS;AAAA,MACvB;AAAA,IACF;AAAA,EACJ;AAEA,WAAS,UAAU,MAAc,YAA0C;AACzE,UAAM,OAAgC;AAAA,MACpC,aAAa;AAAA,MACb,MAAM,KAAK,MAAM,GAAG,eAAe;AAAA,IACrC;AACA,QAAI,YAAY;AACd,WAAK,aAAa,WAAW;AAC7B,WAAK,0BAA0B,WAAW;AAAA,IAC5C;AACA,UAAM,aAAa,OAAO,cAAc;AACxC,QAAI,WAAY,MAAK,cAAc;AACnC,UAAM,YAAY,OAAO,aAAa;AACtC,QAAI,UAAW,MAAK,aAAa;AACjC,QAAI,OAAO,aAAa,eAAe,SAAS,MAAM;AACpD,WAAK,WAAW,SAAS,KAAK,MAAM,GAAG,mBAAmB;AAAA,IAC5D;AACA,SAAK,UAAU,OAAO,WAAW;AACjC,WAAO,KAAK,UAAU,IAAI;AAAA,EAC5B;AAEA,iBAAe,SAAwB;AACrC,QAAI,WAAW,CAAC,YAAa;AAC7B,UAAM,OAAO,QAAQ,MAAM,KAAK;AAChC,QAAI,KAAK,WAAW,GAAG;AACrB,eAAS,4CAA4C;AACrD;AAAA,IACF;AACA,UAAM,UAAU;AAChB,UAAM,cAAc,MAAe,CAAC,aAAa,eAAe,mBAAmB;AACnF,cAAU;AACV,iBAAa,WAAW;AACxB,aAAS,MAAS;AAClB,QAAI;AACF,UAAI,aAAmC;AACvC,UAAI,cAAc;AAChB,qBAAa,MAAM,KAAK,gBAAgB,cAAc,QAAQ,KAAK,GAAG;AAAA,UACpE,UAAU;AAAA,QACZ,CAAC;AAAA,MACH;AACA,UAAI,CAAC,OAAO,MAAO,OAAM,IAAI,MAAM,4CAA4C;AAC/E,YAAM,WAAW,MAAM,OAAO,MAAM,OAAO,KAAK;AAAA,QAC9C,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,oBAAoB,iBAAiB,OAAO,IAAI;AAAA,QAC3E,MAAM,UAAU,MAAM,UAAU;AAAA,MAClC,CAAC;AACD,UAAI,CAAC,YAAY,EAAG;AACpB,UAAI,SAAS,IAAI;AACf,uBAAe,KAAK;AACpB,gBAAQ,SAAS;AACjB,qBAAa,WAAW,MAAM,OAAO,KAAK,GAAG,sBAAsB;AAAA,MACrE,OAAO;AACL,iBAAS,4CAA4C;AAAA,MACvD;AAAA,IACF,SAAS,aAAa;AACpB,aAAO,WAAW;AAClB,UAAI,YAAY,EAAG,UAAS,4CAA4C;AAAA,IAC1E,UAAE;AACA,UAAI,mBAAmB,SAAS;AAC9B,kBAAU;AACV,qBAAa,WAAW;AAAA,MAC1B;AAAA,IACF;AAAA,EACF;AAEA,MAAI,iBAAiB,SAAS,MAAM,OAAO,IAAI,CAAC;AAChD,cAAY,iBAAiB,SAAS,MAAM,OAAO,KAAK,CAAC;AACzD,eAAa,iBAAiB,SAAS,MAAM,OAAO,KAAK,CAAC;AAC1D,WAAS,iBAAiB,SAAS,MAAM,OAAO,KAAK,CAAC;AACtD,eAAa,iBAAiB,SAAS,MAAM,OAAO,MAAM,KAAK,OAAO,CAAC,CAAC;AAExE,SAAO;AAAA,IACL,MAAM,MAAM,OAAO,IAAI;AAAA,IACvB,SAAS,MAAM;AACb,UAAI,UAAW;AACf,kBAAY;AACZ,aAAO,MAAM;AACX,cAAM;AACN,iBAAS,oBAAoB,WAAW,SAAS;AACjD,aAAK,OAAO;AAAA,MACd,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAEA,SAAS,QACP,KACA,WAC0B;AAC1B,QAAM,OAAO,SAAS,cAAc,GAAG;AACvC,OAAK,YAAY;AACjB,SAAO;AACT;AAEA,SAAS,YAAY,SAA6B,QAAwB;AACxE,QAAM,SAAS,QAAQ,UAAU;AACjC,QAAM,OAAO,QAAQ,aAAa,gBAAgB,SAAS;AAC3D,SAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,aAOI,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQf,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA,gBAKQ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,gBAoGN,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAUtB;","names":["error"]}
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
24
24
|
async function captureViewport(exclude, pixelRatio) {
|
|
25
25
|
return (0, import_html_to_image.toCanvas)(document.documentElement, {
|
|
26
26
|
filter: (node) => node !== exclude,
|
|
27
|
+
imagePlaceholder: TRANSPARENT_PIXEL_DATA_URL,
|
|
27
28
|
pixelRatio,
|
|
28
29
|
width: window.innerWidth,
|
|
29
30
|
height: window.innerHeight,
|
|
@@ -32,11 +33,12 @@ async function captureViewport(exclude, pixelRatio) {
|
|
|
32
33
|
}
|
|
33
34
|
});
|
|
34
35
|
}
|
|
35
|
-
var import_html_to_image;
|
|
36
|
+
var import_html_to_image, TRANSPARENT_PIXEL_DATA_URL;
|
|
36
37
|
var init_capture = __esm({
|
|
37
38
|
"src/feedback/capture.ts"() {
|
|
38
39
|
"use strict";
|
|
39
40
|
import_html_to_image = require("html-to-image");
|
|
41
|
+
TRANSPARENT_PIXEL_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=";
|
|
40
42
|
}
|
|
41
43
|
});
|
|
42
44
|
|
|
@@ -842,7 +844,7 @@ function createBrowserStorage(resolve = resolveLocalStorage) {
|
|
|
842
844
|
|
|
843
845
|
// src/index.ts
|
|
844
846
|
var WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
845
|
-
var WEB_SDK_VERSION = "0.3.
|
|
847
|
+
var WEB_SDK_VERSION = "0.3.1";
|
|
846
848
|
var KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;
|
|
847
849
|
var textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
|
|
848
850
|
function byteLength(value) {
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/feedback/capture.ts","../src/feedback/exportImage.ts","../src/feedback/strokes.ts","../src/feedback/index.ts","../src/index.ts","../src/context.ts","../src/pageviews.ts","../src/storage.ts"],"sourcesContent":["import { toCanvas } from \"html-to-image\";\n\n// html-to-image has no clip option, so the visible viewport is captured by\n// sizing the render to the window and translating the cloned document up and\n// left by the current scroll offset.\nexport async function captureViewport(exclude: Node, pixelRatio: number): Promise<HTMLCanvasElement> {\n return toCanvas(document.documentElement, {\n filter: (node) => node !== exclude,\n pixelRatio,\n width: window.innerWidth,\n height: window.innerHeight,\n style: {\n transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`,\n },\n });\n}\n","import type { Stroke } from \"./strokes\";\n\nexport interface ExportedImage {\n base64: string;\n contentType: \"image/png\" | \"image/jpeg\";\n}\n\nexport interface ExportImageOptions {\n maxBytes: number;\n}\n\nconst JPEG_QUALITY = 0.85;\nconst DOWNSCALE_FACTOR = 0.7;\nconst MIN_EXPORT_DIMENSION = 200;\n\nexport function drawStrokes(ctx: CanvasRenderingContext2D, strokes: readonly Stroke[]): void {\n for (const stroke of strokes) {\n const [first, ...rest] = stroke.points;\n if (!first) continue;\n ctx.strokeStyle = stroke.color;\n ctx.lineWidth = stroke.width;\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n ctx.beginPath();\n ctx.moveTo(first.x, first.y);\n if (rest.length === 0) ctx.lineTo(first.x, first.y);\n for (const point of rest) ctx.lineTo(point.x, point.y);\n ctx.stroke();\n }\n}\n\n// Composites the full-resolution screenshot with the strokes, then encodes to\n// PNG; when the PNG is over the byte budget it falls back to JPEG and, if\n// still too large, downscales in 0.7 steps until it fits. Returns null when no\n// encoding fits or the environment has no working 2d canvas.\nexport async function exportAnnotatedImage(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: ExportImageOptions,\n): Promise<ExportedImage | null> {\n try {\n const composed = compose(source, strokes);\n if (!composed) return null;\n\n const png = await encode(composed, \"image/png\");\n if (png && png.size <= options.maxBytes) {\n return { base64: await blobToBase64(png), contentType: \"image/png\" };\n }\n\n let canvas = composed;\n let jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n while (\n jpeg &&\n jpeg.size > options.maxBytes &&\n Math.min(canvas.width, canvas.height) * DOWNSCALE_FACTOR >= MIN_EXPORT_DIMENSION\n ) {\n const smaller = downscale(canvas, DOWNSCALE_FACTOR);\n if (!smaller) break;\n canvas = smaller;\n jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n }\n if (jpeg && jpeg.size <= options.maxBytes) {\n return { base64: await blobToBase64(jpeg), contentType: \"image/jpeg\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction compose(source: HTMLCanvasElement, strokes: readonly Stroke[]): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = source.width;\n canvas.height = source.height;\n const ctx = get2dContext(canvas);\n if (!ctx || canvas.width === 0 || canvas.height === 0) return null;\n // JPEG has no alpha channel and would composite transparency onto black.\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(source, 0, 0);\n drawStrokes(ctx, strokes);\n return canvas;\n}\n\nfunction downscale(source: HTMLCanvasElement, factor: number): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(source.width * factor));\n canvas.height = Math.max(1, Math.round(source.height * factor));\n const ctx = get2dContext(canvas);\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\nfunction get2dContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {\n try {\n return canvas.getContext(\"2d\");\n } catch {\n return null;\n }\n}\n\nfunction encode(canvas: HTMLCanvasElement, type: string, quality?: number): Promise<Blob | null> {\n return new Promise((resolve) => {\n try {\n canvas.toBlob((blob) => resolve(blob), type, quality);\n } catch {\n resolve(null);\n }\n });\n}\n\nexport async function blobToBase64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n const chunkSize = 0x8000;\n for (let i = 0; i < bytes.length; i += chunkSize) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));\n }\n return btoa(binary);\n}\n","export interface StrokePoint {\n x: number;\n y: number;\n}\n\nexport interface Stroke {\n points: StrokePoint[];\n color: string;\n width: number;\n}\n\nexport interface StrokeStore {\n begin(color: string, width: number, point: StrokePoint): void;\n extend(point: StrokePoint): void;\n end(): void;\n undo(): void;\n clear(): void;\n list(): readonly Stroke[];\n isDrawing(): boolean;\n}\n\n// Each stroke snapshots its color and width when it starts, so a later color\n// change never restyles strokes already on the canvas. Undo and clear are\n// no-ops mid-drag to keep the active stroke consistent.\nexport function createStrokeStore(): StrokeStore {\n const strokes: Stroke[] = [];\n let active: Stroke | undefined;\n return {\n begin(color, width, point) {\n if (active) return;\n active = { points: [point], color, width };\n strokes.push(active);\n },\n extend(point) {\n active?.points.push(point);\n },\n end() {\n active = undefined;\n },\n undo() {\n if (active) return;\n strokes.pop();\n },\n clear() {\n if (active) return;\n strokes.length = 0;\n },\n list: () => strokes,\n isDrawing: () => active !== undefined,\n };\n}\n\nexport interface DisplayRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n// Strokes live in the screenshot's own pixel space so they land at the same\n// spot in the full-resolution export as they did on the scaled-down preview.\n// A degenerate rect (jsdom reports zero sizes) maps 1:1 instead of dividing by zero.\nexport function mapToImagePoint(\n clientX: number,\n clientY: number,\n rect: DisplayRect,\n imageWidth: number,\n imageHeight: number,\n): StrokePoint {\n const scaleX = rect.width > 0 && imageWidth > 0 ? imageWidth / rect.width : 1;\n const scaleY = rect.height > 0 && imageHeight > 0 ? imageHeight / rect.height : 1;\n return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };\n}\n","import { uuidV7, type EventContext, type FetchLike } from \"@getdevteam/analytics-core\";\nimport { captureViewport } from \"./capture\";\nimport { drawStrokes, exportAnnotatedImage, type ExportedImage } from \"./exportImage\";\nimport { createStrokeStore, mapToImagePoint, type StrokePoint } from \"./strokes\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nimport type { Stroke } from \"./strokes\";\n\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nexport { createStrokeStore, mapToImagePoint } from \"./strokes\";\nexport type { Stroke, StrokePoint, StrokeStore } from \"./strokes\";\nexport { exportAnnotatedImage } from \"./exportImage\";\nexport type { ExportedImage } from \"./exportImage\";\n\nexport interface FeedbackConfig {\n url: string;\n key: string;\n fetch: FetchLike | undefined;\n getDistinctId(): string;\n getSessionId(): string;\n getContext(): EventContext;\n options: WebFeedbackOptions;\n onError(error: unknown): void;\n}\n\nexport interface FeedbackDeps {\n capture(exclude: HTMLElement, pixelRatio: number): Promise<HTMLCanvasElement>;\n exportAnnotated(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: { maxBytes: number },\n ): Promise<ExportedImage | null>;\n}\n\nconst DEFAULT_COLORS = [\"#f44336\", \"#4caf50\", \"#2196f3\", \"#ffeb3b\"];\nconst DEFAULT_ACCENT = \"#5b9cf5\";\nconst DEFAULT_Z_INDEX = 2147483000;\nconst PEN_WIDTH = 5;\nconst MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;\nconst MAX_TEXT_LENGTH = 10000;\nconst MAX_PAGE_URL_LENGTH = 2048;\nconst SUCCESS_CLOSE_DELAY_MS = 1600;\n\nconst defaultDeps: FeedbackDeps = {\n capture: captureViewport,\n exportAnnotated: exportAnnotatedImage,\n};\n\nexport function mountFeedback(config: FeedbackConfig, deps: FeedbackDeps = defaultDeps): FeedbackHandle {\n const options = config.options;\n const colors = options.colors && options.colors.length > 0 ? options.colors : DEFAULT_COLORS;\n const accent = options.accentColor ?? DEFAULT_ACCENT;\n const pixelRatio =\n options.pixelRatio ??\n Math.min(typeof devicePixelRatio === \"number\" && devicePixelRatio > 0 ? devicePixelRatio : 1, 2);\n\n let destroyed = false;\n let overlayOpen = false;\n // Bumped on every open and close: a submit resolving for an earlier overlay\n // session (sent, then closed and reopened mid-flight) must not paint its\n // outcome into, or auto-close, the new draft.\n let overlaySession = 0;\n let capturing = false;\n let sending = false;\n // One idempotency key per draft (the contract dedupes on it), so retrying a\n // failed send cannot store the same feedback twice; re-minted on each open.\n let draftFeedbackId = uuidV7();\n let sourceCanvas: HTMLCanvasElement | undefined;\n let activeColor = colors[0] ?? DEFAULT_COLORS[0]!;\n let penWidth = PEN_WIDTH;\n let previousBodyOverflow: string | undefined;\n let closeTimer: ReturnType<typeof setTimeout> | undefined;\n let drawContext: CanvasRenderingContext2D | null | undefined;\n const strokes = createStrokeStore();\n\n const report = (error: unknown): void => {\n try {\n config.onError(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n const safely = (fn: () => void): void => {\n try {\n fn();\n } catch (error) {\n report(error);\n }\n };\n\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-devteam-feedback\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = buildStyles(options, accent);\n\n const container = element(\"div\", \"container\");\n const fab = element(\"button\", \"fab\");\n fab.type = \"button\";\n fab.textContent = options.buttonLabel ?? \"Feedback\";\n\n const overlay = element(\"div\", \"overlay\");\n overlay.hidden = true;\n const backdrop = element(\"div\", \"backdrop\");\n const panel = element(\"div\", \"panel\");\n panel.setAttribute(\"role\", \"dialog\");\n panel.setAttribute(\"aria-modal\", \"true\");\n\n const header = element(\"div\", \"header\");\n const title = element(\"div\", \"title\");\n title.textContent = options.title ?? \"Send feedback\";\n const closeButton = element(\"button\", \"close\");\n closeButton.type = \"button\";\n closeButton.setAttribute(\"aria-label\", \"Close\");\n closeButton.textContent = \"×\";\n header.append(title, closeButton);\n\n const preview = element(\"div\", \"preview\");\n const drawCanvas = document.createElement(\"canvas\");\n drawCanvas.className = \"draw\";\n preview.appendChild(drawCanvas);\n\n const tools = element(\"div\", \"tools\");\n const swatches = colors.map((color) => {\n const swatch = element(\"button\", \"swatch\");\n swatch.type = \"button\";\n swatch.style.background = color;\n swatch.setAttribute(\"aria-label\", `Pen color ${color}`);\n swatch.addEventListener(\"click\", () => safely(() => selectColor(color)));\n tools.appendChild(swatch);\n return { color, swatch };\n });\n const undoButton = element(\"button\", \"tool undo\");\n undoButton.type = \"button\";\n undoButton.textContent = \"Undo\";\n const clearButton = element(\"button\", \"tool clear\");\n clearButton.type = \"button\";\n clearButton.textContent = \"Clear\";\n tools.append(undoButton, clearButton);\n\n const comment = document.createElement(\"textarea\");\n comment.className = \"comment\";\n comment.placeholder = options.commentPlaceholder ?? \"What went wrong, or what could be better?\";\n comment.maxLength = MAX_TEXT_LENGTH;\n\n const error = element(\"div\", \"error\");\n error.hidden = true;\n const success = element(\"div\", \"success\");\n success.hidden = true;\n success.textContent = options.successMessage ?? \"Thanks for the feedback!\";\n\n const actions = element(\"div\", \"actions\");\n const cancelButton = element(\"button\", \"cancel\");\n cancelButton.type = \"button\";\n cancelButton.textContent = \"Cancel\";\n const submitButton = element(\"button\", \"submit\");\n submitButton.type = \"button\";\n submitButton.textContent = options.submitLabel ?? \"Send\";\n actions.append(cancelButton, submitButton);\n\n panel.append(header, preview, tools, comment, error, success, actions);\n overlay.append(backdrop, panel);\n container.append(fab, overlay);\n shadow.append(style, container);\n\n if (document.body) {\n document.body.appendChild(host);\n } else {\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n if (!destroyed) document.body?.appendChild(host);\n },\n { once: true },\n );\n }\n\n function selectColor(color: string): void {\n activeColor = color;\n for (const entry of swatches) {\n entry.swatch.classList.toggle(\"active\", entry.color === color);\n }\n }\n selectColor(activeColor);\n\n function context2d(): CanvasRenderingContext2D | null {\n if (drawContext === undefined) {\n try {\n drawContext = drawCanvas.getContext(\"2d\");\n } catch {\n drawContext = null;\n }\n }\n return drawContext;\n }\n\n function repaint(): void {\n const ctx = context2d();\n if (!ctx) return;\n ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);\n drawStrokes(ctx, strokes.list());\n }\n\n function pointFromEvent(event: Event): StrokePoint | undefined {\n const { clientX, clientY } = event as { clientX?: unknown; clientY?: unknown };\n if (typeof clientX !== \"number\" || typeof clientY !== \"number\") return undefined;\n const rect = drawCanvas.getBoundingClientRect();\n return mapToImagePoint(clientX, clientY, rect, drawCanvas.width, drawCanvas.height);\n }\n\n function withPointerCapture(event: Event, action: \"set\" | \"release\"): void {\n const pointerId = (event as { pointerId?: unknown }).pointerId;\n if (typeof pointerId !== \"number\") return;\n const method = action === \"set\" ? drawCanvas.setPointerCapture : drawCanvas.releasePointerCapture;\n if (typeof method !== \"function\") return;\n try {\n method.call(drawCanvas, pointerId);\n } catch {\n // pointer capture is a nicety; drawing works without it\n }\n }\n\n const onPointerDown = (event: Event): void =>\n safely(() => {\n if (!sourceCanvas || sending) return;\n const point = pointFromEvent(event);\n if (!point) return;\n if (typeof event.preventDefault === \"function\") event.preventDefault();\n withPointerCapture(event, \"set\");\n strokes.begin(activeColor, penWidth, point);\n repaint();\n });\n const onPointerMove = (event: Event): void =>\n safely(() => {\n if (!strokes.isDrawing()) return;\n const point = pointFromEvent(event);\n if (!point) return;\n strokes.extend(point);\n repaint();\n });\n const onPointerEnd = (event: Event): void =>\n safely(() => {\n withPointerCapture(event, \"release\");\n strokes.end();\n });\n drawCanvas.addEventListener(\"pointerdown\", onPointerDown);\n drawCanvas.addEventListener(\"pointermove\", onPointerMove);\n drawCanvas.addEventListener(\"pointerup\", onPointerEnd);\n drawCanvas.addEventListener(\"pointercancel\", onPointerEnd);\n // When pointer capture is unavailable a release outside the canvas never\n // reaches it, which would leave the stroke armed (hover keeps extending it\n // and Undo/Clear are mid-drag no-ops). Capture retargets events to the\n // canvas, so these fallbacks only fire on the uncaptured path: leaving the\n // canvas ends the stroke, and the overlay spans the viewport, so any\n // release lands somewhere in the shadow root.\n drawCanvas.addEventListener(\"pointerleave\", onPointerEnd);\n shadow.addEventListener(\"pointerup\", onPointerEnd);\n\n undoButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.undo();\n repaint();\n }),\n );\n clearButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.clear();\n repaint();\n }),\n );\n\n const onKeyDown = (event: Event): void => {\n if ((event as { key?: unknown }).key === \"Escape\") safely(close);\n };\n\n function lockScroll(): void {\n if (previousBodyOverflow !== undefined) return;\n previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n\n function unlockScroll(): void {\n if (previousBodyOverflow === undefined) return;\n document.body.style.overflow = previousBodyOverflow;\n previousBodyOverflow = undefined;\n }\n\n function setError(message: string | undefined): void {\n error.hidden = message === undefined;\n error.textContent = message ?? \"\";\n }\n\n function setFormVisible(visible: boolean): void {\n const hasPreview = sourceCanvas !== undefined;\n preview.hidden = !visible || !hasPreview;\n tools.hidden = !visible || !hasPreview;\n comment.hidden = !visible;\n actions.hidden = !visible;\n }\n\n function showOverlay(canvas: HTMLCanvasElement | undefined): void {\n overlaySession += 1;\n draftFeedbackId = uuidV7();\n sourceCanvas = canvas;\n strokes.end();\n strokes.clear();\n comment.value = \"\";\n setError(undefined);\n success.hidden = true;\n submitButton.disabled = false;\n if (canvas) {\n canvas.classList.add(\"shot\");\n preview.insertBefore(canvas, drawCanvas);\n drawCanvas.width = canvas.width;\n drawCanvas.height = canvas.height;\n penWidth = PEN_WIDTH * (pixelRatio > 0 ? pixelRatio : 1);\n repaint();\n }\n selectColor(colors[0] ?? DEFAULT_COLORS[0]!);\n setFormVisible(true);\n overlay.hidden = false;\n overlayOpen = true;\n lockScroll();\n document.addEventListener(\"keydown\", onKeyDown);\n }\n\n function close(): void {\n if (!overlayOpen) return;\n overlaySession += 1;\n overlayOpen = false;\n overlay.hidden = true;\n document.removeEventListener(\"keydown\", onKeyDown);\n unlockScroll();\n if (closeTimer !== undefined) {\n clearTimeout(closeTimer);\n closeTimer = undefined;\n }\n sending = false;\n if (sourceCanvas) {\n sourceCanvas.remove();\n sourceCanvas = undefined;\n }\n strokes.end();\n strokes.clear();\n }\n\n function open(): void {\n if (destroyed || overlayOpen || capturing) return;\n capturing = true;\n void Promise.resolve()\n .then(() => deps.capture(host, pixelRatio))\n .then(\n (canvas) => {\n capturing = false;\n if (destroyed) return;\n showOverlay(canvas);\n },\n (captureError: unknown) => {\n capturing = false;\n if (destroyed) return;\n report(captureError);\n showOverlay(undefined);\n },\n );\n }\n\n function buildBody(text: string, screenshot: ExportedImage | null): string {\n const body: Record<string, unknown> = {\n feedback_id: draftFeedbackId,\n text: text.slice(0, MAX_TEXT_LENGTH),\n };\n if (screenshot) {\n body.screenshot = screenshot.base64;\n body.screenshot_content_type = screenshot.contentType;\n }\n const distinctId = config.getDistinctId();\n if (distinctId) body.distinct_id = distinctId;\n const sessionId = config.getSessionId();\n if (sessionId) body.session_id = sessionId;\n if (typeof location !== \"undefined\" && location.href) {\n body.page_url = location.href.slice(0, MAX_PAGE_URL_LENGTH);\n }\n body.context = config.getContext();\n return JSON.stringify(body);\n }\n\n async function submit(): Promise<void> {\n if (sending || !overlayOpen) return;\n const text = comment.value.trim();\n if (text.length === 0) {\n setError(\"Please add a short comment before sending.\");\n return;\n }\n const session = overlaySession;\n const sameSession = (): boolean => !destroyed && overlayOpen && overlaySession === session;\n sending = true;\n submitButton.disabled = true;\n setError(undefined);\n try {\n let screenshot: ExportedImage | null = null;\n if (sourceCanvas) {\n screenshot = await deps.exportAnnotated(sourceCanvas, strokes.list(), {\n maxBytes: MAX_SCREENSHOT_BYTES,\n });\n }\n if (!config.fetch) throw new Error(\"fetch is not available in this environment\");\n const response = await config.fetch(config.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-DevTeam-Key\": config.key },\n body: buildBody(text, screenshot),\n });\n if (!sameSession()) return;\n if (response.ok) {\n setFormVisible(false);\n success.hidden = false;\n closeTimer = setTimeout(() => safely(close), SUCCESS_CLOSE_DELAY_MS);\n } else {\n setError(\"Could not send feedback. Please try again.\");\n }\n } catch (submitError) {\n report(submitError);\n if (sameSession()) setError(\"Could not send feedback. Please try again.\");\n } finally {\n if (overlaySession === session) {\n sending = false;\n submitButton.disabled = false;\n }\n }\n }\n\n fab.addEventListener(\"click\", () => safely(open));\n closeButton.addEventListener(\"click\", () => safely(close));\n cancelButton.addEventListener(\"click\", () => safely(close));\n backdrop.addEventListener(\"click\", () => safely(close));\n submitButton.addEventListener(\"click\", () => safely(() => void submit()));\n\n return {\n open: () => safely(open),\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n safely(() => {\n close();\n document.removeEventListener(\"keydown\", onKeyDown);\n host.remove();\n });\n },\n };\n}\n\nfunction element<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n node.className = className;\n return node;\n}\n\nfunction buildStyles(options: WebFeedbackOptions, accent: string): string {\n const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;\n const side = options.position === \"bottom-left\" ? \"left\" : \"right\";\n return `\n:host { all: initial; }\n* { box-sizing: border-box; }\n[hidden] { display: none !important; }\n.container {\n position: fixed;\n inset: 0;\n z-index: ${zIndex};\n pointer-events: none;\n font-family: system-ui, -apple-system, sans-serif;\n color: #1f2933;\n}\n.fab {\n position: absolute;\n bottom: 20px;\n ${side}: 20px;\n pointer-events: auto;\n border: none;\n border-radius: 999px;\n padding: 10px 18px;\n background: ${accent};\n color: #fff;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n}\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgba(15, 23, 42, 0.55);\n}\n.panel {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: min(560px, calc(100vw - 32px));\n max-height: calc(100vh - 32px);\n overflow: auto;\n background: #fff;\n border-radius: 12px;\n padding: 16px;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);\n}\n.header { display: flex; align-items: center; justify-content: space-between; }\n.title { font-size: 16px; font-weight: 600; }\n.close {\n border: none;\n background: none;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n color: #52606d;\n padding: 4px 8px;\n}\n.preview {\n position: relative;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n overflow: hidden;\n background: #f5f7fa;\n}\n.preview .shot { display: block; width: 100%; height: auto; }\n.preview .draw {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n cursor: crosshair;\n touch-action: none;\n}\n.tools { display: flex; align-items: center; gap: 8px; }\n.swatch {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n border: 2px solid transparent;\n cursor: pointer;\n padding: 0;\n}\n.swatch.active { border-color: #1f2933; }\n.tool {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 6px;\n padding: 4px 10px;\n font-size: 13px;\n cursor: pointer;\n}\n.comment {\n width: 100%;\n min-height: 72px;\n resize: vertical;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n}\n.error { color: #b91c1c; font-size: 13px; }\n.success { color: #15803d; font-size: 14px; font-weight: 600; }\n.actions { display: flex; justify-content: flex-end; gap: 8px; }\n.cancel {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 8px;\n padding: 8px 14px;\n font-size: 14px;\n cursor: pointer;\n}\n.submit {\n border: none;\n background: ${accent};\n color: #fff;\n border-radius: 8px;\n padding: 8px 16px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n.submit:disabled { opacity: 0.6; cursor: default; }\n`;\n}\n","import {\n createClient,\n utf8ByteLength,\n type AnalyticsClient,\n type ClientConfig,\n type EventContext,\n type FetchLike,\n type FetchRequestInit,\n type FetchResponseLike,\n} from \"@getdevteam/analytics-core\";\nimport { collectWebContext } from \"./context\";\nimport { installPageviewTracking } from \"./pageviews\";\nimport { createBrowserStorage } from \"./storage\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\n\nexport const WEB_SDK_NAME = \"@getdevteam/analytics-web\";\nexport const WEB_SDK_VERSION = \"0.3.0\";\n\n// The browser turns a `keepalive` request whose body exceeds 64 KiB into a network error, so only a body that\n// fits may take that path.\nconst KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;\n\nconst textEncoder = typeof TextEncoder !== \"undefined\" ? new TextEncoder() : undefined;\n\nfunction byteLength(value: string): number {\n return textEncoder ? textEncoder.encode(value).length : utf8ByteLength(value);\n}\n\nexport interface WebAnalyticsConfig extends ClientConfig {\n autoCaptureErrors?: boolean;\n trackPageviews?: boolean;\n context?: Partial<EventContext>;\n allowUserFeedback?: boolean;\n feedback?: WebFeedbackOptions;\n}\n\nexport interface WebAnalyticsClient extends AnalyticsClient {\n /** Opens the user-feedback overlay. A no-op when `allowUserFeedback` is off or the UI is unavailable. */\n showFeedback(): void;\n}\n\nexport function createAnalytics(config: WebAnalyticsConfig): WebAnalyticsClient {\n let finalFlush = false;\n const baseFetch = config.fetch ?? defaultBrowserFetch();\n\n const reportError = (error: unknown): void => {\n try {\n config.onError?.(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n\n const client = createClient({\n ...config,\n storage: config.storage ?? createBrowserStorage(),\n fetch: (url, init) => {\n if (finalFlush) return sendOnHide(url, init, baseFetch);\n if (baseFetch) return baseFetch(url, init);\n return Promise.reject(new Error(\"fetch is not available in this environment\"));\n },\n });\n\n const webContext: EventContext = {\n sdk_name: WEB_SDK_NAME,\n sdk_version: WEB_SDK_VERSION,\n ...collectWebContext(),\n ...config.context,\n };\n client.setContext(webContext);\n\n // Feedback reads the context at submit time, so it must track later\n // setContext calls. The core client keeps its merged copy private, so the\n // wrapper mirrors the merge (an undefined value deletes the key) here.\n let currentContext = webContext;\n const setContext = (partial: Partial<EventContext>): void => {\n client.setContext(partial);\n try {\n const next: EventContext = { ...currentContext };\n for (const [key, value] of Object.entries(partial)) {\n if (value === undefined) {\n delete next[key as keyof EventContext];\n } else {\n (next as Record<string, unknown>)[key] = value;\n }\n }\n currentContext = next;\n } catch {\n // mirrors the core client, whose setContext never throws into the host app\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (typeof window !== \"undefined\") {\n const flushOnHide = (): void => {\n finalFlush = true;\n // sendOnHide delivers over `keepalive`, so constrain each batch under the browser's 64 KiB keepalive body\n // limit. The first sub-request always fits the quota; the rest are best-effort (they drain fully when the\n // tab is hidden but alive; on a real teardown the remainder stays persisted for the next visit).\n void client.flush({ keepaliveConstrained: true }).finally(() => {\n finalFlush = false;\n });\n };\n const onPageHide = (): void => flushOnHide();\n const onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") flushOnHide();\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onVisibilityChange);\n teardowns.push(() => {\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n });\n\n if (config.autoCaptureErrors !== false) {\n const onError = (event: ErrorEvent): void => {\n client.captureError(event.error ?? event.message, { source: \"window.onerror\" });\n };\n const onRejection = (event: Event): void => {\n const reason = (event as { reason?: unknown }).reason;\n client.captureError(reason ?? \"unhandled promise rejection\", {\n source: \"unhandledrejection\",\n });\n };\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n });\n }\n\n if (config.trackPageviews === true) {\n teardowns.push(installPageviewTracking(client));\n }\n }\n\n let feedback: FeedbackHandle | undefined;\n let feedbackDisposed = false;\n\n if (\n config.allowUserFeedback === true &&\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\" &&\n typeof config.host === \"string\"\n ) {\n const feedbackUrl = `${config.host.replace(/\\/+$/, \"\")}/v1/ingest/feedback`;\n // The feedback UI (and its html-to-image dependency) loads only when the feature is on;\n // a failed load reports through onError and leaves the feature off.\n void import(\"./feedback\")\n .then((module) => {\n if (feedbackDisposed) return;\n feedback = module.mountFeedback({\n url: feedbackUrl,\n key: config.key,\n fetch: baseFetch,\n getDistinctId: () => client.getDistinctId(),\n getSessionId: () => client.getSessionId(),\n getContext: () => currentContext,\n options: config.feedback ?? {},\n onError: reportError,\n });\n teardowns.push(() => {\n feedback?.destroy();\n feedback = undefined;\n });\n })\n .catch(reportError);\n }\n\n return {\n ...client,\n setContext,\n showFeedback: () => {\n try {\n feedback?.open();\n } catch (error) {\n reportError(error);\n }\n },\n shutdown: async () => {\n feedbackDisposed = true;\n for (const teardown of teardowns.splice(0)) {\n teardown();\n }\n await client.shutdown();\n },\n };\n}\n\nfunction defaultBrowserFetch(): FetchLike | undefined {\n if (typeof fetch !== \"function\") return undefined;\n return (url, init) => fetch(url, init);\n}\n\nfunction sendOnHide(\n url: string,\n init: FetchRequestInit,\n baseFetch: FetchLike | undefined,\n): Promise<FetchResponseLike> {\n if (baseFetch) {\n // Only the page-hide flush sizes its batches to the keepalive budget; a flush already in progress when the\n // tab hid still carries full-size ones. Those go over an ordinary fetch, because `keepalive` would reject\n // every attempt and burn the retry budget on a transport that can never accept them.\n if (byteLength(init.body) > KEEPALIVE_MAX_BODY_BYTES) {\n return baseFetch(url, init);\n }\n return baseFetch(url, { ...init, keepalive: true });\n }\n // No sendBeacon fallback. It cannot set headers, and the ingest API only ever reads the key from the\n // X-DevTeam-Key header - there is no query-parameter form of it anywhere in the middleware or the edge config -\n // so a beacon can only be answered 401. Worse, sendBeacon returns true when the request is merely QUEUED, so\n // reporting that as delivered made the client drop the batch it had already taken off the queue. Rejecting\n // instead routes it through the retry path, which requeues it and leaves it in storage for the next visit.\n return Promise.reject(new Error(\"no transport available for final flush\"));\n}\n\nexport { collectWebContext, browserFromUserAgent, osFromUserAgent, detectDeviceType } from \"./context\";\nexport { createBrowserStorage } from \"./storage\";\nexport { currentPageProperties, installPageviewTracking } from \"./pageviews\";\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\nexport type {\n AnalyticsClient,\n AnalyticsLogger,\n ClientConfig,\n EventContext,\n FetchLike,\n LogSeverity,\n Properties,\n StorageAdapter,\n} from \"@getdevteam/analytics-core\";\n","import type { EventContext } from \"@getdevteam/analytics-core\";\n\nexport function browserFromUserAgent(ua: string): string | undefined {\n if (/Edg(e|A|iOS)?\\//.test(ua)) return \"Edge\";\n if (/Firefox\\/|FxiOS\\//.test(ua)) return \"Firefox\";\n if (/CriOS\\/|Chrome\\//.test(ua)) return \"Chrome\";\n if (/Safari\\//.test(ua)) return \"Safari\";\n return undefined;\n}\n\nexport function osFromUserAgent(\n ua: string,\n platform?: string,\n): { os_name?: string; os_version?: string } {\n if (/Windows/.test(ua)) {\n const match = /Windows NT ([\\d.]+)/.exec(ua);\n return { os_name: \"Windows\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/iPhone|iPad|iPod/.test(ua)) {\n const match = /OS (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"iOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Android/.test(ua)) {\n const match = /Android ([\\d.]+)/.exec(ua);\n return { os_name: \"Android\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/Mac OS X|Macintosh/.test(ua) || (platform ?? \"\").startsWith(\"Mac\")) {\n const match = /Mac OS X (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"macOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Linux/.test(ua) || (platform ?? \"\").startsWith(\"Linux\")) {\n return { os_name: \"Linux\" };\n }\n return {};\n}\n\ninterface WindowLike {\n matchMedia?: (query: string) => { matches: boolean };\n innerWidth?: number;\n screen?: { width?: number };\n}\n\nexport function detectDeviceType(win: WindowLike): string {\n try {\n const coarse =\n typeof win.matchMedia === \"function\" && win.matchMedia(\"(pointer: coarse)\").matches;\n if (!coarse) return \"desktop\";\n const width = win.innerWidth || win.screen?.width || 0;\n return width >= 768 ? \"tablet\" : \"mobile\";\n } catch {\n return \"desktop\";\n }\n}\n\nexport function collectWebContext(): EventContext {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return { platform: \"web\" };\n }\n const ua = navigator.userAgent ?? \"\";\n const context: EventContext = { platform: \"web\" };\n const browser = browserFromUserAgent(ua);\n if (browser !== undefined) context.browser = browser;\n Object.assign(context, osFromUserAgent(ua, navigator.platform));\n context.device_type = detectDeviceType(window);\n if (navigator.language) context.locale = navigator.language;\n if (window.screen) {\n context.screen = { w: window.screen.width ?? 0, h: window.screen.height ?? 0 };\n }\n return context;\n}\n","import type { AnalyticsClient, Properties } from \"@getdevteam/analytics-core\";\n\nexport function currentPageProperties(): Properties {\n return {\n url: window.location.href,\n title: document.title,\n };\n}\n\nexport function installPageviewTracking(client: AnalyticsClient): () => void {\n const sendPageview = (): void => client.page(currentPageProperties());\n const history = window.history;\n const originalPushState = history.pushState.bind(history);\n const originalReplaceState = history.replaceState.bind(history);\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n originalPushState(...args);\n sendPageview();\n };\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n originalReplaceState(...args);\n sendPageview();\n };\n window.addEventListener(\"popstate\", sendPageview);\n sendPageview();\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n window.removeEventListener(\"popstate\", sendPageview);\n };\n}\n","import { createMemoryStorage, type StorageAdapter } from \"@getdevteam/analytics-core\";\n\nexport function resolveLocalStorage(): Storage | undefined {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return undefined;\n const probe = \"__devteam_analytics_probe__\";\n window.localStorage.setItem(probe, \"1\");\n window.localStorage.removeItem(probe);\n return window.localStorage;\n } catch {\n return undefined;\n }\n}\n\nexport function createBrowserStorage(\n resolve: () => Storage | undefined = resolveLocalStorage,\n): StorageAdapter {\n const localStorage = resolve();\n if (!localStorage) return createMemoryStorage();\n return {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // quota or private-mode failure; queue persistence degrades gracefully\n }\n },\n removeItem: (key) => {\n try {\n localStorage.removeItem(key);\n } catch {\n // ignore\n }\n },\n };\n}\n"],"mappings":"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","names":["error","import_analytics_core","import_analytics_core","module"]}
|
|
1
|
+
{"version":3,"sources":["../src/feedback/capture.ts","../src/feedback/exportImage.ts","../src/feedback/strokes.ts","../src/feedback/index.ts","../src/index.ts","../src/context.ts","../src/pageviews.ts","../src/storage.ts"],"sourcesContent":["import { toCanvas } from \"html-to-image\";\n\nconst TRANSPARENT_PIXEL_DATA_URL =\n \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=\";\n\n// html-to-image has no clip option, so the visible viewport is captured by\n// sizing the render to the window and translating the cloned document up and\n// left by the current scroll offset.\nexport async function captureViewport(exclude: Node, pixelRatio: number): Promise<HTMLCanvasElement> {\n return toCanvas(document.documentElement, {\n filter: (node) => node !== exclude,\n imagePlaceholder: TRANSPARENT_PIXEL_DATA_URL,\n pixelRatio,\n width: window.innerWidth,\n height: window.innerHeight,\n style: {\n transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`,\n },\n });\n}\n","import type { Stroke } from \"./strokes\";\n\nexport interface ExportedImage {\n base64: string;\n contentType: \"image/png\" | \"image/jpeg\";\n}\n\nexport interface ExportImageOptions {\n maxBytes: number;\n}\n\nconst JPEG_QUALITY = 0.85;\nconst DOWNSCALE_FACTOR = 0.7;\nconst MIN_EXPORT_DIMENSION = 200;\n\nexport function drawStrokes(ctx: CanvasRenderingContext2D, strokes: readonly Stroke[]): void {\n for (const stroke of strokes) {\n const [first, ...rest] = stroke.points;\n if (!first) continue;\n ctx.strokeStyle = stroke.color;\n ctx.lineWidth = stroke.width;\n ctx.lineCap = \"round\";\n ctx.lineJoin = \"round\";\n ctx.beginPath();\n ctx.moveTo(first.x, first.y);\n if (rest.length === 0) ctx.lineTo(first.x, first.y);\n for (const point of rest) ctx.lineTo(point.x, point.y);\n ctx.stroke();\n }\n}\n\n// Composites the full-resolution screenshot with the strokes, then encodes to\n// PNG; when the PNG is over the byte budget it falls back to JPEG and, if\n// still too large, downscales in 0.7 steps until it fits. Returns null when no\n// encoding fits or the environment has no working 2d canvas.\nexport async function exportAnnotatedImage(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: ExportImageOptions,\n): Promise<ExportedImage | null> {\n try {\n const composed = compose(source, strokes);\n if (!composed) return null;\n\n const png = await encode(composed, \"image/png\");\n if (png && png.size <= options.maxBytes) {\n return { base64: await blobToBase64(png), contentType: \"image/png\" };\n }\n\n let canvas = composed;\n let jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n while (\n jpeg &&\n jpeg.size > options.maxBytes &&\n Math.min(canvas.width, canvas.height) * DOWNSCALE_FACTOR >= MIN_EXPORT_DIMENSION\n ) {\n const smaller = downscale(canvas, DOWNSCALE_FACTOR);\n if (!smaller) break;\n canvas = smaller;\n jpeg = await encode(canvas, \"image/jpeg\", JPEG_QUALITY);\n }\n if (jpeg && jpeg.size <= options.maxBytes) {\n return { base64: await blobToBase64(jpeg), contentType: \"image/jpeg\" };\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction compose(source: HTMLCanvasElement, strokes: readonly Stroke[]): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = source.width;\n canvas.height = source.height;\n const ctx = get2dContext(canvas);\n if (!ctx || canvas.width === 0 || canvas.height === 0) return null;\n // JPEG has no alpha channel and would composite transparency onto black.\n ctx.fillStyle = \"#ffffff\";\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.drawImage(source, 0, 0);\n drawStrokes(ctx, strokes);\n return canvas;\n}\n\nfunction downscale(source: HTMLCanvasElement, factor: number): HTMLCanvasElement | null {\n const canvas = document.createElement(\"canvas\");\n canvas.width = Math.max(1, Math.round(source.width * factor));\n canvas.height = Math.max(1, Math.round(source.height * factor));\n const ctx = get2dContext(canvas);\n if (!ctx) return null;\n ctx.drawImage(source, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\nfunction get2dContext(canvas: HTMLCanvasElement): CanvasRenderingContext2D | null {\n try {\n return canvas.getContext(\"2d\");\n } catch {\n return null;\n }\n}\n\nfunction encode(canvas: HTMLCanvasElement, type: string, quality?: number): Promise<Blob | null> {\n return new Promise((resolve) => {\n try {\n canvas.toBlob((blob) => resolve(blob), type, quality);\n } catch {\n resolve(null);\n }\n });\n}\n\nexport async function blobToBase64(blob: Blob): Promise<string> {\n const bytes = new Uint8Array(await blob.arrayBuffer());\n let binary = \"\";\n const chunkSize = 0x8000;\n for (let i = 0; i < bytes.length; i += chunkSize) {\n binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));\n }\n return btoa(binary);\n}\n","export interface StrokePoint {\n x: number;\n y: number;\n}\n\nexport interface Stroke {\n points: StrokePoint[];\n color: string;\n width: number;\n}\n\nexport interface StrokeStore {\n begin(color: string, width: number, point: StrokePoint): void;\n extend(point: StrokePoint): void;\n end(): void;\n undo(): void;\n clear(): void;\n list(): readonly Stroke[];\n isDrawing(): boolean;\n}\n\n// Each stroke snapshots its color and width when it starts, so a later color\n// change never restyles strokes already on the canvas. Undo and clear are\n// no-ops mid-drag to keep the active stroke consistent.\nexport function createStrokeStore(): StrokeStore {\n const strokes: Stroke[] = [];\n let active: Stroke | undefined;\n return {\n begin(color, width, point) {\n if (active) return;\n active = { points: [point], color, width };\n strokes.push(active);\n },\n extend(point) {\n active?.points.push(point);\n },\n end() {\n active = undefined;\n },\n undo() {\n if (active) return;\n strokes.pop();\n },\n clear() {\n if (active) return;\n strokes.length = 0;\n },\n list: () => strokes,\n isDrawing: () => active !== undefined,\n };\n}\n\nexport interface DisplayRect {\n left: number;\n top: number;\n width: number;\n height: number;\n}\n\n// Strokes live in the screenshot's own pixel space so they land at the same\n// spot in the full-resolution export as they did on the scaled-down preview.\n// A degenerate rect (jsdom reports zero sizes) maps 1:1 instead of dividing by zero.\nexport function mapToImagePoint(\n clientX: number,\n clientY: number,\n rect: DisplayRect,\n imageWidth: number,\n imageHeight: number,\n): StrokePoint {\n const scaleX = rect.width > 0 && imageWidth > 0 ? imageWidth / rect.width : 1;\n const scaleY = rect.height > 0 && imageHeight > 0 ? imageHeight / rect.height : 1;\n return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };\n}\n","import { uuidV7, type EventContext, type FetchLike } from \"@getdevteam/analytics-core\";\nimport { captureViewport } from \"./capture\";\nimport { drawStrokes, exportAnnotatedImage, type ExportedImage } from \"./exportImage\";\nimport { createStrokeStore, mapToImagePoint, type StrokePoint } from \"./strokes\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nimport type { Stroke } from \"./strokes\";\n\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./types\";\nexport { createStrokeStore, mapToImagePoint } from \"./strokes\";\nexport type { Stroke, StrokePoint, StrokeStore } from \"./strokes\";\nexport { exportAnnotatedImage } from \"./exportImage\";\nexport type { ExportedImage } from \"./exportImage\";\n\nexport interface FeedbackConfig {\n url: string;\n key: string;\n fetch: FetchLike | undefined;\n getDistinctId(): string;\n getSessionId(): string;\n getContext(): EventContext;\n options: WebFeedbackOptions;\n onError(error: unknown): void;\n}\n\nexport interface FeedbackDeps {\n capture(exclude: HTMLElement, pixelRatio: number): Promise<HTMLCanvasElement>;\n exportAnnotated(\n source: HTMLCanvasElement,\n strokes: readonly Stroke[],\n options: { maxBytes: number },\n ): Promise<ExportedImage | null>;\n}\n\nconst DEFAULT_COLORS = [\"#f44336\", \"#4caf50\", \"#2196f3\", \"#ffeb3b\"];\nconst DEFAULT_ACCENT = \"#5b9cf5\";\nconst DEFAULT_Z_INDEX = 2147483000;\nconst PEN_WIDTH = 5;\nconst MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;\nconst MAX_TEXT_LENGTH = 10000;\nconst MAX_PAGE_URL_LENGTH = 2048;\nconst SUCCESS_CLOSE_DELAY_MS = 1600;\n\nconst defaultDeps: FeedbackDeps = {\n capture: captureViewport,\n exportAnnotated: exportAnnotatedImage,\n};\n\nexport function mountFeedback(config: FeedbackConfig, deps: FeedbackDeps = defaultDeps): FeedbackHandle {\n const options = config.options;\n const colors = options.colors && options.colors.length > 0 ? options.colors : DEFAULT_COLORS;\n const accent = options.accentColor ?? DEFAULT_ACCENT;\n const pixelRatio =\n options.pixelRatio ??\n Math.min(typeof devicePixelRatio === \"number\" && devicePixelRatio > 0 ? devicePixelRatio : 1, 2);\n\n let destroyed = false;\n let overlayOpen = false;\n // Bumped on every open and close: a submit resolving for an earlier overlay\n // session (sent, then closed and reopened mid-flight) must not paint its\n // outcome into, or auto-close, the new draft.\n let overlaySession = 0;\n let capturing = false;\n let sending = false;\n // One idempotency key per draft (the contract dedupes on it), so retrying a\n // failed send cannot store the same feedback twice; re-minted on each open.\n let draftFeedbackId = uuidV7();\n let sourceCanvas: HTMLCanvasElement | undefined;\n let activeColor = colors[0] ?? DEFAULT_COLORS[0]!;\n let penWidth = PEN_WIDTH;\n let previousBodyOverflow: string | undefined;\n let closeTimer: ReturnType<typeof setTimeout> | undefined;\n let drawContext: CanvasRenderingContext2D | null | undefined;\n const strokes = createStrokeStore();\n\n const report = (error: unknown): void => {\n try {\n config.onError(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n const safely = (fn: () => void): void => {\n try {\n fn();\n } catch (error) {\n report(error);\n }\n };\n\n const host = document.createElement(\"div\");\n host.setAttribute(\"data-devteam-feedback\", \"\");\n const shadow = host.attachShadow({ mode: \"open\" });\n\n const style = document.createElement(\"style\");\n style.textContent = buildStyles(options, accent);\n\n const container = element(\"div\", \"container\");\n const fab = element(\"button\", \"fab\");\n fab.type = \"button\";\n fab.textContent = options.buttonLabel ?? \"Feedback\";\n\n const overlay = element(\"div\", \"overlay\");\n overlay.hidden = true;\n const backdrop = element(\"div\", \"backdrop\");\n const panel = element(\"div\", \"panel\");\n panel.setAttribute(\"role\", \"dialog\");\n panel.setAttribute(\"aria-modal\", \"true\");\n\n const header = element(\"div\", \"header\");\n const title = element(\"div\", \"title\");\n title.textContent = options.title ?? \"Send feedback\";\n const closeButton = element(\"button\", \"close\");\n closeButton.type = \"button\";\n closeButton.setAttribute(\"aria-label\", \"Close\");\n closeButton.textContent = \"×\";\n header.append(title, closeButton);\n\n const preview = element(\"div\", \"preview\");\n const drawCanvas = document.createElement(\"canvas\");\n drawCanvas.className = \"draw\";\n preview.appendChild(drawCanvas);\n\n const tools = element(\"div\", \"tools\");\n const swatches = colors.map((color) => {\n const swatch = element(\"button\", \"swatch\");\n swatch.type = \"button\";\n swatch.style.background = color;\n swatch.setAttribute(\"aria-label\", `Pen color ${color}`);\n swatch.addEventListener(\"click\", () => safely(() => selectColor(color)));\n tools.appendChild(swatch);\n return { color, swatch };\n });\n const undoButton = element(\"button\", \"tool undo\");\n undoButton.type = \"button\";\n undoButton.textContent = \"Undo\";\n const clearButton = element(\"button\", \"tool clear\");\n clearButton.type = \"button\";\n clearButton.textContent = \"Clear\";\n tools.append(undoButton, clearButton);\n\n const comment = document.createElement(\"textarea\");\n comment.className = \"comment\";\n comment.placeholder = options.commentPlaceholder ?? \"What went wrong, or what could be better?\";\n comment.maxLength = MAX_TEXT_LENGTH;\n\n const error = element(\"div\", \"error\");\n error.hidden = true;\n const success = element(\"div\", \"success\");\n success.hidden = true;\n success.textContent = options.successMessage ?? \"Thanks for the feedback!\";\n\n const actions = element(\"div\", \"actions\");\n const cancelButton = element(\"button\", \"cancel\");\n cancelButton.type = \"button\";\n cancelButton.textContent = \"Cancel\";\n const submitButton = element(\"button\", \"submit\");\n submitButton.type = \"button\";\n submitButton.textContent = options.submitLabel ?? \"Send\";\n actions.append(cancelButton, submitButton);\n\n panel.append(header, preview, tools, comment, error, success, actions);\n overlay.append(backdrop, panel);\n container.append(fab, overlay);\n shadow.append(style, container);\n\n if (document.body) {\n document.body.appendChild(host);\n } else {\n document.addEventListener(\n \"DOMContentLoaded\",\n () => {\n if (!destroyed) document.body?.appendChild(host);\n },\n { once: true },\n );\n }\n\n function selectColor(color: string): void {\n activeColor = color;\n for (const entry of swatches) {\n entry.swatch.classList.toggle(\"active\", entry.color === color);\n }\n }\n selectColor(activeColor);\n\n function context2d(): CanvasRenderingContext2D | null {\n if (drawContext === undefined) {\n try {\n drawContext = drawCanvas.getContext(\"2d\");\n } catch {\n drawContext = null;\n }\n }\n return drawContext;\n }\n\n function repaint(): void {\n const ctx = context2d();\n if (!ctx) return;\n ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);\n drawStrokes(ctx, strokes.list());\n }\n\n function pointFromEvent(event: Event): StrokePoint | undefined {\n const { clientX, clientY } = event as { clientX?: unknown; clientY?: unknown };\n if (typeof clientX !== \"number\" || typeof clientY !== \"number\") return undefined;\n const rect = drawCanvas.getBoundingClientRect();\n return mapToImagePoint(clientX, clientY, rect, drawCanvas.width, drawCanvas.height);\n }\n\n function withPointerCapture(event: Event, action: \"set\" | \"release\"): void {\n const pointerId = (event as { pointerId?: unknown }).pointerId;\n if (typeof pointerId !== \"number\") return;\n const method = action === \"set\" ? drawCanvas.setPointerCapture : drawCanvas.releasePointerCapture;\n if (typeof method !== \"function\") return;\n try {\n method.call(drawCanvas, pointerId);\n } catch {\n // pointer capture is a nicety; drawing works without it\n }\n }\n\n const onPointerDown = (event: Event): void =>\n safely(() => {\n if (!sourceCanvas || sending) return;\n const point = pointFromEvent(event);\n if (!point) return;\n if (typeof event.preventDefault === \"function\") event.preventDefault();\n withPointerCapture(event, \"set\");\n strokes.begin(activeColor, penWidth, point);\n repaint();\n });\n const onPointerMove = (event: Event): void =>\n safely(() => {\n if (!strokes.isDrawing()) return;\n const point = pointFromEvent(event);\n if (!point) return;\n strokes.extend(point);\n repaint();\n });\n const onPointerEnd = (event: Event): void =>\n safely(() => {\n withPointerCapture(event, \"release\");\n strokes.end();\n });\n drawCanvas.addEventListener(\"pointerdown\", onPointerDown);\n drawCanvas.addEventListener(\"pointermove\", onPointerMove);\n drawCanvas.addEventListener(\"pointerup\", onPointerEnd);\n drawCanvas.addEventListener(\"pointercancel\", onPointerEnd);\n // When pointer capture is unavailable a release outside the canvas never\n // reaches it, which would leave the stroke armed (hover keeps extending it\n // and Undo/Clear are mid-drag no-ops). Capture retargets events to the\n // canvas, so these fallbacks only fire on the uncaptured path: leaving the\n // canvas ends the stroke, and the overlay spans the viewport, so any\n // release lands somewhere in the shadow root.\n drawCanvas.addEventListener(\"pointerleave\", onPointerEnd);\n shadow.addEventListener(\"pointerup\", onPointerEnd);\n\n undoButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.undo();\n repaint();\n }),\n );\n clearButton.addEventListener(\"click\", () =>\n safely(() => {\n strokes.clear();\n repaint();\n }),\n );\n\n const onKeyDown = (event: Event): void => {\n if ((event as { key?: unknown }).key === \"Escape\") safely(close);\n };\n\n function lockScroll(): void {\n if (previousBodyOverflow !== undefined) return;\n previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n }\n\n function unlockScroll(): void {\n if (previousBodyOverflow === undefined) return;\n document.body.style.overflow = previousBodyOverflow;\n previousBodyOverflow = undefined;\n }\n\n function setError(message: string | undefined): void {\n error.hidden = message === undefined;\n error.textContent = message ?? \"\";\n }\n\n function setFormVisible(visible: boolean): void {\n const hasPreview = sourceCanvas !== undefined;\n preview.hidden = !visible || !hasPreview;\n tools.hidden = !visible || !hasPreview;\n comment.hidden = !visible;\n actions.hidden = !visible;\n }\n\n function showOverlay(canvas: HTMLCanvasElement | undefined): void {\n overlaySession += 1;\n draftFeedbackId = uuidV7();\n sourceCanvas = canvas;\n strokes.end();\n strokes.clear();\n comment.value = \"\";\n setError(undefined);\n success.hidden = true;\n submitButton.disabled = false;\n if (canvas) {\n canvas.classList.add(\"shot\");\n preview.insertBefore(canvas, drawCanvas);\n drawCanvas.width = canvas.width;\n drawCanvas.height = canvas.height;\n penWidth = PEN_WIDTH * (pixelRatio > 0 ? pixelRatio : 1);\n repaint();\n }\n selectColor(colors[0] ?? DEFAULT_COLORS[0]!);\n setFormVisible(true);\n overlay.hidden = false;\n overlayOpen = true;\n lockScroll();\n document.addEventListener(\"keydown\", onKeyDown);\n }\n\n function close(): void {\n if (!overlayOpen) return;\n overlaySession += 1;\n overlayOpen = false;\n overlay.hidden = true;\n document.removeEventListener(\"keydown\", onKeyDown);\n unlockScroll();\n if (closeTimer !== undefined) {\n clearTimeout(closeTimer);\n closeTimer = undefined;\n }\n sending = false;\n if (sourceCanvas) {\n sourceCanvas.remove();\n sourceCanvas = undefined;\n }\n strokes.end();\n strokes.clear();\n }\n\n function open(): void {\n if (destroyed || overlayOpen || capturing) return;\n capturing = true;\n void Promise.resolve()\n .then(() => deps.capture(host, pixelRatio))\n .then(\n (canvas) => {\n capturing = false;\n if (destroyed) return;\n showOverlay(canvas);\n },\n (captureError: unknown) => {\n capturing = false;\n if (destroyed) return;\n report(captureError);\n showOverlay(undefined);\n },\n );\n }\n\n function buildBody(text: string, screenshot: ExportedImage | null): string {\n const body: Record<string, unknown> = {\n feedback_id: draftFeedbackId,\n text: text.slice(0, MAX_TEXT_LENGTH),\n };\n if (screenshot) {\n body.screenshot = screenshot.base64;\n body.screenshot_content_type = screenshot.contentType;\n }\n const distinctId = config.getDistinctId();\n if (distinctId) body.distinct_id = distinctId;\n const sessionId = config.getSessionId();\n if (sessionId) body.session_id = sessionId;\n if (typeof location !== \"undefined\" && location.href) {\n body.page_url = location.href.slice(0, MAX_PAGE_URL_LENGTH);\n }\n body.context = config.getContext();\n return JSON.stringify(body);\n }\n\n async function submit(): Promise<void> {\n if (sending || !overlayOpen) return;\n const text = comment.value.trim();\n if (text.length === 0) {\n setError(\"Please add a short comment before sending.\");\n return;\n }\n const session = overlaySession;\n const sameSession = (): boolean => !destroyed && overlayOpen && overlaySession === session;\n sending = true;\n submitButton.disabled = true;\n setError(undefined);\n try {\n let screenshot: ExportedImage | null = null;\n if (sourceCanvas) {\n screenshot = await deps.exportAnnotated(sourceCanvas, strokes.list(), {\n maxBytes: MAX_SCREENSHOT_BYTES,\n });\n }\n if (!config.fetch) throw new Error(\"fetch is not available in this environment\");\n const response = await config.fetch(config.url, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\", \"X-DevTeam-Key\": config.key },\n body: buildBody(text, screenshot),\n });\n if (!sameSession()) return;\n if (response.ok) {\n setFormVisible(false);\n success.hidden = false;\n closeTimer = setTimeout(() => safely(close), SUCCESS_CLOSE_DELAY_MS);\n } else {\n setError(\"Could not send feedback. Please try again.\");\n }\n } catch (submitError) {\n report(submitError);\n if (sameSession()) setError(\"Could not send feedback. Please try again.\");\n } finally {\n if (overlaySession === session) {\n sending = false;\n submitButton.disabled = false;\n }\n }\n }\n\n fab.addEventListener(\"click\", () => safely(open));\n closeButton.addEventListener(\"click\", () => safely(close));\n cancelButton.addEventListener(\"click\", () => safely(close));\n backdrop.addEventListener(\"click\", () => safely(close));\n submitButton.addEventListener(\"click\", () => safely(() => void submit()));\n\n return {\n open: () => safely(open),\n destroy: () => {\n if (destroyed) return;\n destroyed = true;\n safely(() => {\n close();\n document.removeEventListener(\"keydown\", onKeyDown);\n host.remove();\n });\n },\n };\n}\n\nfunction element<K extends keyof HTMLElementTagNameMap>(\n tag: K,\n className: string,\n): HTMLElementTagNameMap[K] {\n const node = document.createElement(tag);\n node.className = className;\n return node;\n}\n\nfunction buildStyles(options: WebFeedbackOptions, accent: string): string {\n const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;\n const side = options.position === \"bottom-left\" ? \"left\" : \"right\";\n return `\n:host { all: initial; }\n* { box-sizing: border-box; }\n[hidden] { display: none !important; }\n.container {\n position: fixed;\n inset: 0;\n z-index: ${zIndex};\n pointer-events: none;\n font-family: system-ui, -apple-system, sans-serif;\n color: #1f2933;\n}\n.fab {\n position: absolute;\n bottom: 20px;\n ${side}: 20px;\n pointer-events: auto;\n border: none;\n border-radius: 999px;\n padding: 10px 18px;\n background: ${accent};\n color: #fff;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n}\n.overlay {\n position: absolute;\n inset: 0;\n pointer-events: auto;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n.backdrop {\n position: absolute;\n inset: 0;\n background: rgba(15, 23, 42, 0.55);\n}\n.panel {\n position: relative;\n display: flex;\n flex-direction: column;\n gap: 10px;\n width: min(560px, calc(100vw - 32px));\n max-height: calc(100vh - 32px);\n overflow: auto;\n background: #fff;\n border-radius: 12px;\n padding: 16px;\n box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);\n}\n.header { display: flex; align-items: center; justify-content: space-between; }\n.title { font-size: 16px; font-weight: 600; }\n.close {\n border: none;\n background: none;\n font-size: 20px;\n line-height: 1;\n cursor: pointer;\n color: #52606d;\n padding: 4px 8px;\n}\n.preview {\n position: relative;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n overflow: hidden;\n background: #f5f7fa;\n}\n.preview .shot { display: block; width: 100%; height: auto; }\n.preview .draw {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n cursor: crosshair;\n touch-action: none;\n}\n.tools { display: flex; align-items: center; gap: 8px; }\n.swatch {\n width: 24px;\n height: 24px;\n border-radius: 50%;\n border: 2px solid transparent;\n cursor: pointer;\n padding: 0;\n}\n.swatch.active { border-color: #1f2933; }\n.tool {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 6px;\n padding: 4px 10px;\n font-size: 13px;\n cursor: pointer;\n}\n.comment {\n width: 100%;\n min-height: 72px;\n resize: vertical;\n border: 1px solid #d3dce6;\n border-radius: 8px;\n padding: 8px 10px;\n font: inherit;\n}\n.error { color: #b91c1c; font-size: 13px; }\n.success { color: #15803d; font-size: 14px; font-weight: 600; }\n.actions { display: flex; justify-content: flex-end; gap: 8px; }\n.cancel {\n border: 1px solid #d3dce6;\n background: #fff;\n border-radius: 8px;\n padding: 8px 14px;\n font-size: 14px;\n cursor: pointer;\n}\n.submit {\n border: none;\n background: ${accent};\n color: #fff;\n border-radius: 8px;\n padding: 8px 16px;\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n}\n.submit:disabled { opacity: 0.6; cursor: default; }\n`;\n}\n","import {\n createClient,\n utf8ByteLength,\n type AnalyticsClient,\n type ClientConfig,\n type EventContext,\n type FetchLike,\n type FetchRequestInit,\n type FetchResponseLike,\n} from \"@getdevteam/analytics-core\";\nimport { collectWebContext } from \"./context\";\nimport { installPageviewTracking } from \"./pageviews\";\nimport { createBrowserStorage } from \"./storage\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\n\nexport const WEB_SDK_NAME = \"@getdevteam/analytics-web\";\nexport const WEB_SDK_VERSION = \"0.3.1\";\n\n// The browser turns a `keepalive` request whose body exceeds 64 KiB into a network error, so only a body that\n// fits may take that path.\nconst KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;\n\nconst textEncoder = typeof TextEncoder !== \"undefined\" ? new TextEncoder() : undefined;\n\nfunction byteLength(value: string): number {\n return textEncoder ? textEncoder.encode(value).length : utf8ByteLength(value);\n}\n\nexport interface WebAnalyticsConfig extends ClientConfig {\n autoCaptureErrors?: boolean;\n trackPageviews?: boolean;\n context?: Partial<EventContext>;\n allowUserFeedback?: boolean;\n feedback?: WebFeedbackOptions;\n}\n\nexport interface WebAnalyticsClient extends AnalyticsClient {\n /** Opens the user-feedback overlay. A no-op when `allowUserFeedback` is off or the UI is unavailable. */\n showFeedback(): void;\n}\n\nexport function createAnalytics(config: WebAnalyticsConfig): WebAnalyticsClient {\n let finalFlush = false;\n const baseFetch = config.fetch ?? defaultBrowserFetch();\n\n const reportError = (error: unknown): void => {\n try {\n config.onError?.(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n\n const client = createClient({\n ...config,\n storage: config.storage ?? createBrowserStorage(),\n fetch: (url, init) => {\n if (finalFlush) return sendOnHide(url, init, baseFetch);\n if (baseFetch) return baseFetch(url, init);\n return Promise.reject(new Error(\"fetch is not available in this environment\"));\n },\n });\n\n const webContext: EventContext = {\n sdk_name: WEB_SDK_NAME,\n sdk_version: WEB_SDK_VERSION,\n ...collectWebContext(),\n ...config.context,\n };\n client.setContext(webContext);\n\n // Feedback reads the context at submit time, so it must track later\n // setContext calls. The core client keeps its merged copy private, so the\n // wrapper mirrors the merge (an undefined value deletes the key) here.\n let currentContext = webContext;\n const setContext = (partial: Partial<EventContext>): void => {\n client.setContext(partial);\n try {\n const next: EventContext = { ...currentContext };\n for (const [key, value] of Object.entries(partial)) {\n if (value === undefined) {\n delete next[key as keyof EventContext];\n } else {\n (next as Record<string, unknown>)[key] = value;\n }\n }\n currentContext = next;\n } catch {\n // mirrors the core client, whose setContext never throws into the host app\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (typeof window !== \"undefined\") {\n const flushOnHide = (): void => {\n finalFlush = true;\n // sendOnHide delivers over `keepalive`, so constrain each batch under the browser's 64 KiB keepalive body\n // limit. The first sub-request always fits the quota; the rest are best-effort (they drain fully when the\n // tab is hidden but alive; on a real teardown the remainder stays persisted for the next visit).\n void client.flush({ keepaliveConstrained: true }).finally(() => {\n finalFlush = false;\n });\n };\n const onPageHide = (): void => flushOnHide();\n const onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") flushOnHide();\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onVisibilityChange);\n teardowns.push(() => {\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n });\n\n if (config.autoCaptureErrors !== false) {\n const onError = (event: ErrorEvent): void => {\n client.captureError(event.error ?? event.message, { source: \"window.onerror\" });\n };\n const onRejection = (event: Event): void => {\n const reason = (event as { reason?: unknown }).reason;\n client.captureError(reason ?? \"unhandled promise rejection\", {\n source: \"unhandledrejection\",\n });\n };\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n });\n }\n\n if (config.trackPageviews === true) {\n teardowns.push(installPageviewTracking(client));\n }\n }\n\n let feedback: FeedbackHandle | undefined;\n let feedbackDisposed = false;\n\n if (\n config.allowUserFeedback === true &&\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\" &&\n typeof config.host === \"string\"\n ) {\n const feedbackUrl = `${config.host.replace(/\\/+$/, \"\")}/v1/ingest/feedback`;\n // The feedback UI (and its html-to-image dependency) loads only when the feature is on;\n // a failed load reports through onError and leaves the feature off.\n void import(\"./feedback\")\n .then((module) => {\n if (feedbackDisposed) return;\n feedback = module.mountFeedback({\n url: feedbackUrl,\n key: config.key,\n fetch: baseFetch,\n getDistinctId: () => client.getDistinctId(),\n getSessionId: () => client.getSessionId(),\n getContext: () => currentContext,\n options: config.feedback ?? {},\n onError: reportError,\n });\n teardowns.push(() => {\n feedback?.destroy();\n feedback = undefined;\n });\n })\n .catch(reportError);\n }\n\n return {\n ...client,\n setContext,\n showFeedback: () => {\n try {\n feedback?.open();\n } catch (error) {\n reportError(error);\n }\n },\n shutdown: async () => {\n feedbackDisposed = true;\n for (const teardown of teardowns.splice(0)) {\n teardown();\n }\n await client.shutdown();\n },\n };\n}\n\nfunction defaultBrowserFetch(): FetchLike | undefined {\n if (typeof fetch !== \"function\") return undefined;\n return (url, init) => fetch(url, init);\n}\n\nfunction sendOnHide(\n url: string,\n init: FetchRequestInit,\n baseFetch: FetchLike | undefined,\n): Promise<FetchResponseLike> {\n if (baseFetch) {\n // Only the page-hide flush sizes its batches to the keepalive budget; a flush already in progress when the\n // tab hid still carries full-size ones. Those go over an ordinary fetch, because `keepalive` would reject\n // every attempt and burn the retry budget on a transport that can never accept them.\n if (byteLength(init.body) > KEEPALIVE_MAX_BODY_BYTES) {\n return baseFetch(url, init);\n }\n return baseFetch(url, { ...init, keepalive: true });\n }\n // No sendBeacon fallback. It cannot set headers, and the ingest API only ever reads the key from the\n // X-DevTeam-Key header - there is no query-parameter form of it anywhere in the middleware or the edge config -\n // so a beacon can only be answered 401. Worse, sendBeacon returns true when the request is merely QUEUED, so\n // reporting that as delivered made the client drop the batch it had already taken off the queue. Rejecting\n // instead routes it through the retry path, which requeues it and leaves it in storage for the next visit.\n return Promise.reject(new Error(\"no transport available for final flush\"));\n}\n\nexport { collectWebContext, browserFromUserAgent, osFromUserAgent, detectDeviceType } from \"./context\";\nexport { createBrowserStorage } from \"./storage\";\nexport { currentPageProperties, installPageviewTracking } from \"./pageviews\";\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\nexport type {\n AnalyticsClient,\n AnalyticsLogger,\n ClientConfig,\n EventContext,\n FetchLike,\n LogSeverity,\n Properties,\n StorageAdapter,\n} from \"@getdevteam/analytics-core\";\n","import type { EventContext } from \"@getdevteam/analytics-core\";\n\nexport function browserFromUserAgent(ua: string): string | undefined {\n if (/Edg(e|A|iOS)?\\//.test(ua)) return \"Edge\";\n if (/Firefox\\/|FxiOS\\//.test(ua)) return \"Firefox\";\n if (/CriOS\\/|Chrome\\//.test(ua)) return \"Chrome\";\n if (/Safari\\//.test(ua)) return \"Safari\";\n return undefined;\n}\n\nexport function osFromUserAgent(\n ua: string,\n platform?: string,\n): { os_name?: string; os_version?: string } {\n if (/Windows/.test(ua)) {\n const match = /Windows NT ([\\d.]+)/.exec(ua);\n return { os_name: \"Windows\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/iPhone|iPad|iPod/.test(ua)) {\n const match = /OS (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"iOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Android/.test(ua)) {\n const match = /Android ([\\d.]+)/.exec(ua);\n return { os_name: \"Android\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/Mac OS X|Macintosh/.test(ua) || (platform ?? \"\").startsWith(\"Mac\")) {\n const match = /Mac OS X (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"macOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Linux/.test(ua) || (platform ?? \"\").startsWith(\"Linux\")) {\n return { os_name: \"Linux\" };\n }\n return {};\n}\n\ninterface WindowLike {\n matchMedia?: (query: string) => { matches: boolean };\n innerWidth?: number;\n screen?: { width?: number };\n}\n\nexport function detectDeviceType(win: WindowLike): string {\n try {\n const coarse =\n typeof win.matchMedia === \"function\" && win.matchMedia(\"(pointer: coarse)\").matches;\n if (!coarse) return \"desktop\";\n const width = win.innerWidth || win.screen?.width || 0;\n return width >= 768 ? \"tablet\" : \"mobile\";\n } catch {\n return \"desktop\";\n }\n}\n\nexport function collectWebContext(): EventContext {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return { platform: \"web\" };\n }\n const ua = navigator.userAgent ?? \"\";\n const context: EventContext = { platform: \"web\" };\n const browser = browserFromUserAgent(ua);\n if (browser !== undefined) context.browser = browser;\n Object.assign(context, osFromUserAgent(ua, navigator.platform));\n context.device_type = detectDeviceType(window);\n if (navigator.language) context.locale = navigator.language;\n if (window.screen) {\n context.screen = { w: window.screen.width ?? 0, h: window.screen.height ?? 0 };\n }\n return context;\n}\n","import type { AnalyticsClient, Properties } from \"@getdevteam/analytics-core\";\n\nexport function currentPageProperties(): Properties {\n return {\n url: window.location.href,\n title: document.title,\n };\n}\n\nexport function installPageviewTracking(client: AnalyticsClient): () => void {\n const sendPageview = (): void => client.page(currentPageProperties());\n const history = window.history;\n const originalPushState = history.pushState.bind(history);\n const originalReplaceState = history.replaceState.bind(history);\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n originalPushState(...args);\n sendPageview();\n };\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n originalReplaceState(...args);\n sendPageview();\n };\n window.addEventListener(\"popstate\", sendPageview);\n sendPageview();\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n window.removeEventListener(\"popstate\", sendPageview);\n };\n}\n","import { createMemoryStorage, type StorageAdapter } from \"@getdevteam/analytics-core\";\n\nexport function resolveLocalStorage(): Storage | undefined {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return undefined;\n const probe = \"__devteam_analytics_probe__\";\n window.localStorage.setItem(probe, \"1\");\n window.localStorage.removeItem(probe);\n return window.localStorage;\n } catch {\n return undefined;\n }\n}\n\nexport function createBrowserStorage(\n resolve: () => Storage | undefined = resolveLocalStorage,\n): StorageAdapter {\n const localStorage = resolve();\n if (!localStorage) return createMemoryStorage();\n return {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // quota or private-mode failure; queue persistence degrades gracefully\n }\n },\n removeItem: (key) => {\n try {\n localStorage.removeItem(key);\n } catch {\n // ignore\n }\n },\n };\n}\n"],"mappings":"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","names":["error","import_analytics_core","import_analytics_core","module"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -41,7 +41,7 @@ declare function currentPageProperties(): Properties;
|
|
|
41
41
|
declare function installPageviewTracking(client: AnalyticsClient): () => void;
|
|
42
42
|
|
|
43
43
|
declare const WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
44
|
-
declare const WEB_SDK_VERSION = "0.3.
|
|
44
|
+
declare const WEB_SDK_VERSION = "0.3.1";
|
|
45
45
|
interface WebAnalyticsConfig extends ClientConfig {
|
|
46
46
|
autoCaptureErrors?: boolean;
|
|
47
47
|
trackPageviews?: boolean;
|
package/dist/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ declare function currentPageProperties(): Properties;
|
|
|
41
41
|
declare function installPageviewTracking(client: AnalyticsClient): () => void;
|
|
42
42
|
|
|
43
43
|
declare const WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
44
|
-
declare const WEB_SDK_VERSION = "0.3.
|
|
44
|
+
declare const WEB_SDK_VERSION = "0.3.1";
|
|
45
45
|
interface WebAnalyticsConfig extends ClientConfig {
|
|
46
46
|
autoCaptureErrors?: boolean;
|
|
47
47
|
trackPageviews?: boolean;
|
package/dist/index.js
CHANGED
|
@@ -131,7 +131,7 @@ function createBrowserStorage(resolve = resolveLocalStorage) {
|
|
|
131
131
|
|
|
132
132
|
// src/index.ts
|
|
133
133
|
var WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
134
|
-
var WEB_SDK_VERSION = "0.3.
|
|
134
|
+
var WEB_SDK_VERSION = "0.3.1";
|
|
135
135
|
var KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;
|
|
136
136
|
var textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
|
|
137
137
|
function byteLength(value) {
|
|
@@ -221,7 +221,7 @@ function createAnalytics(config) {
|
|
|
221
221
|
let feedbackDisposed = false;
|
|
222
222
|
if (config.allowUserFeedback === true && typeof window !== "undefined" && typeof document !== "undefined" && typeof config.host === "string") {
|
|
223
223
|
const feedbackUrl = `${config.host.replace(/\/+$/, "")}/v1/ingest/feedback`;
|
|
224
|
-
void import("./feedback-
|
|
224
|
+
void import("./feedback-SJHUYYJN.js").then((module) => {
|
|
225
225
|
if (feedbackDisposed) return;
|
|
226
226
|
feedback = module.mountFeedback({
|
|
227
227
|
url: feedbackUrl,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/pageviews.ts","../src/storage.ts"],"sourcesContent":["import {\n createClient,\n utf8ByteLength,\n type AnalyticsClient,\n type ClientConfig,\n type EventContext,\n type FetchLike,\n type FetchRequestInit,\n type FetchResponseLike,\n} from \"@getdevteam/analytics-core\";\nimport { collectWebContext } from \"./context\";\nimport { installPageviewTracking } from \"./pageviews\";\nimport { createBrowserStorage } from \"./storage\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\n\nexport const WEB_SDK_NAME = \"@getdevteam/analytics-web\";\nexport const WEB_SDK_VERSION = \"0.3.0\";\n\n// The browser turns a `keepalive` request whose body exceeds 64 KiB into a network error, so only a body that\n// fits may take that path.\nconst KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;\n\nconst textEncoder = typeof TextEncoder !== \"undefined\" ? new TextEncoder() : undefined;\n\nfunction byteLength(value: string): number {\n return textEncoder ? textEncoder.encode(value).length : utf8ByteLength(value);\n}\n\nexport interface WebAnalyticsConfig extends ClientConfig {\n autoCaptureErrors?: boolean;\n trackPageviews?: boolean;\n context?: Partial<EventContext>;\n allowUserFeedback?: boolean;\n feedback?: WebFeedbackOptions;\n}\n\nexport interface WebAnalyticsClient extends AnalyticsClient {\n /** Opens the user-feedback overlay. A no-op when `allowUserFeedback` is off or the UI is unavailable. */\n showFeedback(): void;\n}\n\nexport function createAnalytics(config: WebAnalyticsConfig): WebAnalyticsClient {\n let finalFlush = false;\n const baseFetch = config.fetch ?? defaultBrowserFetch();\n\n const reportError = (error: unknown): void => {\n try {\n config.onError?.(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n\n const client = createClient({\n ...config,\n storage: config.storage ?? createBrowserStorage(),\n fetch: (url, init) => {\n if (finalFlush) return sendOnHide(url, init, baseFetch);\n if (baseFetch) return baseFetch(url, init);\n return Promise.reject(new Error(\"fetch is not available in this environment\"));\n },\n });\n\n const webContext: EventContext = {\n sdk_name: WEB_SDK_NAME,\n sdk_version: WEB_SDK_VERSION,\n ...collectWebContext(),\n ...config.context,\n };\n client.setContext(webContext);\n\n // Feedback reads the context at submit time, so it must track later\n // setContext calls. The core client keeps its merged copy private, so the\n // wrapper mirrors the merge (an undefined value deletes the key) here.\n let currentContext = webContext;\n const setContext = (partial: Partial<EventContext>): void => {\n client.setContext(partial);\n try {\n const next: EventContext = { ...currentContext };\n for (const [key, value] of Object.entries(partial)) {\n if (value === undefined) {\n delete next[key as keyof EventContext];\n } else {\n (next as Record<string, unknown>)[key] = value;\n }\n }\n currentContext = next;\n } catch {\n // mirrors the core client, whose setContext never throws into the host app\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (typeof window !== \"undefined\") {\n const flushOnHide = (): void => {\n finalFlush = true;\n // sendOnHide delivers over `keepalive`, so constrain each batch under the browser's 64 KiB keepalive body\n // limit. The first sub-request always fits the quota; the rest are best-effort (they drain fully when the\n // tab is hidden but alive; on a real teardown the remainder stays persisted for the next visit).\n void client.flush({ keepaliveConstrained: true }).finally(() => {\n finalFlush = false;\n });\n };\n const onPageHide = (): void => flushOnHide();\n const onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") flushOnHide();\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onVisibilityChange);\n teardowns.push(() => {\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n });\n\n if (config.autoCaptureErrors !== false) {\n const onError = (event: ErrorEvent): void => {\n client.captureError(event.error ?? event.message, { source: \"window.onerror\" });\n };\n const onRejection = (event: Event): void => {\n const reason = (event as { reason?: unknown }).reason;\n client.captureError(reason ?? \"unhandled promise rejection\", {\n source: \"unhandledrejection\",\n });\n };\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n });\n }\n\n if (config.trackPageviews === true) {\n teardowns.push(installPageviewTracking(client));\n }\n }\n\n let feedback: FeedbackHandle | undefined;\n let feedbackDisposed = false;\n\n if (\n config.allowUserFeedback === true &&\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\" &&\n typeof config.host === \"string\"\n ) {\n const feedbackUrl = `${config.host.replace(/\\/+$/, \"\")}/v1/ingest/feedback`;\n // The feedback UI (and its html-to-image dependency) loads only when the feature is on;\n // a failed load reports through onError and leaves the feature off.\n void import(\"./feedback\")\n .then((module) => {\n if (feedbackDisposed) return;\n feedback = module.mountFeedback({\n url: feedbackUrl,\n key: config.key,\n fetch: baseFetch,\n getDistinctId: () => client.getDistinctId(),\n getSessionId: () => client.getSessionId(),\n getContext: () => currentContext,\n options: config.feedback ?? {},\n onError: reportError,\n });\n teardowns.push(() => {\n feedback?.destroy();\n feedback = undefined;\n });\n })\n .catch(reportError);\n }\n\n return {\n ...client,\n setContext,\n showFeedback: () => {\n try {\n feedback?.open();\n } catch (error) {\n reportError(error);\n }\n },\n shutdown: async () => {\n feedbackDisposed = true;\n for (const teardown of teardowns.splice(0)) {\n teardown();\n }\n await client.shutdown();\n },\n };\n}\n\nfunction defaultBrowserFetch(): FetchLike | undefined {\n if (typeof fetch !== \"function\") return undefined;\n return (url, init) => fetch(url, init);\n}\n\nfunction sendOnHide(\n url: string,\n init: FetchRequestInit,\n baseFetch: FetchLike | undefined,\n): Promise<FetchResponseLike> {\n if (baseFetch) {\n // Only the page-hide flush sizes its batches to the keepalive budget; a flush already in progress when the\n // tab hid still carries full-size ones. Those go over an ordinary fetch, because `keepalive` would reject\n // every attempt and burn the retry budget on a transport that can never accept them.\n if (byteLength(init.body) > KEEPALIVE_MAX_BODY_BYTES) {\n return baseFetch(url, init);\n }\n return baseFetch(url, { ...init, keepalive: true });\n }\n // No sendBeacon fallback. It cannot set headers, and the ingest API only ever reads the key from the\n // X-DevTeam-Key header - there is no query-parameter form of it anywhere in the middleware or the edge config -\n // so a beacon can only be answered 401. Worse, sendBeacon returns true when the request is merely QUEUED, so\n // reporting that as delivered made the client drop the batch it had already taken off the queue. Rejecting\n // instead routes it through the retry path, which requeues it and leaves it in storage for the next visit.\n return Promise.reject(new Error(\"no transport available for final flush\"));\n}\n\nexport { collectWebContext, browserFromUserAgent, osFromUserAgent, detectDeviceType } from \"./context\";\nexport { createBrowserStorage } from \"./storage\";\nexport { currentPageProperties, installPageviewTracking } from \"./pageviews\";\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\nexport type {\n AnalyticsClient,\n AnalyticsLogger,\n ClientConfig,\n EventContext,\n FetchLike,\n LogSeverity,\n Properties,\n StorageAdapter,\n} from \"@getdevteam/analytics-core\";\n","import type { EventContext } from \"@getdevteam/analytics-core\";\n\nexport function browserFromUserAgent(ua: string): string | undefined {\n if (/Edg(e|A|iOS)?\\//.test(ua)) return \"Edge\";\n if (/Firefox\\/|FxiOS\\//.test(ua)) return \"Firefox\";\n if (/CriOS\\/|Chrome\\//.test(ua)) return \"Chrome\";\n if (/Safari\\//.test(ua)) return \"Safari\";\n return undefined;\n}\n\nexport function osFromUserAgent(\n ua: string,\n platform?: string,\n): { os_name?: string; os_version?: string } {\n if (/Windows/.test(ua)) {\n const match = /Windows NT ([\\d.]+)/.exec(ua);\n return { os_name: \"Windows\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/iPhone|iPad|iPod/.test(ua)) {\n const match = /OS (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"iOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Android/.test(ua)) {\n const match = /Android ([\\d.]+)/.exec(ua);\n return { os_name: \"Android\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/Mac OS X|Macintosh/.test(ua) || (platform ?? \"\").startsWith(\"Mac\")) {\n const match = /Mac OS X (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"macOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Linux/.test(ua) || (platform ?? \"\").startsWith(\"Linux\")) {\n return { os_name: \"Linux\" };\n }\n return {};\n}\n\ninterface WindowLike {\n matchMedia?: (query: string) => { matches: boolean };\n innerWidth?: number;\n screen?: { width?: number };\n}\n\nexport function detectDeviceType(win: WindowLike): string {\n try {\n const coarse =\n typeof win.matchMedia === \"function\" && win.matchMedia(\"(pointer: coarse)\").matches;\n if (!coarse) return \"desktop\";\n const width = win.innerWidth || win.screen?.width || 0;\n return width >= 768 ? \"tablet\" : \"mobile\";\n } catch {\n return \"desktop\";\n }\n}\n\nexport function collectWebContext(): EventContext {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return { platform: \"web\" };\n }\n const ua = navigator.userAgent ?? \"\";\n const context: EventContext = { platform: \"web\" };\n const browser = browserFromUserAgent(ua);\n if (browser !== undefined) context.browser = browser;\n Object.assign(context, osFromUserAgent(ua, navigator.platform));\n context.device_type = detectDeviceType(window);\n if (navigator.language) context.locale = navigator.language;\n if (window.screen) {\n context.screen = { w: window.screen.width ?? 0, h: window.screen.height ?? 0 };\n }\n return context;\n}\n","import type { AnalyticsClient, Properties } from \"@getdevteam/analytics-core\";\n\nexport function currentPageProperties(): Properties {\n return {\n url: window.location.href,\n title: document.title,\n };\n}\n\nexport function installPageviewTracking(client: AnalyticsClient): () => void {\n const sendPageview = (): void => client.page(currentPageProperties());\n const history = window.history;\n const originalPushState = history.pushState.bind(history);\n const originalReplaceState = history.replaceState.bind(history);\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n originalPushState(...args);\n sendPageview();\n };\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n originalReplaceState(...args);\n sendPageview();\n };\n window.addEventListener(\"popstate\", sendPageview);\n sendPageview();\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n window.removeEventListener(\"popstate\", sendPageview);\n };\n}\n","import { createMemoryStorage, type StorageAdapter } from \"@getdevteam/analytics-core\";\n\nexport function resolveLocalStorage(): Storage | undefined {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return undefined;\n const probe = \"__devteam_analytics_probe__\";\n window.localStorage.setItem(probe, \"1\");\n window.localStorage.removeItem(probe);\n return window.localStorage;\n } catch {\n return undefined;\n }\n}\n\nexport function createBrowserStorage(\n resolve: () => Storage | undefined = resolveLocalStorage,\n): StorageAdapter {\n const localStorage = resolve();\n if (!localStorage) return createMemoryStorage();\n return {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // quota or private-mode failure; queue persistence degrades gracefully\n }\n },\n removeItem: (key) => {\n try {\n localStorage.removeItem(key);\n } catch {\n // ignore\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/context.ts","../src/pageviews.ts","../src/storage.ts"],"sourcesContent":["import {\n createClient,\n utf8ByteLength,\n type AnalyticsClient,\n type ClientConfig,\n type EventContext,\n type FetchLike,\n type FetchRequestInit,\n type FetchResponseLike,\n} from \"@getdevteam/analytics-core\";\nimport { collectWebContext } from \"./context\";\nimport { installPageviewTracking } from \"./pageviews\";\nimport { createBrowserStorage } from \"./storage\";\nimport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\n\nexport const WEB_SDK_NAME = \"@getdevteam/analytics-web\";\nexport const WEB_SDK_VERSION = \"0.3.1\";\n\n// The browser turns a `keepalive` request whose body exceeds 64 KiB into a network error, so only a body that\n// fits may take that path.\nconst KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;\n\nconst textEncoder = typeof TextEncoder !== \"undefined\" ? new TextEncoder() : undefined;\n\nfunction byteLength(value: string): number {\n return textEncoder ? textEncoder.encode(value).length : utf8ByteLength(value);\n}\n\nexport interface WebAnalyticsConfig extends ClientConfig {\n autoCaptureErrors?: boolean;\n trackPageviews?: boolean;\n context?: Partial<EventContext>;\n allowUserFeedback?: boolean;\n feedback?: WebFeedbackOptions;\n}\n\nexport interface WebAnalyticsClient extends AnalyticsClient {\n /** Opens the user-feedback overlay. A no-op when `allowUserFeedback` is off or the UI is unavailable. */\n showFeedback(): void;\n}\n\nexport function createAnalytics(config: WebAnalyticsConfig): WebAnalyticsClient {\n let finalFlush = false;\n const baseFetch = config.fetch ?? defaultBrowserFetch();\n\n const reportError = (error: unknown): void => {\n try {\n config.onError?.(error);\n } catch {\n // a throwing observer must never break the host app\n }\n };\n\n const client = createClient({\n ...config,\n storage: config.storage ?? createBrowserStorage(),\n fetch: (url, init) => {\n if (finalFlush) return sendOnHide(url, init, baseFetch);\n if (baseFetch) return baseFetch(url, init);\n return Promise.reject(new Error(\"fetch is not available in this environment\"));\n },\n });\n\n const webContext: EventContext = {\n sdk_name: WEB_SDK_NAME,\n sdk_version: WEB_SDK_VERSION,\n ...collectWebContext(),\n ...config.context,\n };\n client.setContext(webContext);\n\n // Feedback reads the context at submit time, so it must track later\n // setContext calls. The core client keeps its merged copy private, so the\n // wrapper mirrors the merge (an undefined value deletes the key) here.\n let currentContext = webContext;\n const setContext = (partial: Partial<EventContext>): void => {\n client.setContext(partial);\n try {\n const next: EventContext = { ...currentContext };\n for (const [key, value] of Object.entries(partial)) {\n if (value === undefined) {\n delete next[key as keyof EventContext];\n } else {\n (next as Record<string, unknown>)[key] = value;\n }\n }\n currentContext = next;\n } catch {\n // mirrors the core client, whose setContext never throws into the host app\n }\n };\n\n const teardowns: Array<() => void> = [];\n\n if (typeof window !== \"undefined\") {\n const flushOnHide = (): void => {\n finalFlush = true;\n // sendOnHide delivers over `keepalive`, so constrain each batch under the browser's 64 KiB keepalive body\n // limit. The first sub-request always fits the quota; the rest are best-effort (they drain fully when the\n // tab is hidden but alive; on a real teardown the remainder stays persisted for the next visit).\n void client.flush({ keepaliveConstrained: true }).finally(() => {\n finalFlush = false;\n });\n };\n const onPageHide = (): void => flushOnHide();\n const onVisibilityChange = (): void => {\n if (document.visibilityState === \"hidden\") flushOnHide();\n };\n window.addEventListener(\"pagehide\", onPageHide);\n document.addEventListener(\"visibilitychange\", onVisibilityChange);\n teardowns.push(() => {\n window.removeEventListener(\"pagehide\", onPageHide);\n document.removeEventListener(\"visibilitychange\", onVisibilityChange);\n });\n\n if (config.autoCaptureErrors !== false) {\n const onError = (event: ErrorEvent): void => {\n client.captureError(event.error ?? event.message, { source: \"window.onerror\" });\n };\n const onRejection = (event: Event): void => {\n const reason = (event as { reason?: unknown }).reason;\n client.captureError(reason ?? \"unhandled promise rejection\", {\n source: \"unhandledrejection\",\n });\n };\n window.addEventListener(\"error\", onError);\n window.addEventListener(\"unhandledrejection\", onRejection);\n teardowns.push(() => {\n window.removeEventListener(\"error\", onError);\n window.removeEventListener(\"unhandledrejection\", onRejection);\n });\n }\n\n if (config.trackPageviews === true) {\n teardowns.push(installPageviewTracking(client));\n }\n }\n\n let feedback: FeedbackHandle | undefined;\n let feedbackDisposed = false;\n\n if (\n config.allowUserFeedback === true &&\n typeof window !== \"undefined\" &&\n typeof document !== \"undefined\" &&\n typeof config.host === \"string\"\n ) {\n const feedbackUrl = `${config.host.replace(/\\/+$/, \"\")}/v1/ingest/feedback`;\n // The feedback UI (and its html-to-image dependency) loads only when the feature is on;\n // a failed load reports through onError and leaves the feature off.\n void import(\"./feedback\")\n .then((module) => {\n if (feedbackDisposed) return;\n feedback = module.mountFeedback({\n url: feedbackUrl,\n key: config.key,\n fetch: baseFetch,\n getDistinctId: () => client.getDistinctId(),\n getSessionId: () => client.getSessionId(),\n getContext: () => currentContext,\n options: config.feedback ?? {},\n onError: reportError,\n });\n teardowns.push(() => {\n feedback?.destroy();\n feedback = undefined;\n });\n })\n .catch(reportError);\n }\n\n return {\n ...client,\n setContext,\n showFeedback: () => {\n try {\n feedback?.open();\n } catch (error) {\n reportError(error);\n }\n },\n shutdown: async () => {\n feedbackDisposed = true;\n for (const teardown of teardowns.splice(0)) {\n teardown();\n }\n await client.shutdown();\n },\n };\n}\n\nfunction defaultBrowserFetch(): FetchLike | undefined {\n if (typeof fetch !== \"function\") return undefined;\n return (url, init) => fetch(url, init);\n}\n\nfunction sendOnHide(\n url: string,\n init: FetchRequestInit,\n baseFetch: FetchLike | undefined,\n): Promise<FetchResponseLike> {\n if (baseFetch) {\n // Only the page-hide flush sizes its batches to the keepalive budget; a flush already in progress when the\n // tab hid still carries full-size ones. Those go over an ordinary fetch, because `keepalive` would reject\n // every attempt and burn the retry budget on a transport that can never accept them.\n if (byteLength(init.body) > KEEPALIVE_MAX_BODY_BYTES) {\n return baseFetch(url, init);\n }\n return baseFetch(url, { ...init, keepalive: true });\n }\n // No sendBeacon fallback. It cannot set headers, and the ingest API only ever reads the key from the\n // X-DevTeam-Key header - there is no query-parameter form of it anywhere in the middleware or the edge config -\n // so a beacon can only be answered 401. Worse, sendBeacon returns true when the request is merely QUEUED, so\n // reporting that as delivered made the client drop the batch it had already taken off the queue. Rejecting\n // instead routes it through the retry path, which requeues it and leaves it in storage for the next visit.\n return Promise.reject(new Error(\"no transport available for final flush\"));\n}\n\nexport { collectWebContext, browserFromUserAgent, osFromUserAgent, detectDeviceType } from \"./context\";\nexport { createBrowserStorage } from \"./storage\";\nexport { currentPageProperties, installPageviewTracking } from \"./pageviews\";\nexport type { FeedbackHandle, WebFeedbackOptions } from \"./feedback/types\";\nexport type {\n AnalyticsClient,\n AnalyticsLogger,\n ClientConfig,\n EventContext,\n FetchLike,\n LogSeverity,\n Properties,\n StorageAdapter,\n} from \"@getdevteam/analytics-core\";\n","import type { EventContext } from \"@getdevteam/analytics-core\";\n\nexport function browserFromUserAgent(ua: string): string | undefined {\n if (/Edg(e|A|iOS)?\\//.test(ua)) return \"Edge\";\n if (/Firefox\\/|FxiOS\\//.test(ua)) return \"Firefox\";\n if (/CriOS\\/|Chrome\\//.test(ua)) return \"Chrome\";\n if (/Safari\\//.test(ua)) return \"Safari\";\n return undefined;\n}\n\nexport function osFromUserAgent(\n ua: string,\n platform?: string,\n): { os_name?: string; os_version?: string } {\n if (/Windows/.test(ua)) {\n const match = /Windows NT ([\\d.]+)/.exec(ua);\n return { os_name: \"Windows\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/iPhone|iPad|iPod/.test(ua)) {\n const match = /OS (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"iOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Android/.test(ua)) {\n const match = /Android ([\\d.]+)/.exec(ua);\n return { os_name: \"Android\", ...(match?.[1] ? { os_version: match[1] } : {}) };\n }\n if (/Mac OS X|Macintosh/.test(ua) || (platform ?? \"\").startsWith(\"Mac\")) {\n const match = /Mac OS X (\\d+[_.]\\d+(?:[_.]\\d+)?)/.exec(ua);\n return { os_name: \"macOS\", ...(match?.[1] ? { os_version: match[1].replace(/_/g, \".\") } : {}) };\n }\n if (/Linux/.test(ua) || (platform ?? \"\").startsWith(\"Linux\")) {\n return { os_name: \"Linux\" };\n }\n return {};\n}\n\ninterface WindowLike {\n matchMedia?: (query: string) => { matches: boolean };\n innerWidth?: number;\n screen?: { width?: number };\n}\n\nexport function detectDeviceType(win: WindowLike): string {\n try {\n const coarse =\n typeof win.matchMedia === \"function\" && win.matchMedia(\"(pointer: coarse)\").matches;\n if (!coarse) return \"desktop\";\n const width = win.innerWidth || win.screen?.width || 0;\n return width >= 768 ? \"tablet\" : \"mobile\";\n } catch {\n return \"desktop\";\n }\n}\n\nexport function collectWebContext(): EventContext {\n if (typeof window === \"undefined\" || typeof navigator === \"undefined\") {\n return { platform: \"web\" };\n }\n const ua = navigator.userAgent ?? \"\";\n const context: EventContext = { platform: \"web\" };\n const browser = browserFromUserAgent(ua);\n if (browser !== undefined) context.browser = browser;\n Object.assign(context, osFromUserAgent(ua, navigator.platform));\n context.device_type = detectDeviceType(window);\n if (navigator.language) context.locale = navigator.language;\n if (window.screen) {\n context.screen = { w: window.screen.width ?? 0, h: window.screen.height ?? 0 };\n }\n return context;\n}\n","import type { AnalyticsClient, Properties } from \"@getdevteam/analytics-core\";\n\nexport function currentPageProperties(): Properties {\n return {\n url: window.location.href,\n title: document.title,\n };\n}\n\nexport function installPageviewTracking(client: AnalyticsClient): () => void {\n const sendPageview = (): void => client.page(currentPageProperties());\n const history = window.history;\n const originalPushState = history.pushState.bind(history);\n const originalReplaceState = history.replaceState.bind(history);\n\n history.pushState = (...args: Parameters<History[\"pushState\"]>) => {\n originalPushState(...args);\n sendPageview();\n };\n history.replaceState = (...args: Parameters<History[\"replaceState\"]>) => {\n originalReplaceState(...args);\n sendPageview();\n };\n window.addEventListener(\"popstate\", sendPageview);\n sendPageview();\n\n return () => {\n history.pushState = originalPushState;\n history.replaceState = originalReplaceState;\n window.removeEventListener(\"popstate\", sendPageview);\n };\n}\n","import { createMemoryStorage, type StorageAdapter } from \"@getdevteam/analytics-core\";\n\nexport function resolveLocalStorage(): Storage | undefined {\n try {\n if (typeof window === \"undefined\" || !window.localStorage) return undefined;\n const probe = \"__devteam_analytics_probe__\";\n window.localStorage.setItem(probe, \"1\");\n window.localStorage.removeItem(probe);\n return window.localStorage;\n } catch {\n return undefined;\n }\n}\n\nexport function createBrowserStorage(\n resolve: () => Storage | undefined = resolveLocalStorage,\n): StorageAdapter {\n const localStorage = resolve();\n if (!localStorage) return createMemoryStorage();\n return {\n getItem: (key) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n },\n setItem: (key, value) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // quota or private-mode failure; queue persistence degrades gracefully\n }\n },\n removeItem: (key) => {\n try {\n localStorage.removeItem(key);\n } catch {\n // ignore\n }\n },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getdevteam/analytics-web",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Browser SDK for the DevTeam analytics and logging platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@getdevteam/analytics-core": "0.
|
|
38
|
+
"@getdevteam/analytics-core": "0.3.1",
|
|
39
39
|
"html-to-image": "1.11.13"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|