@getdevteam/analytics-web 0.2.0 → 0.3.0
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 +9 -0
- package/dist/feedback-W5PGVRNZ.js +651 -0
- package/dist/feedback-W5PGVRNZ.js.map +1 -0
- package/dist/index.cjs +750 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +26 -3
- package/dist/index.d.ts +26 -3
- package/dist/index.js +69 -14
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/index.cjs.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 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\";\n\nexport const WEB_SDK_NAME = \"@getdevteam/analytics-web\";\nexport const WEB_SDK_VERSION = \"0.2.0\";\n\nexport interface WebAnalyticsConfig extends ClientConfig {\n autoCaptureErrors?: boolean;\n trackPageviews?: boolean;\n context?: Partial<EventContext>;\n}\n\nexport function createAnalytics(config: WebAnalyticsConfig): AnalyticsClient {\n let finalFlush = false;\n const baseFetch = config.fetch ?? defaultBrowserFetch();\n\n const client = createClient({\n ...config,\n storage: config.storage ?? createBrowserStorage(),\n fetch: (url, init) => {\n if (finalFlush) return sendOnHide(url, init, config.key, 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 client.setContext({\n sdk_name: WEB_SDK_NAME,\n sdk_version: WEB_SDK_VERSION,\n ...collectWebContext(),\n ...config.context,\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 return {\n ...client,\n shutdown: async () => {\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 key: string,\n baseFetch: FetchLike | undefined,\n): Promise<FetchResponseLike> {\n if (baseFetch) {\n return baseFetch(url, { ...init, keepalive: true });\n }\n if (typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\") {\n // sendBeacon cannot set headers, so the key rides as a query parameter as a last resort.\n const beaconUrl = `${url}${url.includes(\"?\") ? \"&\" : \"?\"}key=${encodeURIComponent(key)}`;\n const delivered = navigator.sendBeacon(\n beaconUrl,\n new Blob([init.body], { type: \"application/json\" }),\n );\n return Promise.resolve({ ok: delivered, status: delivered ? 202 : 503 });\n }\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 {\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":["import_analytics_core"]}
|
|
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"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { EventContext, StorageAdapter, Properties, AnalyticsClient, ClientConfig } from '@getdevteam/analytics-core';
|
|
2
2
|
export { AnalyticsClient, AnalyticsLogger, ClientConfig, EventContext, FetchLike, LogSeverity, Properties, StorageAdapter } from '@getdevteam/analytics-core';
|
|
3
3
|
|
|
4
|
+
interface WebFeedbackOptions {
|
|
5
|
+
colors?: string[];
|
|
6
|
+
accentColor?: string;
|
|
7
|
+
buttonLabel?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
submitLabel?: string;
|
|
10
|
+
commentPlaceholder?: string;
|
|
11
|
+
successMessage?: string;
|
|
12
|
+
position?: "bottom-right" | "bottom-left";
|
|
13
|
+
zIndex?: number;
|
|
14
|
+
pixelRatio?: number;
|
|
15
|
+
}
|
|
16
|
+
interface FeedbackHandle {
|
|
17
|
+
open(): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
declare function browserFromUserAgent(ua: string): string | undefined;
|
|
5
22
|
declare function osFromUserAgent(ua: string, platform?: string): {
|
|
6
23
|
os_name?: string;
|
|
@@ -24,12 +41,18 @@ declare function currentPageProperties(): Properties;
|
|
|
24
41
|
declare function installPageviewTracking(client: AnalyticsClient): () => void;
|
|
25
42
|
|
|
26
43
|
declare const WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
27
|
-
declare const WEB_SDK_VERSION = "0.
|
|
44
|
+
declare const WEB_SDK_VERSION = "0.3.0";
|
|
28
45
|
interface WebAnalyticsConfig extends ClientConfig {
|
|
29
46
|
autoCaptureErrors?: boolean;
|
|
30
47
|
trackPageviews?: boolean;
|
|
31
48
|
context?: Partial<EventContext>;
|
|
49
|
+
allowUserFeedback?: boolean;
|
|
50
|
+
feedback?: WebFeedbackOptions;
|
|
51
|
+
}
|
|
52
|
+
interface WebAnalyticsClient extends AnalyticsClient {
|
|
53
|
+
/** Opens the user-feedback overlay. A no-op when `allowUserFeedback` is off or the UI is unavailable. */
|
|
54
|
+
showFeedback(): void;
|
|
32
55
|
}
|
|
33
|
-
declare function createAnalytics(config: WebAnalyticsConfig):
|
|
56
|
+
declare function createAnalytics(config: WebAnalyticsConfig): WebAnalyticsClient;
|
|
34
57
|
|
|
35
|
-
export { WEB_SDK_NAME, WEB_SDK_VERSION, type WebAnalyticsConfig, browserFromUserAgent, collectWebContext, createAnalytics, createBrowserStorage, currentPageProperties, detectDeviceType, installPageviewTracking, osFromUserAgent };
|
|
58
|
+
export { type FeedbackHandle, WEB_SDK_NAME, WEB_SDK_VERSION, type WebAnalyticsClient, type WebAnalyticsConfig, type WebFeedbackOptions, browserFromUserAgent, collectWebContext, createAnalytics, createBrowserStorage, currentPageProperties, detectDeviceType, installPageviewTracking, osFromUserAgent };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
import { EventContext, StorageAdapter, Properties, AnalyticsClient, ClientConfig } from '@getdevteam/analytics-core';
|
|
2
2
|
export { AnalyticsClient, AnalyticsLogger, ClientConfig, EventContext, FetchLike, LogSeverity, Properties, StorageAdapter } from '@getdevteam/analytics-core';
|
|
3
3
|
|
|
4
|
+
interface WebFeedbackOptions {
|
|
5
|
+
colors?: string[];
|
|
6
|
+
accentColor?: string;
|
|
7
|
+
buttonLabel?: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
submitLabel?: string;
|
|
10
|
+
commentPlaceholder?: string;
|
|
11
|
+
successMessage?: string;
|
|
12
|
+
position?: "bottom-right" | "bottom-left";
|
|
13
|
+
zIndex?: number;
|
|
14
|
+
pixelRatio?: number;
|
|
15
|
+
}
|
|
16
|
+
interface FeedbackHandle {
|
|
17
|
+
open(): void;
|
|
18
|
+
destroy(): void;
|
|
19
|
+
}
|
|
20
|
+
|
|
4
21
|
declare function browserFromUserAgent(ua: string): string | undefined;
|
|
5
22
|
declare function osFromUserAgent(ua: string, platform?: string): {
|
|
6
23
|
os_name?: string;
|
|
@@ -24,12 +41,18 @@ declare function currentPageProperties(): Properties;
|
|
|
24
41
|
declare function installPageviewTracking(client: AnalyticsClient): () => void;
|
|
25
42
|
|
|
26
43
|
declare const WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
27
|
-
declare const WEB_SDK_VERSION = "0.
|
|
44
|
+
declare const WEB_SDK_VERSION = "0.3.0";
|
|
28
45
|
interface WebAnalyticsConfig extends ClientConfig {
|
|
29
46
|
autoCaptureErrors?: boolean;
|
|
30
47
|
trackPageviews?: boolean;
|
|
31
48
|
context?: Partial<EventContext>;
|
|
49
|
+
allowUserFeedback?: boolean;
|
|
50
|
+
feedback?: WebFeedbackOptions;
|
|
51
|
+
}
|
|
52
|
+
interface WebAnalyticsClient extends AnalyticsClient {
|
|
53
|
+
/** Opens the user-feedback overlay. A no-op when `allowUserFeedback` is off or the UI is unavailable. */
|
|
54
|
+
showFeedback(): void;
|
|
32
55
|
}
|
|
33
|
-
declare function createAnalytics(config: WebAnalyticsConfig):
|
|
56
|
+
declare function createAnalytics(config: WebAnalyticsConfig): WebAnalyticsClient;
|
|
34
57
|
|
|
35
|
-
export { WEB_SDK_NAME, WEB_SDK_VERSION, type WebAnalyticsConfig, browserFromUserAgent, collectWebContext, createAnalytics, createBrowserStorage, currentPageProperties, detectDeviceType, installPageviewTracking, osFromUserAgent };
|
|
58
|
+
export { type FeedbackHandle, WEB_SDK_NAME, WEB_SDK_VERSION, type WebAnalyticsClient, type WebAnalyticsConfig, type WebFeedbackOptions, browserFromUserAgent, collectWebContext, createAnalytics, createBrowserStorage, currentPageProperties, detectDeviceType, installPageviewTracking, osFromUserAgent };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import {
|
|
3
|
-
createClient
|
|
3
|
+
createClient,
|
|
4
|
+
utf8ByteLength
|
|
4
5
|
} from "@getdevteam/analytics-core";
|
|
5
6
|
|
|
6
7
|
// src/context.ts
|
|
@@ -130,25 +131,53 @@ function createBrowserStorage(resolve = resolveLocalStorage) {
|
|
|
130
131
|
|
|
131
132
|
// src/index.ts
|
|
132
133
|
var WEB_SDK_NAME = "@getdevteam/analytics-web";
|
|
133
|
-
var WEB_SDK_VERSION = "0.
|
|
134
|
+
var WEB_SDK_VERSION = "0.3.0";
|
|
135
|
+
var KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;
|
|
136
|
+
var textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
|
|
137
|
+
function byteLength(value) {
|
|
138
|
+
return textEncoder ? textEncoder.encode(value).length : utf8ByteLength(value);
|
|
139
|
+
}
|
|
134
140
|
function createAnalytics(config) {
|
|
135
141
|
let finalFlush = false;
|
|
136
142
|
const baseFetch = config.fetch ?? defaultBrowserFetch();
|
|
143
|
+
const reportError = (error) => {
|
|
144
|
+
try {
|
|
145
|
+
config.onError?.(error);
|
|
146
|
+
} catch {
|
|
147
|
+
}
|
|
148
|
+
};
|
|
137
149
|
const client = createClient({
|
|
138
150
|
...config,
|
|
139
151
|
storage: config.storage ?? createBrowserStorage(),
|
|
140
152
|
fetch: (url, init) => {
|
|
141
|
-
if (finalFlush) return sendOnHide(url, init,
|
|
153
|
+
if (finalFlush) return sendOnHide(url, init, baseFetch);
|
|
142
154
|
if (baseFetch) return baseFetch(url, init);
|
|
143
155
|
return Promise.reject(new Error("fetch is not available in this environment"));
|
|
144
156
|
}
|
|
145
157
|
});
|
|
146
|
-
|
|
158
|
+
const webContext = {
|
|
147
159
|
sdk_name: WEB_SDK_NAME,
|
|
148
160
|
sdk_version: WEB_SDK_VERSION,
|
|
149
161
|
...collectWebContext(),
|
|
150
162
|
...config.context
|
|
151
|
-
}
|
|
163
|
+
};
|
|
164
|
+
client.setContext(webContext);
|
|
165
|
+
let currentContext = webContext;
|
|
166
|
+
const setContext = (partial) => {
|
|
167
|
+
client.setContext(partial);
|
|
168
|
+
try {
|
|
169
|
+
const next = { ...currentContext };
|
|
170
|
+
for (const [key, value] of Object.entries(partial)) {
|
|
171
|
+
if (value === void 0) {
|
|
172
|
+
delete next[key];
|
|
173
|
+
} else {
|
|
174
|
+
next[key] = value;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
currentContext = next;
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
180
|
+
};
|
|
152
181
|
const teardowns = [];
|
|
153
182
|
if (typeof window !== "undefined") {
|
|
154
183
|
const flushOnHide = () => {
|
|
@@ -188,9 +217,40 @@ function createAnalytics(config) {
|
|
|
188
217
|
teardowns.push(installPageviewTracking(client));
|
|
189
218
|
}
|
|
190
219
|
}
|
|
220
|
+
let feedback;
|
|
221
|
+
let feedbackDisposed = false;
|
|
222
|
+
if (config.allowUserFeedback === true && typeof window !== "undefined" && typeof document !== "undefined" && typeof config.host === "string") {
|
|
223
|
+
const feedbackUrl = `${config.host.replace(/\/+$/, "")}/v1/ingest/feedback`;
|
|
224
|
+
void import("./feedback-W5PGVRNZ.js").then((module) => {
|
|
225
|
+
if (feedbackDisposed) return;
|
|
226
|
+
feedback = module.mountFeedback({
|
|
227
|
+
url: feedbackUrl,
|
|
228
|
+
key: config.key,
|
|
229
|
+
fetch: baseFetch,
|
|
230
|
+
getDistinctId: () => client.getDistinctId(),
|
|
231
|
+
getSessionId: () => client.getSessionId(),
|
|
232
|
+
getContext: () => currentContext,
|
|
233
|
+
options: config.feedback ?? {},
|
|
234
|
+
onError: reportError
|
|
235
|
+
});
|
|
236
|
+
teardowns.push(() => {
|
|
237
|
+
feedback?.destroy();
|
|
238
|
+
feedback = void 0;
|
|
239
|
+
});
|
|
240
|
+
}).catch(reportError);
|
|
241
|
+
}
|
|
191
242
|
return {
|
|
192
243
|
...client,
|
|
244
|
+
setContext,
|
|
245
|
+
showFeedback: () => {
|
|
246
|
+
try {
|
|
247
|
+
feedback?.open();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
reportError(error);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
193
252
|
shutdown: async () => {
|
|
253
|
+
feedbackDisposed = true;
|
|
194
254
|
for (const teardown of teardowns.splice(0)) {
|
|
195
255
|
teardown();
|
|
196
256
|
}
|
|
@@ -202,18 +262,13 @@ function defaultBrowserFetch() {
|
|
|
202
262
|
if (typeof fetch !== "function") return void 0;
|
|
203
263
|
return (url, init) => fetch(url, init);
|
|
204
264
|
}
|
|
205
|
-
function sendOnHide(url, init,
|
|
265
|
+
function sendOnHide(url, init, baseFetch) {
|
|
206
266
|
if (baseFetch) {
|
|
267
|
+
if (byteLength(init.body) > KEEPALIVE_MAX_BODY_BYTES) {
|
|
268
|
+
return baseFetch(url, init);
|
|
269
|
+
}
|
|
207
270
|
return baseFetch(url, { ...init, keepalive: true });
|
|
208
271
|
}
|
|
209
|
-
if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
|
|
210
|
-
const beaconUrl = `${url}${url.includes("?") ? "&" : "?"}key=${encodeURIComponent(key)}`;
|
|
211
|
-
const delivered = navigator.sendBeacon(
|
|
212
|
-
beaconUrl,
|
|
213
|
-
new Blob([init.body], { type: "application/json" })
|
|
214
|
-
);
|
|
215
|
-
return Promise.resolve({ ok: delivered, status: delivered ? 202 : 503 });
|
|
216
|
-
}
|
|
217
272
|
return Promise.reject(new Error("no transport available for final flush"));
|
|
218
273
|
}
|
|
219
274
|
export {
|
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 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\";\n\nexport const WEB_SDK_NAME = \"@getdevteam/analytics-web\";\nexport const WEB_SDK_VERSION = \"0.2.0\";\n\nexport interface WebAnalyticsConfig extends ClientConfig {\n autoCaptureErrors?: boolean;\n trackPageviews?: boolean;\n context?: Partial<EventContext>;\n}\n\nexport function createAnalytics(config: WebAnalyticsConfig): AnalyticsClient {\n let finalFlush = false;\n const baseFetch = config.fetch ?? defaultBrowserFetch();\n\n const client = createClient({\n ...config,\n storage: config.storage ?? createBrowserStorage(),\n fetch: (url, init) => {\n if (finalFlush) return sendOnHide(url, init, config.key, 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 client.setContext({\n sdk_name: WEB_SDK_NAME,\n sdk_version: WEB_SDK_VERSION,\n ...collectWebContext(),\n ...config.context,\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 return {\n ...client,\n shutdown: async () => {\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 key: string,\n baseFetch: FetchLike | undefined,\n): Promise<FetchResponseLike> {\n if (baseFetch) {\n return baseFetch(url, { ...init, keepalive: true });\n }\n if (typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\") {\n // sendBeacon cannot set headers, so the key rides as a query parameter as a last resort.\n const beaconUrl = `${url}${url.includes(\"?\") ? \"&\" : \"?\"}key=${encodeURIComponent(key)}`;\n const delivered = navigator.sendBeacon(\n beaconUrl,\n new Blob([init.body], { type: \"application/json\" }),\n );\n return Promise.resolve({ ok: delivered, status: delivered ? 202 : 503 });\n }\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 {\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.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":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,OAOK;;;ACPA,SAAS,qBAAqB,IAAgC;AACnE,MAAI,kBAAkB,KAAK,EAAE,EAAG,QAAO;AACvC,MAAI,oBAAoB,KAAK,EAAE,EAAG,QAAO;AACzC,MAAI,mBAAmB,KAAK,EAAE,EAAG,QAAO;AACxC,MAAI,WAAW,KAAK,EAAE,EAAG,QAAO;AAChC,SAAO;AACT;AAEO,SAAS,gBACd,IACA,UAC2C;AAC3C,MAAI,UAAU,KAAK,EAAE,GAAG;AACtB,UAAM,QAAQ,sBAAsB,KAAK,EAAE;AAC3C,WAAO,EAAE,SAAS,WAAW,GAAI,QAAQ,CAAC,IAAI,EAAE,YAAY,MAAM,CAAC,EAAE,IAAI,CAAC,EAAG;AAAA,EAC/E;AACA,MAAI,mBAAmB,KAAK,EAAE,GAAG;AAC/B,UAAM,QAAQ,8BAA8B,KAAK,EAAE;AACnD,WAAO,EAAE,SAAS,OAAO,GAAI,QAAQ,CAAC,IAAI,EAAE,YAAY,MAAM,CAAC,EAAE,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,EAAG;AAAA,EAC9F;AACA,MAAI,UAAU,KAAK,EAAE,GAAG;AACtB,UAAM,QAAQ,mBAAmB,KAAK,EAAE;AACxC,WAAO,EAAE,SAAS,WAAW,GAAI,QAAQ,CAAC,IAAI,EAAE,YAAY,MAAM,CAAC,EAAE,IAAI,CAAC,EAAG;AAAA,EAC/E;AACA,MAAI,qBAAqB,KAAK,EAAE,MAAM,YAAY,IAAI,WAAW,KAAK,GAAG;AACvE,UAAM,QAAQ,oCAAoC,KAAK,EAAE;AACzD,WAAO,EAAE,SAAS,SAAS,GAAI,QAAQ,CAAC,IAAI,EAAE,YAAY,MAAM,CAAC,EAAE,QAAQ,MAAM,GAAG,EAAE,IAAI,CAAC,EAAG;AAAA,EAChG;AACA,MAAI,QAAQ,KAAK,EAAE,MAAM,YAAY,IAAI,WAAW,OAAO,GAAG;AAC5D,WAAO,EAAE,SAAS,QAAQ;AAAA,EAC5B;AACA,SAAO,CAAC;AACV;AAQO,SAAS,iBAAiB,KAAyB;AACxD,MAAI;AACF,UAAM,SACJ,OAAO,IAAI,eAAe,cAAc,IAAI,WAAW,mBAAmB,EAAE;AAC9E,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,QAAQ,IAAI,cAAc,IAAI,QAAQ,SAAS;AACrD,WAAO,SAAS,MAAM,WAAW;AAAA,EACnC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,oBAAkC;AAChD,MAAI,OAAO,WAAW,eAAe,OAAO,cAAc,aAAa;AACrE,WAAO,EAAE,UAAU,MAAM;AAAA,EAC3B;AACA,QAAM,KAAK,UAAU,aAAa;AAClC,QAAM,UAAwB,EAAE,UAAU,MAAM;AAChD,QAAM,UAAU,qBAAqB,EAAE;AACvC,MAAI,YAAY,OAAW,SAAQ,UAAU;AAC7C,SAAO,OAAO,SAAS,gBAAgB,IAAI,UAAU,QAAQ,CAAC;AAC9D,UAAQ,cAAc,iBAAiB,MAAM;AAC7C,MAAI,UAAU,SAAU,SAAQ,SAAS,UAAU;AACnD,MAAI,OAAO,QAAQ;AACjB,YAAQ,SAAS,EAAE,GAAG,OAAO,OAAO,SAAS,GAAG,GAAG,OAAO,OAAO,UAAU,EAAE;AAAA,EAC/E;AACA,SAAO;AACT;;;ACnEO,SAAS,wBAAoC;AAClD,SAAO;AAAA,IACL,KAAK,OAAO,SAAS;AAAA,IACrB,OAAO,SAAS;AAAA,EAClB;AACF;AAEO,SAAS,wBAAwB,QAAqC;AAC3E,QAAM,eAAe,MAAY,OAAO,KAAK,sBAAsB,CAAC;AACpE,QAAM,UAAU,OAAO;AACvB,QAAM,oBAAoB,QAAQ,UAAU,KAAK,OAAO;AACxD,QAAM,uBAAuB,QAAQ,aAAa,KAAK,OAAO;AAE9D,UAAQ,YAAY,IAAI,SAA2C;AACjE,sBAAkB,GAAG,IAAI;AACzB,iBAAa;AAAA,EACf;AACA,UAAQ,eAAe,IAAI,SAA8C;AACvE,yBAAqB,GAAG,IAAI;AAC5B,iBAAa;AAAA,EACf;AACA,SAAO,iBAAiB,YAAY,YAAY;AAChD,eAAa;AAEb,SAAO,MAAM;AACX,YAAQ,YAAY;AACpB,YAAQ,eAAe;AACvB,WAAO,oBAAoB,YAAY,YAAY;AAAA,EACrD;AACF;;;AC/BA,SAAS,2BAAgD;AAElD,SAAS,sBAA2C;AACzD,MAAI;AACF,QAAI,OAAO,WAAW,eAAe,CAAC,OAAO,aAAc,QAAO;AAClE,UAAM,QAAQ;AACd,WAAO,aAAa,QAAQ,OAAO,GAAG;AACtC,WAAO,aAAa,WAAW,KAAK;AACpC,WAAO,OAAO;AAAA,EAChB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,qBACd,UAAqC,qBACrB;AAChB,QAAM,eAAe,QAAQ;AAC7B,MAAI,CAAC,aAAc,QAAO,oBAAoB;AAC9C,SAAO;AAAA,IACL,SAAS,CAAC,QAAQ;AAChB,UAAI;AACF,eAAO,aAAa,QAAQ,GAAG;AAAA,MACjC,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF;AAAA,IACA,SAAS,CAAC,KAAK,UAAU;AACvB,UAAI;AACF,qBAAa,QAAQ,KAAK,KAAK;AAAA,MACjC,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,IACA,YAAY,CAAC,QAAQ;AACnB,UAAI;AACF,qBAAa,WAAW,GAAG;AAAA,MAC7B,QAAQ;AAAA,MAER;AAAA,IACF;AAAA,EACF;AACF;;;AH3BO,IAAM,eAAe;AACrB,IAAM,kBAAkB;AAI/B,IAAM,2BAA2B,KAAK;AAEtC,IAAM,cAAc,OAAO,gBAAgB,cAAc,IAAI,YAAY,IAAI;AAE7E,SAAS,WAAW,OAAuB;AACzC,SAAO,cAAc,YAAY,OAAO,KAAK,EAAE,SAAS,eAAe,KAAK;AAC9E;AAeO,SAAS,gBAAgB,QAAgD;AAC9E,MAAI,aAAa;AACjB,QAAM,YAAY,OAAO,SAAS,oBAAoB;AAEtD,QAAM,cAAc,CAAC,UAAyB;AAC5C,QAAI;AACF,aAAO,UAAU,KAAK;AAAA,IACxB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,SAAS,aAAa;AAAA,IAC1B,GAAG;AAAA,IACH,SAAS,OAAO,WAAW,qBAAqB;AAAA,IAChD,OAAO,CAAC,KAAK,SAAS;AACpB,UAAI,WAAY,QAAO,WAAW,KAAK,MAAM,SAAS;AACtD,UAAI,UAAW,QAAO,UAAU,KAAK,IAAI;AACzC,aAAO,QAAQ,OAAO,IAAI,MAAM,4CAA4C,CAAC;AAAA,IAC/E;AAAA,EACF,CAAC;AAED,QAAM,aAA2B;AAAA,IAC/B,UAAU;AAAA,IACV,aAAa;AAAA,IACb,GAAG,kBAAkB;AAAA,IACrB,GAAG,OAAO;AAAA,EACZ;AACA,SAAO,WAAW,UAAU;AAK5B,MAAI,iBAAiB;AACrB,QAAM,aAAa,CAAC,YAAyC;AAC3D,WAAO,WAAW,OAAO;AACzB,QAAI;AACF,YAAM,OAAqB,EAAE,GAAG,eAAe;AAC/C,iBAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,YAAI,UAAU,QAAW;AACvB,iBAAO,KAAK,GAAyB;AAAA,QACvC,OAAO;AACL,UAAC,KAAiC,GAAG,IAAI;AAAA,QAC3C;AAAA,MACF;AACA,uBAAiB;AAAA,IACnB,QAAQ;AAAA,IAER;AAAA,EACF;AAEA,QAAM,YAA+B,CAAC;AAEtC,MAAI,OAAO,WAAW,aAAa;AACjC,UAAM,cAAc,MAAY;AAC9B,mBAAa;AAIb,WAAK,OAAO,MAAM,EAAE,sBAAsB,KAAK,CAAC,EAAE,QAAQ,MAAM;AAC9D,qBAAa;AAAA,MACf,CAAC;AAAA,IACH;AACA,UAAM,aAAa,MAAY,YAAY;AAC3C,UAAM,qBAAqB,MAAY;AACrC,UAAI,SAAS,oBAAoB,SAAU,aAAY;AAAA,IACzD;AACA,WAAO,iBAAiB,YAAY,UAAU;AAC9C,aAAS,iBAAiB,oBAAoB,kBAAkB;AAChE,cAAU,KAAK,MAAM;AACnB,aAAO,oBAAoB,YAAY,UAAU;AACjD,eAAS,oBAAoB,oBAAoB,kBAAkB;AAAA,IACrE,CAAC;AAED,QAAI,OAAO,sBAAsB,OAAO;AACtC,YAAM,UAAU,CAAC,UAA4B;AAC3C,eAAO,aAAa,MAAM,SAAS,MAAM,SAAS,EAAE,QAAQ,iBAAiB,CAAC;AAAA,MAChF;AACA,YAAM,cAAc,CAAC,UAAuB;AAC1C,cAAM,SAAU,MAA+B;AAC/C,eAAO,aAAa,UAAU,+BAA+B;AAAA,UAC3D,QAAQ;AAAA,QACV,CAAC;AAAA,MACH;AACA,aAAO,iBAAiB,SAAS,OAAO;AACxC,aAAO,iBAAiB,sBAAsB,WAAW;AACzD,gBAAU,KAAK,MAAM;AACnB,eAAO,oBAAoB,SAAS,OAAO;AAC3C,eAAO,oBAAoB,sBAAsB,WAAW;AAAA,MAC9D,CAAC;AAAA,IACH;AAEA,QAAI,OAAO,mBAAmB,MAAM;AAClC,gBAAU,KAAK,wBAAwB,MAAM,CAAC;AAAA,IAChD;AAAA,EACF;AAEA,MAAI;AACJ,MAAI,mBAAmB;AAEvB,MACE,OAAO,sBAAsB,QAC7B,OAAO,WAAW,eAClB,OAAO,aAAa,eACpB,OAAO,OAAO,SAAS,UACvB;AACA,UAAM,cAAc,GAAG,OAAO,KAAK,QAAQ,QAAQ,EAAE,CAAC;AAGtD,SAAK,OAAO,wBAAY,EACrB,KAAK,CAAC,WAAW;AAChB,UAAI,iBAAkB;AACtB,iBAAW,OAAO,cAAc;AAAA,QAC9B,KAAK;AAAA,QACL,KAAK,OAAO;AAAA,QACZ,OAAO;AAAA,QACP,eAAe,MAAM,OAAO,cAAc;AAAA,QAC1C,cAAc,MAAM,OAAO,aAAa;AAAA,QACxC,YAAY,MAAM;AAAA,QAClB,SAAS,OAAO,YAAY,CAAC;AAAA,QAC7B,SAAS;AAAA,MACX,CAAC;AACD,gBAAU,KAAK,MAAM;AACnB,kBAAU,QAAQ;AAClB,mBAAW;AAAA,MACb,CAAC;AAAA,IACH,CAAC,EACA,MAAM,WAAW;AAAA,EACtB;AAEA,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,cAAc,MAAM;AAClB,UAAI;AACF,kBAAU,KAAK;AAAA,MACjB,SAAS,OAAO;AACd,oBAAY,KAAK;AAAA,MACnB;AAAA,IACF;AAAA,IACA,UAAU,YAAY;AACpB,yBAAmB;AACnB,iBAAW,YAAY,UAAU,OAAO,CAAC,GAAG;AAC1C,iBAAS;AAAA,MACX;AACA,YAAM,OAAO,SAAS;AAAA,IACxB;AAAA,EACF;AACF;AAEA,SAAS,sBAA6C;AACpD,MAAI,OAAO,UAAU,WAAY,QAAO;AACxC,SAAO,CAAC,KAAK,SAAS,MAAM,KAAK,IAAI;AACvC;AAEA,SAAS,WACP,KACA,MACA,WAC4B;AAC5B,MAAI,WAAW;AAIb,QAAI,WAAW,KAAK,IAAI,IAAI,0BAA0B;AACpD,aAAO,UAAU,KAAK,IAAI;AAAA,IAC5B;AACA,WAAO,UAAU,KAAK,EAAE,GAAG,MAAM,WAAW,KAAK,CAAC;AAAA,EACpD;AAMA,SAAO,QAAQ,OAAO,IAAI,MAAM,wCAAwC,CAAC;AAC3E;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@getdevteam/analytics-web",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Browser SDK for the DevTeam analytics and logging platform.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,12 +35,16 @@
|
|
|
35
35
|
"typecheck": "tsc --noEmit"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@getdevteam/analytics-core": "0.2.0"
|
|
38
|
+
"@getdevteam/analytics-core": "0.2.0",
|
|
39
|
+
"html-to-image": "1.11.13"
|
|
39
40
|
},
|
|
40
41
|
"devDependencies": {
|
|
42
|
+
"@getdevteam/analytics-contract": "0.2.0",
|
|
43
|
+
"ajv": "^8.17.1",
|
|
44
|
+
"ajv-formats": "^3.0.1",
|
|
41
45
|
"jsdom": "^26.1.0",
|
|
42
46
|
"tsup": "^8.5.0",
|
|
43
47
|
"typescript": "^5.8.3",
|
|
44
|
-
"vitest": "^
|
|
48
|
+
"vitest": "^4.1.11"
|
|
45
49
|
}
|
|
46
50
|
}
|