@getdevteam/analytics-web 0.1.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/dist/index.cjs CHANGED
@@ -3,6 +3,9 @@ var __defProp = Object.defineProperty;
3
3
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
4
  var __getOwnPropNames = Object.getOwnPropertyNames;
5
5
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
6
9
  var __export = (target, all) => {
7
10
  for (var name in all)
8
11
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -17,6 +20,684 @@ var __copyProps = (to, from, except, desc) => {
17
20
  };
18
21
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
22
 
23
+ // src/feedback/capture.ts
24
+ async function captureViewport(exclude, pixelRatio) {
25
+ return (0, import_html_to_image.toCanvas)(document.documentElement, {
26
+ filter: (node) => node !== exclude,
27
+ pixelRatio,
28
+ width: window.innerWidth,
29
+ height: window.innerHeight,
30
+ style: {
31
+ transform: `translate(${-window.scrollX}px, ${-window.scrollY}px)`
32
+ }
33
+ });
34
+ }
35
+ var import_html_to_image;
36
+ var init_capture = __esm({
37
+ "src/feedback/capture.ts"() {
38
+ "use strict";
39
+ import_html_to_image = require("html-to-image");
40
+ }
41
+ });
42
+
43
+ // src/feedback/exportImage.ts
44
+ function drawStrokes(ctx, strokes) {
45
+ for (const stroke of strokes) {
46
+ const [first, ...rest] = stroke.points;
47
+ if (!first) continue;
48
+ ctx.strokeStyle = stroke.color;
49
+ ctx.lineWidth = stroke.width;
50
+ ctx.lineCap = "round";
51
+ ctx.lineJoin = "round";
52
+ ctx.beginPath();
53
+ ctx.moveTo(first.x, first.y);
54
+ if (rest.length === 0) ctx.lineTo(first.x, first.y);
55
+ for (const point of rest) ctx.lineTo(point.x, point.y);
56
+ ctx.stroke();
57
+ }
58
+ }
59
+ async function exportAnnotatedImage(source, strokes, options) {
60
+ try {
61
+ const composed = compose(source, strokes);
62
+ if (!composed) return null;
63
+ const png = await encode(composed, "image/png");
64
+ if (png && png.size <= options.maxBytes) {
65
+ return { base64: await blobToBase64(png), contentType: "image/png" };
66
+ }
67
+ let canvas = composed;
68
+ let jpeg = await encode(canvas, "image/jpeg", JPEG_QUALITY);
69
+ while (jpeg && jpeg.size > options.maxBytes && Math.min(canvas.width, canvas.height) * DOWNSCALE_FACTOR >= MIN_EXPORT_DIMENSION) {
70
+ const smaller = downscale(canvas, DOWNSCALE_FACTOR);
71
+ if (!smaller) break;
72
+ canvas = smaller;
73
+ jpeg = await encode(canvas, "image/jpeg", JPEG_QUALITY);
74
+ }
75
+ if (jpeg && jpeg.size <= options.maxBytes) {
76
+ return { base64: await blobToBase64(jpeg), contentType: "image/jpeg" };
77
+ }
78
+ return null;
79
+ } catch {
80
+ return null;
81
+ }
82
+ }
83
+ function compose(source, strokes) {
84
+ const canvas = document.createElement("canvas");
85
+ canvas.width = source.width;
86
+ canvas.height = source.height;
87
+ const ctx = get2dContext(canvas);
88
+ if (!ctx || canvas.width === 0 || canvas.height === 0) return null;
89
+ ctx.fillStyle = "#ffffff";
90
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
91
+ ctx.drawImage(source, 0, 0);
92
+ drawStrokes(ctx, strokes);
93
+ return canvas;
94
+ }
95
+ function downscale(source, factor) {
96
+ const canvas = document.createElement("canvas");
97
+ canvas.width = Math.max(1, Math.round(source.width * factor));
98
+ canvas.height = Math.max(1, Math.round(source.height * factor));
99
+ const ctx = get2dContext(canvas);
100
+ if (!ctx) return null;
101
+ ctx.drawImage(source, 0, 0, canvas.width, canvas.height);
102
+ return canvas;
103
+ }
104
+ function get2dContext(canvas) {
105
+ try {
106
+ return canvas.getContext("2d");
107
+ } catch {
108
+ return null;
109
+ }
110
+ }
111
+ function encode(canvas, type, quality) {
112
+ return new Promise((resolve) => {
113
+ try {
114
+ canvas.toBlob((blob) => resolve(blob), type, quality);
115
+ } catch {
116
+ resolve(null);
117
+ }
118
+ });
119
+ }
120
+ async function blobToBase64(blob) {
121
+ const bytes = new Uint8Array(await blob.arrayBuffer());
122
+ let binary = "";
123
+ const chunkSize = 32768;
124
+ for (let i = 0; i < bytes.length; i += chunkSize) {
125
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunkSize));
126
+ }
127
+ return btoa(binary);
128
+ }
129
+ var JPEG_QUALITY, DOWNSCALE_FACTOR, MIN_EXPORT_DIMENSION;
130
+ var init_exportImage = __esm({
131
+ "src/feedback/exportImage.ts"() {
132
+ "use strict";
133
+ JPEG_QUALITY = 0.85;
134
+ DOWNSCALE_FACTOR = 0.7;
135
+ MIN_EXPORT_DIMENSION = 200;
136
+ }
137
+ });
138
+
139
+ // src/feedback/strokes.ts
140
+ function createStrokeStore() {
141
+ const strokes = [];
142
+ let active;
143
+ return {
144
+ begin(color, width, point) {
145
+ if (active) return;
146
+ active = { points: [point], color, width };
147
+ strokes.push(active);
148
+ },
149
+ extend(point) {
150
+ active?.points.push(point);
151
+ },
152
+ end() {
153
+ active = void 0;
154
+ },
155
+ undo() {
156
+ if (active) return;
157
+ strokes.pop();
158
+ },
159
+ clear() {
160
+ if (active) return;
161
+ strokes.length = 0;
162
+ },
163
+ list: () => strokes,
164
+ isDrawing: () => active !== void 0
165
+ };
166
+ }
167
+ function mapToImagePoint(clientX, clientY, rect, imageWidth, imageHeight) {
168
+ const scaleX = rect.width > 0 && imageWidth > 0 ? imageWidth / rect.width : 1;
169
+ const scaleY = rect.height > 0 && imageHeight > 0 ? imageHeight / rect.height : 1;
170
+ return { x: (clientX - rect.left) * scaleX, y: (clientY - rect.top) * scaleY };
171
+ }
172
+ var init_strokes = __esm({
173
+ "src/feedback/strokes.ts"() {
174
+ "use strict";
175
+ }
176
+ });
177
+
178
+ // src/feedback/index.ts
179
+ var feedback_exports = {};
180
+ __export(feedback_exports, {
181
+ createStrokeStore: () => createStrokeStore,
182
+ exportAnnotatedImage: () => exportAnnotatedImage,
183
+ mapToImagePoint: () => mapToImagePoint,
184
+ mountFeedback: () => mountFeedback
185
+ });
186
+ function mountFeedback(config, deps = defaultDeps) {
187
+ const options = config.options;
188
+ const colors = options.colors && options.colors.length > 0 ? options.colors : DEFAULT_COLORS;
189
+ const accent = options.accentColor ?? DEFAULT_ACCENT;
190
+ const pixelRatio = options.pixelRatio ?? Math.min(typeof devicePixelRatio === "number" && devicePixelRatio > 0 ? devicePixelRatio : 1, 2);
191
+ let destroyed = false;
192
+ let overlayOpen = false;
193
+ let overlaySession = 0;
194
+ let capturing = false;
195
+ let sending = false;
196
+ let draftFeedbackId = (0, import_analytics_core2.uuidV7)();
197
+ let sourceCanvas;
198
+ let activeColor = colors[0] ?? DEFAULT_COLORS[0];
199
+ let penWidth = PEN_WIDTH;
200
+ let previousBodyOverflow;
201
+ let closeTimer;
202
+ let drawContext;
203
+ const strokes = createStrokeStore();
204
+ const report = (error2) => {
205
+ try {
206
+ config.onError(error2);
207
+ } catch {
208
+ }
209
+ };
210
+ const safely = (fn) => {
211
+ try {
212
+ fn();
213
+ } catch (error2) {
214
+ report(error2);
215
+ }
216
+ };
217
+ const host = document.createElement("div");
218
+ host.setAttribute("data-devteam-feedback", "");
219
+ const shadow = host.attachShadow({ mode: "open" });
220
+ const style = document.createElement("style");
221
+ style.textContent = buildStyles(options, accent);
222
+ const container = element("div", "container");
223
+ const fab = element("button", "fab");
224
+ fab.type = "button";
225
+ fab.textContent = options.buttonLabel ?? "Feedback";
226
+ const overlay = element("div", "overlay");
227
+ overlay.hidden = true;
228
+ const backdrop = element("div", "backdrop");
229
+ const panel = element("div", "panel");
230
+ panel.setAttribute("role", "dialog");
231
+ panel.setAttribute("aria-modal", "true");
232
+ const header = element("div", "header");
233
+ const title = element("div", "title");
234
+ title.textContent = options.title ?? "Send feedback";
235
+ const closeButton = element("button", "close");
236
+ closeButton.type = "button";
237
+ closeButton.setAttribute("aria-label", "Close");
238
+ closeButton.textContent = "\xD7";
239
+ header.append(title, closeButton);
240
+ const preview = element("div", "preview");
241
+ const drawCanvas = document.createElement("canvas");
242
+ drawCanvas.className = "draw";
243
+ preview.appendChild(drawCanvas);
244
+ const tools = element("div", "tools");
245
+ const swatches = colors.map((color) => {
246
+ const swatch = element("button", "swatch");
247
+ swatch.type = "button";
248
+ swatch.style.background = color;
249
+ swatch.setAttribute("aria-label", `Pen color ${color}`);
250
+ swatch.addEventListener("click", () => safely(() => selectColor(color)));
251
+ tools.appendChild(swatch);
252
+ return { color, swatch };
253
+ });
254
+ const undoButton = element("button", "tool undo");
255
+ undoButton.type = "button";
256
+ undoButton.textContent = "Undo";
257
+ const clearButton = element("button", "tool clear");
258
+ clearButton.type = "button";
259
+ clearButton.textContent = "Clear";
260
+ tools.append(undoButton, clearButton);
261
+ const comment = document.createElement("textarea");
262
+ comment.className = "comment";
263
+ comment.placeholder = options.commentPlaceholder ?? "What went wrong, or what could be better?";
264
+ comment.maxLength = MAX_TEXT_LENGTH;
265
+ const error = element("div", "error");
266
+ error.hidden = true;
267
+ const success = element("div", "success");
268
+ success.hidden = true;
269
+ success.textContent = options.successMessage ?? "Thanks for the feedback!";
270
+ const actions = element("div", "actions");
271
+ const cancelButton = element("button", "cancel");
272
+ cancelButton.type = "button";
273
+ cancelButton.textContent = "Cancel";
274
+ const submitButton = element("button", "submit");
275
+ submitButton.type = "button";
276
+ submitButton.textContent = options.submitLabel ?? "Send";
277
+ actions.append(cancelButton, submitButton);
278
+ panel.append(header, preview, tools, comment, error, success, actions);
279
+ overlay.append(backdrop, panel);
280
+ container.append(fab, overlay);
281
+ shadow.append(style, container);
282
+ if (document.body) {
283
+ document.body.appendChild(host);
284
+ } else {
285
+ document.addEventListener(
286
+ "DOMContentLoaded",
287
+ () => {
288
+ if (!destroyed) document.body?.appendChild(host);
289
+ },
290
+ { once: true }
291
+ );
292
+ }
293
+ function selectColor(color) {
294
+ activeColor = color;
295
+ for (const entry of swatches) {
296
+ entry.swatch.classList.toggle("active", entry.color === color);
297
+ }
298
+ }
299
+ selectColor(activeColor);
300
+ function context2d() {
301
+ if (drawContext === void 0) {
302
+ try {
303
+ drawContext = drawCanvas.getContext("2d");
304
+ } catch {
305
+ drawContext = null;
306
+ }
307
+ }
308
+ return drawContext;
309
+ }
310
+ function repaint() {
311
+ const ctx = context2d();
312
+ if (!ctx) return;
313
+ ctx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
314
+ drawStrokes(ctx, strokes.list());
315
+ }
316
+ function pointFromEvent(event) {
317
+ const { clientX, clientY } = event;
318
+ if (typeof clientX !== "number" || typeof clientY !== "number") return void 0;
319
+ const rect = drawCanvas.getBoundingClientRect();
320
+ return mapToImagePoint(clientX, clientY, rect, drawCanvas.width, drawCanvas.height);
321
+ }
322
+ function withPointerCapture(event, action) {
323
+ const pointerId = event.pointerId;
324
+ if (typeof pointerId !== "number") return;
325
+ const method = action === "set" ? drawCanvas.setPointerCapture : drawCanvas.releasePointerCapture;
326
+ if (typeof method !== "function") return;
327
+ try {
328
+ method.call(drawCanvas, pointerId);
329
+ } catch {
330
+ }
331
+ }
332
+ const onPointerDown = (event) => safely(() => {
333
+ if (!sourceCanvas || sending) return;
334
+ const point = pointFromEvent(event);
335
+ if (!point) return;
336
+ if (typeof event.preventDefault === "function") event.preventDefault();
337
+ withPointerCapture(event, "set");
338
+ strokes.begin(activeColor, penWidth, point);
339
+ repaint();
340
+ });
341
+ const onPointerMove = (event) => safely(() => {
342
+ if (!strokes.isDrawing()) return;
343
+ const point = pointFromEvent(event);
344
+ if (!point) return;
345
+ strokes.extend(point);
346
+ repaint();
347
+ });
348
+ const onPointerEnd = (event) => safely(() => {
349
+ withPointerCapture(event, "release");
350
+ strokes.end();
351
+ });
352
+ drawCanvas.addEventListener("pointerdown", onPointerDown);
353
+ drawCanvas.addEventListener("pointermove", onPointerMove);
354
+ drawCanvas.addEventListener("pointerup", onPointerEnd);
355
+ drawCanvas.addEventListener("pointercancel", onPointerEnd);
356
+ drawCanvas.addEventListener("pointerleave", onPointerEnd);
357
+ shadow.addEventListener("pointerup", onPointerEnd);
358
+ undoButton.addEventListener(
359
+ "click",
360
+ () => safely(() => {
361
+ strokes.undo();
362
+ repaint();
363
+ })
364
+ );
365
+ clearButton.addEventListener(
366
+ "click",
367
+ () => safely(() => {
368
+ strokes.clear();
369
+ repaint();
370
+ })
371
+ );
372
+ const onKeyDown = (event) => {
373
+ if (event.key === "Escape") safely(close);
374
+ };
375
+ function lockScroll() {
376
+ if (previousBodyOverflow !== void 0) return;
377
+ previousBodyOverflow = document.body.style.overflow;
378
+ document.body.style.overflow = "hidden";
379
+ }
380
+ function unlockScroll() {
381
+ if (previousBodyOverflow === void 0) return;
382
+ document.body.style.overflow = previousBodyOverflow;
383
+ previousBodyOverflow = void 0;
384
+ }
385
+ function setError(message) {
386
+ error.hidden = message === void 0;
387
+ error.textContent = message ?? "";
388
+ }
389
+ function setFormVisible(visible) {
390
+ const hasPreview = sourceCanvas !== void 0;
391
+ preview.hidden = !visible || !hasPreview;
392
+ tools.hidden = !visible || !hasPreview;
393
+ comment.hidden = !visible;
394
+ actions.hidden = !visible;
395
+ }
396
+ function showOverlay(canvas) {
397
+ overlaySession += 1;
398
+ draftFeedbackId = (0, import_analytics_core2.uuidV7)();
399
+ sourceCanvas = canvas;
400
+ strokes.end();
401
+ strokes.clear();
402
+ comment.value = "";
403
+ setError(void 0);
404
+ success.hidden = true;
405
+ submitButton.disabled = false;
406
+ if (canvas) {
407
+ canvas.classList.add("shot");
408
+ preview.insertBefore(canvas, drawCanvas);
409
+ drawCanvas.width = canvas.width;
410
+ drawCanvas.height = canvas.height;
411
+ penWidth = PEN_WIDTH * (pixelRatio > 0 ? pixelRatio : 1);
412
+ repaint();
413
+ }
414
+ selectColor(colors[0] ?? DEFAULT_COLORS[0]);
415
+ setFormVisible(true);
416
+ overlay.hidden = false;
417
+ overlayOpen = true;
418
+ lockScroll();
419
+ document.addEventListener("keydown", onKeyDown);
420
+ }
421
+ function close() {
422
+ if (!overlayOpen) return;
423
+ overlaySession += 1;
424
+ overlayOpen = false;
425
+ overlay.hidden = true;
426
+ document.removeEventListener("keydown", onKeyDown);
427
+ unlockScroll();
428
+ if (closeTimer !== void 0) {
429
+ clearTimeout(closeTimer);
430
+ closeTimer = void 0;
431
+ }
432
+ sending = false;
433
+ if (sourceCanvas) {
434
+ sourceCanvas.remove();
435
+ sourceCanvas = void 0;
436
+ }
437
+ strokes.end();
438
+ strokes.clear();
439
+ }
440
+ function open() {
441
+ if (destroyed || overlayOpen || capturing) return;
442
+ capturing = true;
443
+ void Promise.resolve().then(() => deps.capture(host, pixelRatio)).then(
444
+ (canvas) => {
445
+ capturing = false;
446
+ if (destroyed) return;
447
+ showOverlay(canvas);
448
+ },
449
+ (captureError) => {
450
+ capturing = false;
451
+ if (destroyed) return;
452
+ report(captureError);
453
+ showOverlay(void 0);
454
+ }
455
+ );
456
+ }
457
+ function buildBody(text, screenshot) {
458
+ const body = {
459
+ feedback_id: draftFeedbackId,
460
+ text: text.slice(0, MAX_TEXT_LENGTH)
461
+ };
462
+ if (screenshot) {
463
+ body.screenshot = screenshot.base64;
464
+ body.screenshot_content_type = screenshot.contentType;
465
+ }
466
+ const distinctId = config.getDistinctId();
467
+ if (distinctId) body.distinct_id = distinctId;
468
+ const sessionId = config.getSessionId();
469
+ if (sessionId) body.session_id = sessionId;
470
+ if (typeof location !== "undefined" && location.href) {
471
+ body.page_url = location.href.slice(0, MAX_PAGE_URL_LENGTH);
472
+ }
473
+ body.context = config.getContext();
474
+ return JSON.stringify(body);
475
+ }
476
+ async function submit() {
477
+ if (sending || !overlayOpen) return;
478
+ const text = comment.value.trim();
479
+ if (text.length === 0) {
480
+ setError("Please add a short comment before sending.");
481
+ return;
482
+ }
483
+ const session = overlaySession;
484
+ const sameSession = () => !destroyed && overlayOpen && overlaySession === session;
485
+ sending = true;
486
+ submitButton.disabled = true;
487
+ setError(void 0);
488
+ try {
489
+ let screenshot = null;
490
+ if (sourceCanvas) {
491
+ screenshot = await deps.exportAnnotated(sourceCanvas, strokes.list(), {
492
+ maxBytes: MAX_SCREENSHOT_BYTES
493
+ });
494
+ }
495
+ if (!config.fetch) throw new Error("fetch is not available in this environment");
496
+ const response = await config.fetch(config.url, {
497
+ method: "POST",
498
+ headers: { "Content-Type": "application/json", "X-DevTeam-Key": config.key },
499
+ body: buildBody(text, screenshot)
500
+ });
501
+ if (!sameSession()) return;
502
+ if (response.ok) {
503
+ setFormVisible(false);
504
+ success.hidden = false;
505
+ closeTimer = setTimeout(() => safely(close), SUCCESS_CLOSE_DELAY_MS);
506
+ } else {
507
+ setError("Could not send feedback. Please try again.");
508
+ }
509
+ } catch (submitError) {
510
+ report(submitError);
511
+ if (sameSession()) setError("Could not send feedback. Please try again.");
512
+ } finally {
513
+ if (overlaySession === session) {
514
+ sending = false;
515
+ submitButton.disabled = false;
516
+ }
517
+ }
518
+ }
519
+ fab.addEventListener("click", () => safely(open));
520
+ closeButton.addEventListener("click", () => safely(close));
521
+ cancelButton.addEventListener("click", () => safely(close));
522
+ backdrop.addEventListener("click", () => safely(close));
523
+ submitButton.addEventListener("click", () => safely(() => void submit()));
524
+ return {
525
+ open: () => safely(open),
526
+ destroy: () => {
527
+ if (destroyed) return;
528
+ destroyed = true;
529
+ safely(() => {
530
+ close();
531
+ document.removeEventListener("keydown", onKeyDown);
532
+ host.remove();
533
+ });
534
+ }
535
+ };
536
+ }
537
+ function element(tag, className) {
538
+ const node = document.createElement(tag);
539
+ node.className = className;
540
+ return node;
541
+ }
542
+ function buildStyles(options, accent) {
543
+ const zIndex = options.zIndex ?? DEFAULT_Z_INDEX;
544
+ const side = options.position === "bottom-left" ? "left" : "right";
545
+ return `
546
+ :host { all: initial; }
547
+ * { box-sizing: border-box; }
548
+ [hidden] { display: none !important; }
549
+ .container {
550
+ position: fixed;
551
+ inset: 0;
552
+ z-index: ${zIndex};
553
+ pointer-events: none;
554
+ font-family: system-ui, -apple-system, sans-serif;
555
+ color: #1f2933;
556
+ }
557
+ .fab {
558
+ position: absolute;
559
+ bottom: 20px;
560
+ ${side}: 20px;
561
+ pointer-events: auto;
562
+ border: none;
563
+ border-radius: 999px;
564
+ padding: 10px 18px;
565
+ background: ${accent};
566
+ color: #fff;
567
+ font-size: 14px;
568
+ font-weight: 600;
569
+ cursor: pointer;
570
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
571
+ }
572
+ .overlay {
573
+ position: absolute;
574
+ inset: 0;
575
+ pointer-events: auto;
576
+ display: flex;
577
+ align-items: center;
578
+ justify-content: center;
579
+ }
580
+ .backdrop {
581
+ position: absolute;
582
+ inset: 0;
583
+ background: rgba(15, 23, 42, 0.55);
584
+ }
585
+ .panel {
586
+ position: relative;
587
+ display: flex;
588
+ flex-direction: column;
589
+ gap: 10px;
590
+ width: min(560px, calc(100vw - 32px));
591
+ max-height: calc(100vh - 32px);
592
+ overflow: auto;
593
+ background: #fff;
594
+ border-radius: 12px;
595
+ padding: 16px;
596
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.35);
597
+ }
598
+ .header { display: flex; align-items: center; justify-content: space-between; }
599
+ .title { font-size: 16px; font-weight: 600; }
600
+ .close {
601
+ border: none;
602
+ background: none;
603
+ font-size: 20px;
604
+ line-height: 1;
605
+ cursor: pointer;
606
+ color: #52606d;
607
+ padding: 4px 8px;
608
+ }
609
+ .preview {
610
+ position: relative;
611
+ border: 1px solid #d3dce6;
612
+ border-radius: 8px;
613
+ overflow: hidden;
614
+ background: #f5f7fa;
615
+ }
616
+ .preview .shot { display: block; width: 100%; height: auto; }
617
+ .preview .draw {
618
+ position: absolute;
619
+ inset: 0;
620
+ width: 100%;
621
+ height: 100%;
622
+ cursor: crosshair;
623
+ touch-action: none;
624
+ }
625
+ .tools { display: flex; align-items: center; gap: 8px; }
626
+ .swatch {
627
+ width: 24px;
628
+ height: 24px;
629
+ border-radius: 50%;
630
+ border: 2px solid transparent;
631
+ cursor: pointer;
632
+ padding: 0;
633
+ }
634
+ .swatch.active { border-color: #1f2933; }
635
+ .tool {
636
+ border: 1px solid #d3dce6;
637
+ background: #fff;
638
+ border-radius: 6px;
639
+ padding: 4px 10px;
640
+ font-size: 13px;
641
+ cursor: pointer;
642
+ }
643
+ .comment {
644
+ width: 100%;
645
+ min-height: 72px;
646
+ resize: vertical;
647
+ border: 1px solid #d3dce6;
648
+ border-radius: 8px;
649
+ padding: 8px 10px;
650
+ font: inherit;
651
+ }
652
+ .error { color: #b91c1c; font-size: 13px; }
653
+ .success { color: #15803d; font-size: 14px; font-weight: 600; }
654
+ .actions { display: flex; justify-content: flex-end; gap: 8px; }
655
+ .cancel {
656
+ border: 1px solid #d3dce6;
657
+ background: #fff;
658
+ border-radius: 8px;
659
+ padding: 8px 14px;
660
+ font-size: 14px;
661
+ cursor: pointer;
662
+ }
663
+ .submit {
664
+ border: none;
665
+ background: ${accent};
666
+ color: #fff;
667
+ border-radius: 8px;
668
+ padding: 8px 16px;
669
+ font-size: 14px;
670
+ font-weight: 600;
671
+ cursor: pointer;
672
+ }
673
+ .submit:disabled { opacity: 0.6; cursor: default; }
674
+ `;
675
+ }
676
+ var import_analytics_core2, DEFAULT_COLORS, DEFAULT_ACCENT, DEFAULT_Z_INDEX, PEN_WIDTH, MAX_SCREENSHOT_BYTES, MAX_TEXT_LENGTH, MAX_PAGE_URL_LENGTH, SUCCESS_CLOSE_DELAY_MS, defaultDeps;
677
+ var init_feedback = __esm({
678
+ "src/feedback/index.ts"() {
679
+ "use strict";
680
+ import_analytics_core2 = require("@getdevteam/analytics-core");
681
+ init_capture();
682
+ init_exportImage();
683
+ init_strokes();
684
+ init_strokes();
685
+ init_exportImage();
686
+ DEFAULT_COLORS = ["#f44336", "#4caf50", "#2196f3", "#ffeb3b"];
687
+ DEFAULT_ACCENT = "#5b9cf5";
688
+ DEFAULT_Z_INDEX = 2147483e3;
689
+ PEN_WIDTH = 5;
690
+ MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024;
691
+ MAX_TEXT_LENGTH = 1e4;
692
+ MAX_PAGE_URL_LENGTH = 2048;
693
+ SUCCESS_CLOSE_DELAY_MS = 1600;
694
+ defaultDeps = {
695
+ capture: captureViewport,
696
+ exportAnnotated: exportAnnotatedImage
697
+ };
698
+ }
699
+ });
700
+
20
701
  // src/index.ts
21
702
  var index_exports = {};
22
703
  __export(index_exports, {
@@ -32,7 +713,7 @@ __export(index_exports, {
32
713
  osFromUserAgent: () => osFromUserAgent
33
714
  });
34
715
  module.exports = __toCommonJS(index_exports);
35
- var import_analytics_core2 = require("@getdevteam/analytics-core");
716
+ var import_analytics_core3 = require("@getdevteam/analytics-core");
36
717
 
37
718
  // src/context.ts
38
719
  function browserFromUserAgent(ua) {
@@ -161,30 +842,58 @@ function createBrowserStorage(resolve = resolveLocalStorage) {
161
842
 
162
843
  // src/index.ts
163
844
  var WEB_SDK_NAME = "@getdevteam/analytics-web";
164
- var WEB_SDK_VERSION = "0.1.0";
845
+ var WEB_SDK_VERSION = "0.3.0";
846
+ var KEEPALIVE_MAX_BODY_BYTES = 64 * 1024;
847
+ var textEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : void 0;
848
+ function byteLength(value) {
849
+ return textEncoder ? textEncoder.encode(value).length : (0, import_analytics_core3.utf8ByteLength)(value);
850
+ }
165
851
  function createAnalytics(config) {
166
852
  let finalFlush = false;
167
853
  const baseFetch = config.fetch ?? defaultBrowserFetch();
168
- const client = (0, import_analytics_core2.createClient)({
854
+ const reportError = (error) => {
855
+ try {
856
+ config.onError?.(error);
857
+ } catch {
858
+ }
859
+ };
860
+ const client = (0, import_analytics_core3.createClient)({
169
861
  ...config,
170
862
  storage: config.storage ?? createBrowserStorage(),
171
863
  fetch: (url, init) => {
172
- if (finalFlush) return sendOnHide(url, init, config.key, baseFetch);
864
+ if (finalFlush) return sendOnHide(url, init, baseFetch);
173
865
  if (baseFetch) return baseFetch(url, init);
174
866
  return Promise.reject(new Error("fetch is not available in this environment"));
175
867
  }
176
868
  });
177
- client.setContext({
869
+ const webContext = {
178
870
  sdk_name: WEB_SDK_NAME,
179
871
  sdk_version: WEB_SDK_VERSION,
180
872
  ...collectWebContext(),
181
873
  ...config.context
182
- });
874
+ };
875
+ client.setContext(webContext);
876
+ let currentContext = webContext;
877
+ const setContext = (partial) => {
878
+ client.setContext(partial);
879
+ try {
880
+ const next = { ...currentContext };
881
+ for (const [key, value] of Object.entries(partial)) {
882
+ if (value === void 0) {
883
+ delete next[key];
884
+ } else {
885
+ next[key] = value;
886
+ }
887
+ }
888
+ currentContext = next;
889
+ } catch {
890
+ }
891
+ };
183
892
  const teardowns = [];
184
893
  if (typeof window !== "undefined") {
185
894
  const flushOnHide = () => {
186
895
  finalFlush = true;
187
- void client.flush().finally(() => {
896
+ void client.flush({ keepaliveConstrained: true }).finally(() => {
188
897
  finalFlush = false;
189
898
  });
190
899
  };
@@ -219,9 +928,40 @@ function createAnalytics(config) {
219
928
  teardowns.push(installPageviewTracking(client));
220
929
  }
221
930
  }
931
+ let feedback;
932
+ let feedbackDisposed = false;
933
+ if (config.allowUserFeedback === true && typeof window !== "undefined" && typeof document !== "undefined" && typeof config.host === "string") {
934
+ const feedbackUrl = `${config.host.replace(/\/+$/, "")}/v1/ingest/feedback`;
935
+ void Promise.resolve().then(() => (init_feedback(), feedback_exports)).then((module2) => {
936
+ if (feedbackDisposed) return;
937
+ feedback = module2.mountFeedback({
938
+ url: feedbackUrl,
939
+ key: config.key,
940
+ fetch: baseFetch,
941
+ getDistinctId: () => client.getDistinctId(),
942
+ getSessionId: () => client.getSessionId(),
943
+ getContext: () => currentContext,
944
+ options: config.feedback ?? {},
945
+ onError: reportError
946
+ });
947
+ teardowns.push(() => {
948
+ feedback?.destroy();
949
+ feedback = void 0;
950
+ });
951
+ }).catch(reportError);
952
+ }
222
953
  return {
223
954
  ...client,
955
+ setContext,
956
+ showFeedback: () => {
957
+ try {
958
+ feedback?.open();
959
+ } catch (error) {
960
+ reportError(error);
961
+ }
962
+ },
224
963
  shutdown: async () => {
964
+ feedbackDisposed = true;
225
965
  for (const teardown of teardowns.splice(0)) {
226
966
  teardown();
227
967
  }
@@ -233,18 +973,13 @@ function defaultBrowserFetch() {
233
973
  if (typeof fetch !== "function") return void 0;
234
974
  return (url, init) => fetch(url, init);
235
975
  }
236
- function sendOnHide(url, init, key, baseFetch) {
976
+ function sendOnHide(url, init, baseFetch) {
237
977
  if (baseFetch) {
978
+ if (byteLength(init.body) > KEEPALIVE_MAX_BODY_BYTES) {
979
+ return baseFetch(url, init);
980
+ }
238
981
  return baseFetch(url, { ...init, keepalive: true });
239
982
  }
240
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
241
- const beaconUrl = `${url}${url.includes("?") ? "&" : "?"}key=${encodeURIComponent(key)}`;
242
- const delivered = navigator.sendBeacon(
243
- beaconUrl,
244
- new Blob([init.body], { type: "application/json" })
245
- );
246
- return Promise.resolve({ ok: delivered, status: delivered ? 202 : 503 });
247
- }
248
983
  return Promise.reject(new Error("no transport available for final flush"));
249
984
  }
250
985
  // Annotate the CommonJS export names for ESM import in node: