@repros/sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,937 @@
1
+ // src/claim.ts
2
+ var DEFAULT_API_BASE = "https://www.repros.dev";
3
+ var QUERY_PARAM = "repros_claim";
4
+ function readClaimToken() {
5
+ if (typeof window === "undefined") return null;
6
+ const url = new URL(window.location.href);
7
+ const token = url.searchParams.get(QUERY_PARAM);
8
+ if (!token) return null;
9
+ url.searchParams.delete(QUERY_PARAM);
10
+ window.history.replaceState(window.history.state, "", url.toString());
11
+ return token;
12
+ }
13
+ async function claim(token, apiBase = DEFAULT_API_BASE) {
14
+ try {
15
+ const res = await fetch(`${apiBase.replace(/\/+$/, "")}/api/customer-sessions/claim`, {
16
+ method: "POST",
17
+ headers: { "Content-Type": "application/json" },
18
+ body: JSON.stringify({ token })
19
+ });
20
+ const data = await res.json().catch(() => null);
21
+ if (!res.ok || !data?.ok || !data.sessionId) {
22
+ console.warn("[Repros SDK] couldn't claim report link:", data?.reason ?? res.status);
23
+ return null;
24
+ }
25
+ return { sessionId: data.sessionId, token, apiBase };
26
+ } catch (err) {
27
+ console.warn("[Repros SDK] couldn't reach Repros to claim report link:", err);
28
+ return null;
29
+ }
30
+ }
31
+
32
+ // ../annotation-toolkit/src/select.ts
33
+ function startElementSelect(onPick, onCancel) {
34
+ const highlight = document.createElement("div");
35
+ highlight.style.cssText = "position:fixed;pointer-events:none;z-index:2147483645;border:2px solid #7c3aed;background:rgba(124,58,237,0.15);border-radius:2px;display:none;box-sizing:border-box;";
36
+ document.body.appendChild(highlight);
37
+ let lastEl = null;
38
+ function updateHighlight(el) {
39
+ if (el === lastEl) return;
40
+ lastEl = el;
41
+ const r = el.getBoundingClientRect();
42
+ Object.assign(highlight.style, {
43
+ display: "block",
44
+ left: `${r.left}px`,
45
+ top: `${r.top}px`,
46
+ width: `${r.width}px`,
47
+ height: `${r.height}px`
48
+ });
49
+ }
50
+ function onMouseMove(e) {
51
+ const el = document.elementFromPoint(e.clientX, e.clientY);
52
+ if (el && el !== highlight) updateHighlight(el);
53
+ }
54
+ function onClick(e) {
55
+ e.preventDefault();
56
+ e.stopPropagation();
57
+ const el = lastEl;
58
+ cleanup();
59
+ if (!el) {
60
+ onCancel();
61
+ return;
62
+ }
63
+ const r = el.getBoundingClientRect();
64
+ onPick({ rect: { x: r.left + window.scrollX, y: r.top + window.scrollY, width: r.width, height: r.height } });
65
+ }
66
+ function onKeyDown(e) {
67
+ if (e.key === "Escape") {
68
+ cleanup();
69
+ onCancel();
70
+ }
71
+ }
72
+ function cleanup() {
73
+ window.removeEventListener("mousemove", onMouseMove, true);
74
+ window.removeEventListener("click", onClick, true);
75
+ window.removeEventListener("keydown", onKeyDown, true);
76
+ highlight.remove();
77
+ }
78
+ window.addEventListener("mousemove", onMouseMove, true);
79
+ window.addEventListener("click", onClick, true);
80
+ window.addEventListener("keydown", onKeyDown, true);
81
+ return cleanup;
82
+ }
83
+
84
+ // ../annotation-toolkit/src/draw.ts
85
+ var MIN_DRAG_PX = 4;
86
+ function startFreeDraw(onPick, onCancel) {
87
+ const overlay = document.createElement("div");
88
+ overlay.style.cssText = "position:fixed;inset:0;z-index:2147483645;cursor:crosshair;background:rgba(0,0,0,0.01);";
89
+ const box = document.createElement("div");
90
+ box.style.cssText = "position:fixed;pointer-events:none;z-index:2147483645;border:2px solid #7c3aed;background:rgba(124,58,237,0.15);display:none;box-sizing:border-box;";
91
+ document.body.appendChild(overlay);
92
+ document.body.appendChild(box);
93
+ let dragging = false;
94
+ let startX = 0;
95
+ let startY = 0;
96
+ function viewportRect(curX, curY) {
97
+ return {
98
+ x: Math.min(startX, curX),
99
+ y: Math.min(startY, curY),
100
+ width: Math.abs(curX - startX),
101
+ height: Math.abs(curY - startY)
102
+ };
103
+ }
104
+ function onDown(e) {
105
+ dragging = true;
106
+ startX = e.clientX;
107
+ startY = e.clientY;
108
+ box.style.display = "block";
109
+ paint(viewportRect(e.clientX, e.clientY));
110
+ }
111
+ function paint(r) {
112
+ Object.assign(box.style, { left: `${r.x}px`, top: `${r.y}px`, width: `${r.width}px`, height: `${r.height}px` });
113
+ }
114
+ function onMove(e) {
115
+ if (dragging) paint(viewportRect(e.clientX, e.clientY));
116
+ }
117
+ function onUp(e) {
118
+ if (!dragging) return;
119
+ dragging = false;
120
+ const r = viewportRect(e.clientX, e.clientY);
121
+ cleanup();
122
+ if (r.width < MIN_DRAG_PX || r.height < MIN_DRAG_PX) {
123
+ onCancel();
124
+ return;
125
+ }
126
+ onPick({ rect: { x: r.x + window.scrollX, y: r.y + window.scrollY, width: r.width, height: r.height } });
127
+ }
128
+ function onKeyDown(e) {
129
+ if (e.key === "Escape") {
130
+ cleanup();
131
+ onCancel();
132
+ }
133
+ }
134
+ function cleanup() {
135
+ overlay.removeEventListener("mousedown", onDown);
136
+ window.removeEventListener("mousemove", onMove);
137
+ window.removeEventListener("mouseup", onUp);
138
+ window.removeEventListener("keydown", onKeyDown, true);
139
+ overlay.remove();
140
+ box.remove();
141
+ }
142
+ overlay.addEventListener("mousedown", onDown);
143
+ window.addEventListener("mousemove", onMove);
144
+ window.addEventListener("mouseup", onUp);
145
+ window.addEventListener("keydown", onKeyDown, true);
146
+ return cleanup;
147
+ }
148
+
149
+ // ../annotation-toolkit/src/composite.ts
150
+ function loadImage(src) {
151
+ return new Promise((resolve, reject) => {
152
+ const img = new Image();
153
+ img.onload = () => resolve(img);
154
+ img.onerror = () => reject(new Error("Failed to load screenshot for compositing"));
155
+ img.src = src;
156
+ });
157
+ }
158
+ async function compositeAnnotation(screenshotDataUrl, rect, capturedSize) {
159
+ const img = await loadImage(screenshotDataUrl);
160
+ const canvas = document.createElement("canvas");
161
+ canvas.width = img.width;
162
+ canvas.height = img.height;
163
+ const ctx = canvas.getContext("2d");
164
+ if (!ctx) throw new Error("2d canvas context unavailable");
165
+ ctx.drawImage(img, 0, 0);
166
+ const scaleX = img.width / (capturedSize.width || img.width);
167
+ const scaleY = img.height / (capturedSize.height || img.height);
168
+ const x = rect.x * scaleX;
169
+ const y = rect.y * scaleY;
170
+ const width = rect.width * scaleX;
171
+ const height = rect.height * scaleY;
172
+ ctx.fillStyle = "rgba(124, 58, 237, 0.15)";
173
+ ctx.fillRect(x, y, width, height);
174
+ ctx.lineWidth = 3;
175
+ ctx.strokeStyle = "#7c3aed";
176
+ ctx.strokeRect(x, y, width, height);
177
+ return canvas.toDataURL("image/png");
178
+ }
179
+
180
+ // src/capture/screenshot.ts
181
+ import { domToPng } from "modern-screenshot";
182
+ async function captureScreenshot() {
183
+ try {
184
+ const rect = document.documentElement.getBoundingClientRect();
185
+ const dataUrl = await domToPng(document.documentElement, {
186
+ backgroundColor: "#ffffff",
187
+ quality: 0.92,
188
+ filter: (node) => !(node instanceof HTMLElement && node.hasAttribute("data-repros-toolbar"))
189
+ });
190
+ return { dataUrl, width: rect.width, height: rect.height };
191
+ } catch (err) {
192
+ console.warn("[Repros SDK] screenshot capture failed:", err);
193
+ return null;
194
+ }
195
+ }
196
+
197
+ // src/toolbar/styles.ts
198
+ var TOOLBAR_STYLES = `
199
+ :host { all: initial; }
200
+
201
+ .rp-root {
202
+ all: initial;
203
+ --rp-surface: #161b1f;
204
+ --rp-surface-2: #1f262b;
205
+ --rp-surface-3: #262e34;
206
+ --rp-border: #2c343b;
207
+ --rp-border-strong: #3a4249;
208
+ --rp-ink: #edefe9;
209
+ --rp-ink-2: #c4cbc3;
210
+ --rp-ink-3: #8d958e;
211
+ --rp-accent: #7c3aed;
212
+ --rp-accent-hover: #6d28d9;
213
+ --rp-accent-soft: #a78bfa;
214
+ --rp-error: #f07a5f;
215
+ --rp-pass: #4fbe7c;
216
+ --rp-note: #c4b5fd;
217
+ font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
218
+ font-size: 13px;
219
+ line-height: 1.4;
220
+ color: var(--rp-ink);
221
+ -webkit-font-smoothing: antialiased;
222
+ }
223
+
224
+ .rp-root *, .rp-root *::before, .rp-root *::after { box-sizing: border-box; }
225
+
226
+ .rp-panel, .rp-pill, .rp-consent {
227
+ position: fixed;
228
+ bottom: 16px;
229
+ right: 16px;
230
+ z-index: 2147483647;
231
+ }
232
+
233
+ .rp-panel, .rp-consent {
234
+ width: 300px;
235
+ display: flex;
236
+ flex-direction: column;
237
+ gap: 10px;
238
+ padding: 12px;
239
+ background: var(--rp-surface);
240
+ border: 1px solid var(--rp-border);
241
+ border-radius: 14px;
242
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.35), 0 2px 6px rgba(0, 0, 0, 0.25);
243
+ animation: rp-in 160ms ease-out;
244
+ }
245
+
246
+ @keyframes rp-in {
247
+ from { opacity: 0; transform: translateY(6px); }
248
+ to { opacity: 1; transform: translateY(0); }
249
+ }
250
+
251
+ @media (prefers-reduced-motion: reduce) {
252
+ .rp-dot, .rp-panel, .rp-consent { animation: none; }
253
+ }
254
+
255
+ .rp-mono { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
256
+
257
+ .rp-header { display: flex; align-items: center; justify-content: space-between; }
258
+ .rp-header-main { display: flex; align-items: center; gap: 8px; }
259
+
260
+ .rp-status {
261
+ display: inline-flex; align-items: center; gap: 6px;
262
+ padding: 2px 8px; border-radius: 999px;
263
+ font-size: 10.5px; font-weight: 700; letter-spacing: 0.06em;
264
+ background: rgba(124, 58, 237, 0.18); color: var(--rp-accent-soft);
265
+ }
266
+
267
+ .rp-elapsed { font-size: 12px; color: var(--rp-ink-3); }
268
+
269
+ .rp-dot {
270
+ width: 7px; height: 7px; border-radius: 50%; background: var(--rp-error);
271
+ animation: rp-pulse 1.8s ease-in-out infinite;
272
+ }
273
+
274
+ @keyframes rp-pulse {
275
+ 0% { box-shadow: 0 0 0 0 rgba(240, 122, 95, 0.55); }
276
+ 70% { box-shadow: 0 0 0 6px rgba(240, 122, 95, 0); }
277
+ 100% { box-shadow: 0 0 0 0 rgba(240, 122, 95, 0); }
278
+ }
279
+
280
+ .rp-title { font-size: 14px; font-weight: 650; color: var(--rp-ink); }
281
+ .rp-body { font-size: 12.5px; color: var(--rp-ink-2); }
282
+
283
+ .rp-counts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
284
+ .rp-count {
285
+ display: flex; flex-direction: column; align-items: flex-start;
286
+ padding: 5px 7px; border-radius: 8px; background: var(--rp-surface-2);
287
+ color: var(--rp-ink-3); font-size: 10px; line-height: 1.2; white-space: nowrap;
288
+ }
289
+ .rp-count-value { font-size: 15px; font-weight: 650; font-variant-numeric: tabular-nums; color: var(--rp-ink-2); }
290
+ .rp-count-error .rp-count-value { color: var(--rp-error); }
291
+ .rp-count-note .rp-count-value { color: var(--rp-note); }
292
+
293
+ .rp-actions { display: flex; gap: 6px; }
294
+
295
+ .rp-btn, .rp-icon-btn, .rp-pill {
296
+ font: inherit; color: inherit; cursor: pointer; border: none; background: none;
297
+ }
298
+ .rp-btn {
299
+ display: inline-flex; align-items: center; justify-content: center; gap: 5px;
300
+ flex: 1 1 auto; padding: 7px 10px; border-radius: 8px;
301
+ font-size: 12.5px; font-weight: 600; white-space: nowrap;
302
+ transition: background-color 120ms ease, border-color 120ms ease, opacity 120ms ease;
303
+ }
304
+ .rp-btn:disabled { opacity: 0.45; cursor: default; }
305
+ .rp-btn:focus-visible, .rp-icon-btn:focus-visible, .rp-pill:focus-visible {
306
+ outline: 2px solid var(--rp-accent-soft); outline-offset: 2px;
307
+ }
308
+
309
+ .rp-btn-primary { background: var(--rp-accent); color: #fff; }
310
+ .rp-btn-primary:hover:not(:disabled) { background: var(--rp-accent-hover); }
311
+
312
+ .rp-btn-secondary { background: var(--rp-surface-2); border: 1px solid var(--rp-border); color: var(--rp-ink-2); }
313
+ .rp-btn-secondary:hover:not(:disabled) { background: var(--rp-surface-3); border-color: var(--rp-border-strong); }
314
+
315
+ .rp-btn-stop {
316
+ background: rgba(240, 122, 95, 0.14); border: 1px solid rgba(240, 122, 95, 0.35); color: var(--rp-error);
317
+ }
318
+ .rp-btn-stop:hover:not(:disabled) { background: rgba(240, 122, 95, 0.22); }
319
+
320
+ .rp-icon-btn {
321
+ display: inline-flex; align-items: center; justify-content: center;
322
+ width: 24px; height: 24px; border-radius: 6px; color: var(--rp-ink-3);
323
+ }
324
+ .rp-icon-btn:hover { background: var(--rp-surface-2); color: var(--rp-ink); }
325
+
326
+ .rp-pill {
327
+ display: inline-flex; align-items: center; gap: 8px;
328
+ padding: 7px 12px; border-radius: 999px;
329
+ background: var(--rp-surface); border: 1px solid var(--rp-border);
330
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3); font-size: 12px; color: var(--rp-ink-2);
331
+ }
332
+ .rp-pill:hover { border-color: var(--rp-border-strong); }
333
+ .rp-pill-count { font-weight: 700; font-variant-numeric: tabular-nums; color: var(--rp-note); }
334
+ .rp-pill-count.rp-tone-error { color: var(--rp-error); }
335
+
336
+ .rp-error { font-size: 11.5px; color: var(--rp-error); margin: 0; }
337
+ .rp-fine-print { font-size: 10.5px; color: var(--rp-ink-3); margin: 0; }
338
+ `;
339
+
340
+ // src/toolbar/submit.ts
341
+ async function submitSession(token, apiBase) {
342
+ try {
343
+ const res = await fetch(`${apiBase.replace(/\/+$/, "")}/api/customer-sessions/submit`, {
344
+ method: "POST",
345
+ headers: { "Content-Type": "application/json" },
346
+ body: JSON.stringify({ token })
347
+ });
348
+ const data = await res.json().catch(() => null);
349
+ if (data?.ok) return "ok";
350
+ if (data?.reason === "already_submitted") return "already_submitted";
351
+ console.warn("[Repros SDK] couldn't send report:", data?.reason ?? res.status);
352
+ return "error";
353
+ } catch (err) {
354
+ console.warn("[Repros SDK] couldn't reach Repros to send report:", err);
355
+ return "error";
356
+ }
357
+ }
358
+
359
+ // src/toolbar/annotation.ts
360
+ async function submitAnnotation(token, apiBase, note, screenshot) {
361
+ try {
362
+ const res = await fetch(`${apiBase.replace(/\/+$/, "")}/api/customer-sessions/annotation`, {
363
+ method: "POST",
364
+ headers: { "Content-Type": "application/json" },
365
+ body: JSON.stringify({ token, note, screenshot })
366
+ });
367
+ const data = await res.json().catch(() => null);
368
+ if (data?.ok) return "ok";
369
+ console.warn("[Repros SDK] couldn't save that note:", data?.reason ?? res.status);
370
+ return "error";
371
+ } catch (err) {
372
+ console.warn("[Repros SDK] couldn't reach Repros to save that note:", err);
373
+ return "error";
374
+ }
375
+ }
376
+
377
+ // src/toolbar/toolbar.ts
378
+ function formatElapsed(ms) {
379
+ const totalSeconds = Math.max(0, Math.floor(ms / 1e3));
380
+ const minutes = Math.floor(totalSeconds / 60);
381
+ const seconds = totalSeconds % 60;
382
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
383
+ }
384
+ function mountToolbar(session2, options) {
385
+ const host = document.createElement("div");
386
+ host.setAttribute("data-repros-toolbar", "");
387
+ const shadow = host.attachShadow({ mode: "open" });
388
+ const styleEl = document.createElement("style");
389
+ styleEl.textContent = TOOLBAR_STYLES;
390
+ shadow.appendChild(styleEl);
391
+ const root = document.createElement("div");
392
+ root.className = "rp-root";
393
+ shadow.appendChild(root);
394
+ document.body.appendChild(host);
395
+ const startedAt = Date.now();
396
+ let state = options.mode === "customer" ? { kind: "consent" } : { kind: "active", minimized: false, submitting: false, error: null, annotate: { kind: "idle" } };
397
+ let elapsedTimer;
398
+ let counts = { errors: 0, warnings: 0, failedRequests: 0 };
399
+ let notesCount = 0;
400
+ let cancelPicking = null;
401
+ function setState(next) {
402
+ state = next;
403
+ render();
404
+ }
405
+ function ensureElapsedTimer() {
406
+ if (elapsedTimer) return;
407
+ elapsedTimer = setInterval(() => {
408
+ if (state.kind === "active" && !state.minimized) render();
409
+ }, 1e3);
410
+ }
411
+ async function handleSubmit() {
412
+ if (state.kind !== "active") return;
413
+ setState({ ...state, submitting: true, error: null });
414
+ const outcome = await submitSession(session2.token, session2.apiBase);
415
+ if (outcome === "ok" || outcome === "already_submitted") {
416
+ if (elapsedTimer) clearInterval(elapsedTimer);
417
+ options.onSubmitted?.();
418
+ setState({ kind: "sent" });
419
+ return;
420
+ }
421
+ setState({
422
+ kind: "active",
423
+ minimized: false,
424
+ submitting: false,
425
+ error: "Couldn't send that \u2014 check your connection and try again.",
426
+ annotate: { kind: "idle" }
427
+ });
428
+ }
429
+ function startPicking(mode) {
430
+ if (state.kind !== "active") return;
431
+ setState({ ...state, annotate: { kind: "picking", mode } });
432
+ host.style.display = "none";
433
+ const onPick = ({ rect }) => {
434
+ cancelPicking = null;
435
+ host.style.display = "";
436
+ if (state.kind !== "active") return;
437
+ setState({ ...state, annotate: { kind: "composing", rect, note: "", saving: false, error: null } });
438
+ };
439
+ const onCancel = () => {
440
+ cancelPicking = null;
441
+ host.style.display = "";
442
+ if (state.kind !== "active") return;
443
+ setState({ ...state, annotate: { kind: "idle" } });
444
+ };
445
+ cancelPicking = mode === "element" ? startElementSelect(onPick, onCancel) : startFreeDraw(onPick, onCancel);
446
+ }
447
+ async function handleSaveAnnotation() {
448
+ if (state.kind !== "active" || state.annotate.kind !== "composing") return;
449
+ const { rect, note } = state.annotate;
450
+ const trimmed = note.trim();
451
+ if (!trimmed) return;
452
+ setState({ ...state, annotate: { ...state.annotate, saving: true, error: null } });
453
+ const shot = await captureScreenshot();
454
+ if (!shot) {
455
+ setState({
456
+ ...state,
457
+ annotate: { kind: "composing", rect, note, saving: false, error: "Couldn't capture a screenshot \u2014 try again." }
458
+ });
459
+ return;
460
+ }
461
+ const composited = await compositeAnnotation(shot.dataUrl, rect, { width: shot.width, height: shot.height });
462
+ const outcome = await submitAnnotation(session2.token, session2.apiBase, trimmed, composited);
463
+ if (outcome !== "ok") {
464
+ setState({
465
+ ...state,
466
+ annotate: { kind: "composing", rect, note, saving: false, error: "Couldn't save that note \u2014 try again." }
467
+ });
468
+ return;
469
+ }
470
+ notesCount++;
471
+ if (state.kind === "active") setState({ ...state, annotate: { kind: "idle" } });
472
+ }
473
+ function render() {
474
+ root.innerHTML = "";
475
+ if (state.kind === "consent") {
476
+ const card = document.createElement("div");
477
+ card.className = "rp-consent";
478
+ card.setAttribute("role", "dialog");
479
+ card.setAttribute("aria-label", "Repros consent");
480
+ card.innerHTML = `
481
+ <div class="rp-title">Help report this problem?</div>
482
+ <p class="rp-body">Repros will note what you do on this page \u2014 clicks, errors, a note you add \u2014 so the team can see exactly what went wrong. Nothing is shared until you send it.</p>
483
+ <div class="rp-actions">
484
+ <button class="rp-btn rp-btn-secondary" data-action="decline">Not now</button>
485
+ <button class="rp-btn rp-btn-primary" data-action="accept">Continue</button>
486
+ </div>
487
+ `;
488
+ card.querySelector('[data-action="decline"]')?.addEventListener("click", () => {
489
+ host.remove();
490
+ options.onDeclined?.();
491
+ });
492
+ card.querySelector('[data-action="accept"]')?.addEventListener("click", () => {
493
+ ensureElapsedTimer();
494
+ options.onAccepted?.();
495
+ setState({ kind: "active", minimized: false, submitting: false, error: null, annotate: { kind: "idle" } });
496
+ });
497
+ root.appendChild(card);
498
+ return;
499
+ }
500
+ if (state.kind === "sent") {
501
+ const card = document.createElement("div");
502
+ card.className = "rp-consent";
503
+ card.innerHTML = `
504
+ <div class="rp-title">Thanks \u2014 report sent</div>
505
+ <p class="rp-body">The team can now see what happened here.</p>
506
+ `;
507
+ root.appendChild(card);
508
+ setTimeout(() => host.remove(), 4e3);
509
+ return;
510
+ }
511
+ ensureElapsedTimer();
512
+ const active = state;
513
+ if (active.minimized) {
514
+ const pill = document.createElement("button");
515
+ pill.className = "rp-pill";
516
+ pill.title = "Show the Repros toolbar";
517
+ const errorBadge = options.mode === "qa" && counts.errors > 0 ? `<span class="rp-pill-count rp-tone-error">${counts.errors}</span>` : "";
518
+ pill.innerHTML = `<span class="rp-dot"></span><span class="rp-mono">${formatElapsed(Date.now() - startedAt)}</span>${errorBadge}`;
519
+ pill.addEventListener("click", () => setState({ ...active, minimized: false }));
520
+ root.appendChild(pill);
521
+ return;
522
+ }
523
+ const panel = document.createElement("div");
524
+ panel.className = "rp-panel";
525
+ panel.setAttribute("role", "region");
526
+ panel.setAttribute("aria-label", options.mode === "customer" ? "Repros report" : "Repros test session");
527
+ const header = document.createElement("div");
528
+ header.className = "rp-header";
529
+ header.innerHTML = `
530
+ <div class="rp-header-main">
531
+ <span class="rp-status"><span class="rp-dot"></span>${options.mode === "customer" ? "RECORDING" : "REC"}</span>
532
+ <span class="rp-mono rp-elapsed">${formatElapsed(Date.now() - startedAt)}</span>
533
+ </div>
534
+ `;
535
+ const minimizeBtn = document.createElement("button");
536
+ minimizeBtn.className = "rp-icon-btn";
537
+ minimizeBtn.title = "Minimize";
538
+ minimizeBtn.setAttribute("aria-label", "Minimize toolbar");
539
+ minimizeBtn.textContent = "\u2013";
540
+ minimizeBtn.addEventListener("click", () => setState({ ...active, minimized: true }));
541
+ header.appendChild(minimizeBtn);
542
+ panel.appendChild(header);
543
+ if (options.mode === "qa") {
544
+ const countsEl = document.createElement("div");
545
+ countsEl.className = "rp-counts";
546
+ countsEl.innerHTML = `
547
+ <span class="rp-count rp-count-error"><span class="rp-count-value">${counts.errors}</span>errors</span>
548
+ <span class="rp-count rp-count-error"><span class="rp-count-value">${counts.failedRequests}</span>failed req</span>
549
+ <span class="rp-count"><span class="rp-count-value">${counts.warnings}</span>warnings</span>
550
+ <span class="rp-count rp-count-note"><span class="rp-count-value">${notesCount}</span>notes</span>
551
+ `;
552
+ panel.appendChild(countsEl);
553
+ }
554
+ if (active.annotate.kind === "choosing") {
555
+ const chooser = document.createElement("div");
556
+ chooser.className = "rp-actions";
557
+ chooser.innerHTML = `
558
+ <button class="rp-btn rp-btn-secondary" data-action="element">Select element</button>
559
+ <button class="rp-btn rp-btn-secondary" data-action="draw">Draw box</button>
560
+ `;
561
+ chooser.querySelector('[data-action="element"]')?.addEventListener("click", () => startPicking("element"));
562
+ chooser.querySelector('[data-action="draw"]')?.addEventListener("click", () => startPicking("draw"));
563
+ panel.appendChild(chooser);
564
+ const cancel = document.createElement("button");
565
+ cancel.className = "rp-btn rp-btn-secondary";
566
+ cancel.textContent = "Cancel";
567
+ cancel.addEventListener("click", () => setState({ ...active, annotate: { kind: "idle" } }));
568
+ panel.appendChild(cancel);
569
+ } else if (active.annotate.kind === "picking") {
570
+ const hint = document.createElement("p");
571
+ hint.className = "rp-fine-print";
572
+ hint.textContent = active.annotate.mode === "element" ? "Click something on the page\u2026 (Esc to cancel)" : "Drag a box\u2026 (Esc to cancel)";
573
+ panel.appendChild(hint);
574
+ } else if (active.annotate.kind === "composing") {
575
+ const compose = active.annotate;
576
+ const wrap = document.createElement("div");
577
+ wrap.innerHTML = `<textarea class="rp-fine-print" style="width:100%;min-height:56px;background:var(--rp-surface-2);border:1px solid var(--rp-border);border-radius:8px;padding:6px 8px;color:var(--rp-ink);font:inherit;resize:vertical;" placeholder="What's wrong here?"></textarea>`;
578
+ const textarea = wrap.querySelector("textarea");
579
+ textarea.value = compose.note;
580
+ panel.appendChild(wrap);
581
+ const composeActions = document.createElement("div");
582
+ composeActions.className = "rp-actions";
583
+ const saveBtn = document.createElement("button");
584
+ saveBtn.className = "rp-btn rp-btn-primary";
585
+ saveBtn.disabled = compose.saving || !compose.note.trim();
586
+ saveBtn.textContent = compose.saving ? "Saving\u2026" : "Save note";
587
+ saveBtn.addEventListener("click", () => void handleSaveAnnotation());
588
+ textarea.addEventListener("input", () => {
589
+ if (state.kind === "active" && state.annotate.kind === "composing") state.annotate.note = textarea.value;
590
+ saveBtn.disabled = compose.saving || !textarea.value.trim();
591
+ });
592
+ const cancelBtn = document.createElement("button");
593
+ cancelBtn.className = "rp-btn rp-btn-secondary";
594
+ cancelBtn.disabled = compose.saving;
595
+ cancelBtn.textContent = "Cancel";
596
+ cancelBtn.addEventListener("click", () => setState({ ...active, annotate: { kind: "idle" } }));
597
+ composeActions.append(saveBtn, cancelBtn);
598
+ panel.appendChild(composeActions);
599
+ if (compose.error) {
600
+ const err = document.createElement("p");
601
+ err.className = "rp-error";
602
+ err.textContent = compose.error;
603
+ panel.appendChild(err);
604
+ }
605
+ } else {
606
+ const actions = document.createElement("div");
607
+ actions.className = "rp-actions";
608
+ const noteBtn = document.createElement("button");
609
+ noteBtn.className = "rp-btn rp-btn-secondary";
610
+ noteBtn.textContent = "Note";
611
+ noteBtn.addEventListener("click", () => setState({ ...active, annotate: { kind: "choosing" } }));
612
+ actions.appendChild(noteBtn);
613
+ if (options.mode === "customer") {
614
+ const submitBtn = document.createElement("button");
615
+ submitBtn.className = "rp-btn rp-btn-primary";
616
+ submitBtn.disabled = active.submitting;
617
+ submitBtn.textContent = active.submitting ? "Sending\u2026" : "Send report";
618
+ submitBtn.addEventListener("click", () => void handleSubmit());
619
+ actions.appendChild(submitBtn);
620
+ } else {
621
+ const stopBtn = document.createElement("button");
622
+ stopBtn.className = "rp-btn rp-btn-stop";
623
+ stopBtn.disabled = !options.onStop;
624
+ stopBtn.title = options.onStop ? "Stop recording" : "Not wired up yet";
625
+ stopBtn.textContent = "Stop";
626
+ stopBtn.addEventListener("click", () => options.onStop?.());
627
+ actions.appendChild(stopBtn);
628
+ }
629
+ panel.appendChild(actions);
630
+ if (active.error) {
631
+ const err = document.createElement("p");
632
+ err.className = "rp-error";
633
+ err.textContent = active.error;
634
+ panel.appendChild(err);
635
+ }
636
+ }
637
+ root.appendChild(panel);
638
+ }
639
+ render();
640
+ return {
641
+ setCounts(next) {
642
+ counts = next;
643
+ if (state.kind === "active") render();
644
+ },
645
+ destroy() {
646
+ if (elapsedTimer) clearInterval(elapsedTimer);
647
+ cancelPicking?.();
648
+ host.remove();
649
+ }
650
+ };
651
+ }
652
+
653
+ // src/capture/buffer.ts
654
+ var FLUSH_INTERVAL_MS = 5e3;
655
+ var MAX_BUFFERED = 50;
656
+ function createCaptureBuffer(session2, onCounts) {
657
+ let logEntries = [];
658
+ let networkRequests = [];
659
+ const counts = { errors: 0, warnings: 0, failedRequests: 0 };
660
+ function redact(text) {
661
+ return text.split(session2.token).join("[repros-token]");
662
+ }
663
+ function addLog(entry) {
664
+ if (logEntries.length >= MAX_BUFFERED) return;
665
+ logEntries.push({
666
+ ...entry,
667
+ message: redact(entry.message),
668
+ stackTrace: entry.stackTrace ? redact(entry.stackTrace) : entry.stackTrace,
669
+ sourceUrl: entry.sourceUrl ? redact(entry.sourceUrl) : entry.sourceUrl
670
+ });
671
+ if (entry.type === "console_warn") counts.warnings++;
672
+ else counts.errors++;
673
+ onCounts({ ...counts });
674
+ }
675
+ function addNetwork(entry) {
676
+ if (networkRequests.length >= MAX_BUFFERED) return;
677
+ networkRequests.push({ ...entry, url: redact(entry.url) });
678
+ counts.failedRequests++;
679
+ onCounts({ ...counts });
680
+ }
681
+ async function flush() {
682
+ if (logEntries.length === 0 && networkRequests.length === 0) return;
683
+ const batchLog = logEntries;
684
+ const batchNetwork = networkRequests;
685
+ logEntries = [];
686
+ networkRequests = [];
687
+ const url = `${session2.apiBase.replace(/\/+$/, "")}/api/customer-sessions/capture`;
688
+ try {
689
+ await fetch(url, {
690
+ method: "POST",
691
+ headers: { "Content-Type": "application/json" },
692
+ body: JSON.stringify({ token: session2.token, logEntries: batchLog, networkRequests: batchNetwork }),
693
+ keepalive: true
694
+ });
695
+ } catch (err) {
696
+ console.warn("[Repros SDK] capture flush failed:", err);
697
+ }
698
+ }
699
+ const timer = setInterval(() => void flush(), FLUSH_INTERVAL_MS);
700
+ const onVisibilityChange = () => {
701
+ if (document.visibilityState === "hidden") void flush();
702
+ };
703
+ document.addEventListener("visibilitychange", onVisibilityChange);
704
+ window.addEventListener("pagehide", () => void flush());
705
+ function stop() {
706
+ clearInterval(timer);
707
+ document.removeEventListener("visibilitychange", onVisibilityChange);
708
+ void flush();
709
+ }
710
+ return { addLog, addNetwork, stop };
711
+ }
712
+
713
+ // src/capture/serialize.ts
714
+ var MAX_MESSAGE_CHARS = 2e3;
715
+ function serializeArg(arg) {
716
+ if (typeof arg === "string") return arg;
717
+ if (arg instanceof Error) return arg.stack?.startsWith(arg.name) ? arg.stack : `${arg.name}: ${arg.message}
718
+ ${arg.stack ?? ""}`;
719
+ if (typeof arg === "undefined") return "undefined";
720
+ if (typeof arg === "function") return `[Function ${arg.name || "anonymous"}]`;
721
+ if (typeof arg === "symbol" || typeof arg === "bigint") return String(arg);
722
+ const seen = /* @__PURE__ */ new WeakSet();
723
+ try {
724
+ return JSON.stringify(arg, (_key, value) => {
725
+ if (typeof value === "object" && value !== null) {
726
+ if (seen.has(value)) return "[Circular]";
727
+ seen.add(value);
728
+ if (value instanceof Map) return Object.fromEntries(value);
729
+ if (value instanceof Set) return [...value];
730
+ }
731
+ if (typeof value === "bigint") return `${value}n`;
732
+ return value;
733
+ }) ?? String(arg);
734
+ } catch {
735
+ try {
736
+ return String(arg);
737
+ } catch {
738
+ return "[Unserializable]";
739
+ }
740
+ }
741
+ }
742
+ function truncate(message) {
743
+ return message.length > MAX_MESSAGE_CHARS ? `${message.slice(0, MAX_MESSAGE_CHARS)}\u2026 [truncated]` : message;
744
+ }
745
+ function serializeConsoleArgs(args) {
746
+ return truncate(args.map(serializeArg).join(" "));
747
+ }
748
+
749
+ // src/capture/console.ts
750
+ var METHOD_TO_TYPE = { warn: "console_warn", error: "console_error" };
751
+ function installConsoleCapture(buffer) {
752
+ const originals = { warn: console.warn.bind(console), error: console.error.bind(console) };
753
+ Object.keys(METHOD_TO_TYPE).forEach((method) => {
754
+ console[method] = (...args) => {
755
+ originals[method](...args);
756
+ try {
757
+ const errorArg = args.find((a) => a instanceof Error);
758
+ buffer.addLog({
759
+ type: METHOD_TO_TYPE[method],
760
+ message: serializeConsoleArgs(args),
761
+ stackTrace: errorArg?.stack ?? (method === "error" ? new Error().stack : void 0),
762
+ sourceUrl: window.location.href,
763
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
764
+ });
765
+ } catch {
766
+ }
767
+ };
768
+ });
769
+ return () => {
770
+ console.warn = originals.warn;
771
+ console.error = originals.error;
772
+ };
773
+ }
774
+
775
+ // src/capture/errors.ts
776
+ function installErrorCapture(buffer) {
777
+ const onError = (event) => {
778
+ const location = event.filename ? `${event.filename}:${event.lineno}:${event.colno}` : void 0;
779
+ buffer.addLog({
780
+ type: "window_error",
781
+ message: truncate(event.message || serializeArg(event.error)),
782
+ stackTrace: event.error instanceof Error ? event.error.stack : location ? ` at ${location}` : void 0,
783
+ sourceUrl: window.location.href,
784
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
785
+ });
786
+ };
787
+ const onRejection = (event) => {
788
+ const reason = event.reason;
789
+ buffer.addLog({
790
+ type: "unhandled_rejection",
791
+ message: truncate(reason instanceof Error ? `${reason.name}: ${reason.message}` : serializeArg(reason)),
792
+ stackTrace: reason instanceof Error ? reason.stack : void 0,
793
+ sourceUrl: window.location.href,
794
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
795
+ });
796
+ };
797
+ window.addEventListener("error", onError);
798
+ window.addEventListener("unhandledrejection", onRejection);
799
+ return () => {
800
+ window.removeEventListener("error", onError);
801
+ window.removeEventListener("unhandledrejection", onRejection);
802
+ };
803
+ }
804
+
805
+ // src/capture/network.ts
806
+ function installNetworkCapture(buffer) {
807
+ const restoreFetch = installFetchCapture(buffer);
808
+ const restoreXhr = installXhrCapture(buffer);
809
+ return () => {
810
+ restoreFetch();
811
+ restoreXhr();
812
+ };
813
+ }
814
+ function requestInfo(input, init2) {
815
+ if (typeof input === "string" || input instanceof URL) {
816
+ return { method: init2?.method?.toUpperCase() ?? "GET", url: String(input) };
817
+ }
818
+ return { method: (init2?.method ?? input.method ?? "GET").toUpperCase(), url: input.url };
819
+ }
820
+ function installFetchCapture(buffer) {
821
+ if (typeof window.fetch !== "function") return () => {
822
+ };
823
+ const originalFetch = window.fetch.bind(window);
824
+ window.fetch = async (input, init2) => {
825
+ const { method, url } = requestInfo(input, init2);
826
+ const startedAt = performance.now();
827
+ try {
828
+ const response = await originalFetch(input, init2);
829
+ if (!response.ok) {
830
+ buffer.addNetwork({
831
+ method,
832
+ url,
833
+ statusCode: response.status,
834
+ statusText: response.statusText,
835
+ durationMs: Math.round(performance.now() - startedAt),
836
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
837
+ });
838
+ }
839
+ return response;
840
+ } catch (err) {
841
+ buffer.addNetwork({
842
+ method,
843
+ url,
844
+ errorText: err instanceof Error ? err.message : String(err),
845
+ durationMs: Math.round(performance.now() - startedAt),
846
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
847
+ });
848
+ throw err;
849
+ }
850
+ };
851
+ return () => {
852
+ window.fetch = originalFetch;
853
+ };
854
+ }
855
+ var XHR_STATE = /* @__PURE__ */ new WeakMap();
856
+ function installXhrCapture(buffer) {
857
+ const proto = XMLHttpRequest.prototype;
858
+ const originalOpen = proto.open;
859
+ const originalSend = proto.send;
860
+ proto.open = function(method, url, ...rest) {
861
+ XHR_STATE.set(this, { method: method.toUpperCase(), url: String(url), startedAt: 0 });
862
+ return originalOpen.call(this, method, url, ...rest);
863
+ };
864
+ proto.send = function(...args) {
865
+ const state = XHR_STATE.get(this);
866
+ if (state) state.startedAt = performance.now();
867
+ const onLoadEnd = () => {
868
+ const current = XHR_STATE.get(this);
869
+ if (!current) return;
870
+ if (this.status === 0 || this.status >= 400) {
871
+ buffer.addNetwork({
872
+ method: current.method,
873
+ url: current.url,
874
+ statusCode: this.status || void 0,
875
+ statusText: this.statusText || void 0,
876
+ errorText: this.status === 0 ? "Network error" : void 0,
877
+ durationMs: Math.round(performance.now() - current.startedAt),
878
+ occurredAt: (/* @__PURE__ */ new Date()).toISOString()
879
+ });
880
+ }
881
+ this.removeEventListener("loadend", onLoadEnd);
882
+ XHR_STATE.delete(this);
883
+ };
884
+ this.addEventListener("loadend", onLoadEnd);
885
+ return originalSend.apply(this, args);
886
+ };
887
+ return () => {
888
+ proto.open = originalOpen;
889
+ proto.send = originalSend;
890
+ };
891
+ }
892
+
893
+ // src/capture/index.ts
894
+ function startCapture(session2, onCounts) {
895
+ const buffer = createCaptureBuffer(session2, onCounts);
896
+ const restoreConsole = installConsoleCapture(buffer);
897
+ const restoreErrors = installErrorCapture(buffer);
898
+ const restoreNetwork = installNetworkCapture(buffer);
899
+ return () => {
900
+ restoreConsole();
901
+ restoreErrors();
902
+ restoreNetwork();
903
+ buffer.stop();
904
+ };
905
+ }
906
+
907
+ // src/index.ts
908
+ var session = null;
909
+ var initStarted = false;
910
+ async function init(options = {}) {
911
+ if (initStarted) return session;
912
+ initStarted = true;
913
+ const token = readClaimToken();
914
+ if (!token) return null;
915
+ session = await claim(token, options.apiBase);
916
+ if (!session) return null;
917
+ const activeSession = session;
918
+ let stopCapture = null;
919
+ const toolbar = mountToolbar(activeSession, {
920
+ mode: "customer",
921
+ onAccepted: () => {
922
+ stopCapture = startCapture(activeSession, (counts) => toolbar.setCounts(counts));
923
+ },
924
+ onSubmitted: () => stopCapture?.()
925
+ });
926
+ return session;
927
+ }
928
+ function getSession() {
929
+ return session;
930
+ }
931
+ export {
932
+ captureScreenshot,
933
+ getSession,
934
+ init,
935
+ mountToolbar
936
+ };
937
+ //# sourceMappingURL=index.js.map