issue-sticker 0.8.0 → 0.9.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.
@@ -1,4 +1,4 @@
1
- import { q as gzip_1, r as record, a as cn, o as useCallbackRef$1, h as useComposedRefs, e as Primitive, P as Presence, i as Portal$1, f as composeEventHandlers, j as createContextScope, k as createSlot, D as DismissableLayer, C as Content, t as createPopperScope, l as useControllableState, v as Root2$1, m as useId, A as Anchor, w as Arrow, b as usePortalContainer, d as OVERLAY_LAYER, s as stopEscapePropagation, n as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, x as clsx, S as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./mouse-pointer-2-BR00y3ir.js";
1
+ import { q as onThreeRendererRegistered, t as shouldOverlayCanvas, v as getThreeHandle, C as CANVAS_INTERACTION_TAG, w as rendersBlank, x as gzip_1, y as redactUrlSecrets, z as redactSensitiveValues, A as record, B as resolvePrivacyConfig, E as isElementFullyMasked, F as grabCanvasFrames, G as applyScreenshotPrivacyMask, H as overlayCanvasFrames, I as forEachElementDeep, J as redactSensitiveText, K as REDACTED_BODY_VALUE, L as isSensitiveFieldName, N as redactSensitiveHeaders, Q as redactSensitiveBody, a as cn, o as useCallbackRef$1, h as useComposedRefs, e as Primitive, P as Presence, i as Portal$1, f as composeEventHandlers, j as createContextScope, k as createSlot, D as DismissableLayer, S as Content, T as createPopperScope, l as useControllableState, U as Root2$1, m as useId, V as Anchor, W as Arrow, b as usePortalContainer, d as OVERLAY_LAYER, s as stopEscapePropagation, n as useLayoutEffect2, X as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, Y as clsx, Z as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./three-B7nbYqGK.js";
2
2
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
3
3
  import * as React from "react";
4
4
  import React__default, { useState, createContext, useContext, useCallback, useRef, useLayoutEffect, useEffect, useMemo } from "react";
@@ -18,10 +18,12 @@ function getDefaultDisplayMedia() {
18
18
  if (typeof navigator === "undefined" || !((_a = navigator.mediaDevices) == null ? void 0 : _a.getDisplayMedia)) {
19
19
  throw new Error("Screen recording is not supported in this browser.");
20
20
  }
21
- return navigator.mediaDevices.getDisplayMedia({
22
- video: { frameRate: 30 },
23
- audio: false
24
- });
21
+ const options = {
22
+ video: { frameRate: 30, displaySurface: "browser" },
23
+ audio: false,
24
+ selfBrowserSurface: "include"
25
+ };
26
+ return navigator.mediaDevices.getDisplayMedia(options);
25
27
  }
26
28
  function createDefaultMediaRecorder(stream, options) {
27
29
  if (typeof MediaRecorder === "undefined") {
@@ -205,98 +207,361 @@ class ScreenRecordingController {
205
207
  this.listeners.forEach((listener) => listener(this.snapshot));
206
208
  }
207
209
  }
208
- const SESSION_REPLAY_BLOCK_CLASS = "rr-block";
209
- const SESSION_REPLAY_MASK_CLASS = "rr-mask";
210
- const SESSION_REPLAY_UNMASK_CLASS = "rr-unmask";
211
- const FOREIGN_BLOCK_SELECTORS = [
212
- // FullStory
213
- ".fs-exclude",
214
- ".fs-block",
215
- // Sentry — ignore 는 의미가 "입력 이벤트를 기록하지 않는다"라 차단과 어긋나지만,
216
- // 빼면 이미 그것에 기대고 있는 고객에게 회귀다. 과잉 마스킹은 안전한 방향이라 둔다.
217
- ".sentry-block",
218
- ".sentry-ignore",
219
- "[data-sentry-block]",
220
- // PostHog
221
- ".ph-no-capture",
222
- // Hotjar
223
- "[data-hj-suppress]",
224
- // OpenReplay
225
- "[data-openreplay-hidden]",
226
- // Highlight.io
227
- ".highlight-block",
228
- ".highlight-ignore",
229
- // Heap
230
- "[data-heap-ignore]",
231
- ".heap-ignore"
232
- ];
233
- const FOREIGN_MASK_SELECTORS = [
234
- // FullStory
235
- ".fs-mask",
236
- // Sentry
237
- ".sentry-mask",
238
- "[data-sentry-mask]",
239
- // LogRocket
240
- "[data-private]",
241
- // Microsoft Clarity
242
- "[data-clarity-mask]",
243
- // Hotjar
244
- "[data-hj-masked]",
245
- // OpenReplay
246
- "[data-openreplay-obscured]",
247
- // Highlight.io
248
- ".highlight-mask",
249
- // ContentSquare
250
- "[data-cs-mask]",
251
- "[data-cs-encrypt]",
252
- // Heap
253
- "[data-heap-redact-text]",
254
- "[data-heap-redact-attributes]",
255
- // Amplitude
256
- "[data-amp-mask]",
257
- // Matomo
258
- "[data-matomo-mask]"
259
- ];
260
- const FOREIGN_UNMASK_SELECTORS = [
261
- ".fs-unmask",
262
- ".sentry-unmask",
263
- "[data-clarity-unmask]",
264
- "[data-amp-unmask]",
265
- "[data-cs-capture]"
266
- ];
267
- const SESSION_REPLAY_FULL_MASK_SELECTOR = [
268
- `.${SESSION_REPLAY_MASK_CLASS}`,
269
- ...FOREIGN_MASK_SELECTORS
270
- ].join(",");
271
- const SESSION_REPLAY_BLOCK_SELECTOR = [
272
- `.${SESSION_REPLAY_BLOCK_CLASS}`,
273
- ...FOREIGN_BLOCK_SELECTORS
274
- ].join(",");
275
- const SESSION_REPLAY_UNMASK_SELECTOR = [
276
- `.${SESSION_REPLAY_UNMASK_CLASS}`,
277
- ...FOREIGN_UNMASK_SELECTORS
278
- ].join(",");
279
- function isElementFullyMasked(element, maskSelector = SESSION_REPLAY_FULL_MASK_SELECTOR, unmaskSelector = SESSION_REPLAY_UNMASK_SELECTOR) {
280
- if (!element) return false;
210
+ const SNAPSHOT_MAX_WIDTH = 640;
211
+ const SNAPSHOT_QUALITY = 0.8;
212
+ const SAMPLE_INTERVAL_MS = 250;
213
+ const SAMPLED_MAX_WIDTH = 480;
214
+ const SAMPLED_QUALITY = 0.6;
215
+ const DRAG_THRESHOLD_PX = 6;
216
+ const DISCRETE_MIN_GAP_MS = 60;
217
+ const SETTLE_TAIL_MS = 300;
218
+ function isThreeNamespace(value) {
219
+ const candidate = value;
220
+ return typeof (candidate == null ? void 0 : candidate.Raycaster) === "function" && typeof (candidate == null ? void 0 : candidate.Vector2) === "function";
221
+ }
222
+ function isHelper(object) {
223
+ let current = object;
224
+ while (current) {
225
+ if (typeof current.type === "string" && current.type.endsWith("Helper")) {
226
+ return true;
227
+ }
228
+ current = current.parent;
229
+ }
230
+ return false;
231
+ }
232
+ function nameOf(object) {
233
+ let current = object;
234
+ while (current) {
235
+ if (current.name) {
236
+ return current.name;
237
+ }
238
+ current = current.parent;
239
+ }
240
+ return (object == null ? void 0 : object.type) ?? null;
241
+ }
242
+ function pickTarget(handle, pointer) {
243
+ var _a;
244
+ const three = handle.three;
245
+ if (!isThreeNamespace(three)) {
246
+ return null;
247
+ }
248
+ const children = (_a = handle.scene) == null ? void 0 : _a.children;
249
+ if (!Array.isArray(children)) {
250
+ return null;
251
+ }
281
252
  try {
282
- const nearest = element.closest(`${maskSelector},${unmaskSelector}`);
283
- return nearest !== null && nearest.matches(maskSelector);
253
+ const raycaster = new three.Raycaster();
254
+ raycaster.setFromCamera(new three.Vector2(pointer[0], pointer[1]), handle.camera);
255
+ const hits = raycaster.intersectObjects(children, true);
256
+ const hit = hits.find(
257
+ (candidate) => {
258
+ var _a2, _b;
259
+ return ((_a2 = candidate.object) == null ? void 0 : _a2.isMesh) === true && ((_b = candidate.object) == null ? void 0 : _b.visible) !== false && !isHelper(candidate.object);
260
+ }
261
+ );
262
+ return hit ? nameOf(hit.object) : null;
284
263
  } catch {
285
- return true;
264
+ return null;
286
265
  }
287
266
  }
288
- const DEFAULT_PRIVACY_CONFIG_V2 = {
289
- version: "v2",
290
- maskAllInputs: true,
291
- maskTextSelector: "*",
292
- fullMaskSelector: SESSION_REPLAY_FULL_MASK_SELECTOR,
293
- unmaskSelector: SESSION_REPLAY_UNMASK_SELECTOR,
294
- blockSelector: SESSION_REPLAY_BLOCK_SELECTOR,
295
- allowCanvasRecording: false
296
- };
297
- function resolvePrivacyConfig(override) {
298
- if (!override) return DEFAULT_PRIVACY_CONFIG_V2;
299
- return { ...DEFAULT_PRIVACY_CONFIG_V2, ...override };
267
+ function cameraMatrixOf(handle) {
268
+ var _a, _b;
269
+ const matrix = (_a = handle.camera) == null ? void 0 : _a.matrixWorld;
270
+ try {
271
+ const values = (_b = matrix == null ? void 0 : matrix.toArray) == null ? void 0 : _b.call(matrix);
272
+ return Array.isArray(values) ? values : null;
273
+ } catch {
274
+ return null;
275
+ }
276
+ }
277
+ const FINGERPRINT_WIDTH = 32;
278
+ const FINGERPRINT_HEIGHT = 18;
279
+ async function captureSnapshot(canvas, handle, maxWidth, quality) {
280
+ try {
281
+ if (handle) {
282
+ handle.renderer.render(handle.scene, handle.camera);
283
+ } else if (rendersBlank(canvas)) {
284
+ return null;
285
+ }
286
+ const bitmap = await createImageBitmap(canvas);
287
+ try {
288
+ const scale = Math.min(1, maxWidth / bitmap.width);
289
+ const output = document.createElement("canvas");
290
+ output.width = Math.max(1, Math.round(bitmap.width * scale));
291
+ output.height = Math.max(1, Math.round(bitmap.height * scale));
292
+ const context = output.getContext("2d");
293
+ if (!context) {
294
+ return null;
295
+ }
296
+ context.drawImage(bitmap, 0, 0, output.width, output.height);
297
+ const thumb = document.createElement("canvas");
298
+ thumb.width = FINGERPRINT_WIDTH;
299
+ thumb.height = FINGERPRINT_HEIGHT;
300
+ const thumbContext = thumb.getContext("2d");
301
+ thumbContext == null ? void 0 : thumbContext.drawImage(bitmap, 0, 0, FINGERPRINT_WIDTH, FINGERPRINT_HEIGHT);
302
+ const fingerprint = thumbContext ? Array.from(
303
+ thumbContext.getImageData(0, 0, FINGERPRINT_WIDTH, FINGERPRINT_HEIGHT).data
304
+ ).join(",") : "";
305
+ return { dataUrl: output.toDataURL("image/webp", quality), fingerprint };
306
+ } finally {
307
+ bitmap.close();
308
+ }
309
+ } catch {
310
+ return null;
311
+ }
312
+ }
313
+ const FRAME_WAIT_MS = 120;
314
+ function settleFrame() {
315
+ return new Promise((resolve) => {
316
+ let done = false;
317
+ const finish = () => {
318
+ if (!done) {
319
+ done = true;
320
+ resolve();
321
+ }
322
+ };
323
+ requestAnimationFrame(finish);
324
+ setTimeout(finish, FRAME_WAIT_MS);
325
+ });
326
+ }
327
+ async function resolveNodeId(canvas) {
328
+ var _a;
329
+ try {
330
+ const { record: record2 } = await import("./three-B7nbYqGK.js").then((n) => n.a0);
331
+ const id = (_a = record2.mirror) == null ? void 0 : _a.getId(canvas);
332
+ return typeof id === "number" && id > 0 ? id : null;
333
+ } catch {
334
+ return null;
335
+ }
336
+ }
337
+ function canvasIndexOf(canvas) {
338
+ return Array.prototype.indexOf.call(document.querySelectorAll("canvas"), canvas);
339
+ }
340
+ function startCanvasInteractionTracking(options = {}) {
341
+ if (typeof document === "undefined") {
342
+ return Object.assign(() => void 0, { captureBaseline: () => void 0 });
343
+ }
344
+ const snapshotMaxWidth = options.snapshotMaxWidth ?? SNAPSHOT_MAX_WIDTH;
345
+ const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
346
+ const lastSnapshot = /* @__PURE__ */ new WeakMap();
347
+ let pending = null;
348
+ const emit2 = options.emit ?? ((tag, payload) => {
349
+ void import("./three-B7nbYqGK.js").then((n) => n.a0).then(({ addCustomEvent }) => addCustomEvent(tag, payload));
350
+ });
351
+ const pointerOf = (started, rect) => [
352
+ (started.x - rect.left) / rect.width * 2 - 1,
353
+ -((started.y - rect.top) / rect.height) * 2 + 1
354
+ ];
355
+ const trackedCanvases = () => Array.from(document.querySelectorAll("canvas")).filter((canvas) => {
356
+ const rect = canvas.getBoundingClientRect();
357
+ return rect.width >= 1 && rect.height >= 1 && shouldOverlayCanvas(canvas, []);
358
+ });
359
+ const emitShot = async (canvas, shot) => {
360
+ const handle = getThreeHandle(canvas);
361
+ const snapshot = await captureSnapshot(
362
+ canvas,
363
+ handle,
364
+ shot.detail ? snapshotMaxWidth : SAMPLED_MAX_WIDTH,
365
+ shot.detail ? SNAPSHOT_QUALITY : SAMPLED_QUALITY
366
+ );
367
+ if (snapshot && shot.sampled && !shot.baseline && lastSnapshot.get(canvas) === snapshot.fingerprint) {
368
+ return;
369
+ }
370
+ if (snapshot) {
371
+ lastSnapshot.set(canvas, snapshot.fingerprint);
372
+ }
373
+ const cameraMatrix = handle ? cameraMatrixOf(handle) : null;
374
+ if (!snapshot && !shot.target && !cameraMatrix) {
375
+ return;
376
+ }
377
+ const payload = {
378
+ kind: shot.kind,
379
+ target: shot.target,
380
+ nodeId: await resolveNodeId(canvas),
381
+ canvasIndex: canvasIndexOf(canvas),
382
+ pointer: shot.pointer,
383
+ cameraStartMatrix: shot.cameraStartMatrix,
384
+ cameraMatrix,
385
+ snapshot: (snapshot == null ? void 0 : snapshot.dataUrl) ?? null,
386
+ ...shot.sampled ? { sampled: true } : {},
387
+ ...shot.baseline ? { baseline: true } : {}
388
+ };
389
+ try {
390
+ emit2(CANVAS_INTERACTION_TAG, payload);
391
+ } catch {
392
+ }
393
+ };
394
+ const sweep = async (shot) => {
395
+ for (const canvas of trackedCanvases()) {
396
+ await emitShot(canvas, {
397
+ kind: "external",
398
+ target: null,
399
+ pointer: null,
400
+ cameraStartMatrix: null,
401
+ ...shot
402
+ });
403
+ }
404
+ };
405
+ let sweeping = false;
406
+ let pendingSweep = null;
407
+ const runSweep = async (shot) => {
408
+ if (sweeping) {
409
+ pendingSweep = shot;
410
+ return;
411
+ }
412
+ sweeping = true;
413
+ try {
414
+ await sweep(shot);
415
+ while (pendingSweep) {
416
+ const next = pendingSweep;
417
+ pendingSweep = null;
418
+ await sweep(next);
419
+ }
420
+ } finally {
421
+ sweeping = false;
422
+ }
423
+ };
424
+ let sampleTimer = null;
425
+ let tailTimer = null;
426
+ let lastDiscreteAt = 0;
427
+ const clearTimers = () => {
428
+ if (sampleTimer !== null) {
429
+ window.clearInterval(sampleTimer);
430
+ sampleTimer = null;
431
+ }
432
+ if (tailTimer !== null) {
433
+ window.clearTimeout(tailTimer);
434
+ tailTimer = null;
435
+ }
436
+ };
437
+ const markChanged = () => {
438
+ if (sampleTimer === null && sampleIntervalMs > 0) {
439
+ sampleTimer = window.setInterval(() => {
440
+ void runSweep({ sampled: true, detail: false });
441
+ }, sampleIntervalMs);
442
+ }
443
+ if (tailTimer !== null) {
444
+ window.clearTimeout(tailTimer);
445
+ }
446
+ tailTimer = window.setTimeout(() => {
447
+ tailTimer = null;
448
+ if (sampleTimer !== null) {
449
+ window.clearInterval(sampleTimer);
450
+ sampleTimer = null;
451
+ }
452
+ void (async () => {
453
+ await settleFrame();
454
+ await runSweep({ sampled: true, detail: true });
455
+ })();
456
+ }, SETTLE_TAIL_MS);
457
+ };
458
+ const onPointerDown = (event) => {
459
+ const target = event.target;
460
+ if (!(target instanceof HTMLCanvasElement) || !shouldOverlayCanvas(target, [])) {
461
+ return;
462
+ }
463
+ const handle = getThreeHandle(target);
464
+ pending = {
465
+ canvas: target,
466
+ x: event.clientX,
467
+ y: event.clientY,
468
+ cameraStartMatrix: handle ? cameraMatrixOf(handle) : null
469
+ };
470
+ };
471
+ const onPointerMove = (event) => {
472
+ const started = pending;
473
+ if (!started) {
474
+ return;
475
+ }
476
+ if (Math.hypot(event.clientX - started.x, event.clientY - started.y) <= DRAG_THRESHOLD_PX) {
477
+ return;
478
+ }
479
+ markChanged();
480
+ };
481
+ const captureDiscrete = () => {
482
+ const now = Date.now();
483
+ if (now - lastDiscreteAt >= DISCRETE_MIN_GAP_MS) {
484
+ lastDiscreteAt = now;
485
+ void (async () => {
486
+ await settleFrame();
487
+ await runSweep({ sampled: true, detail: false });
488
+ })();
489
+ }
490
+ markChanged();
491
+ };
492
+ const onContinuousInput = () => {
493
+ markChanged();
494
+ };
495
+ const onPointerUp = (event) => {
496
+ const started = pending;
497
+ pending = null;
498
+ if (!started) {
499
+ captureDiscrete();
500
+ return;
501
+ }
502
+ clearTimers();
503
+ const handle = getThreeHandle(started.canvas);
504
+ const rect = started.canvas.getBoundingClientRect();
505
+ if (rect.width < 1 || rect.height < 1) {
506
+ return;
507
+ }
508
+ const moved = Math.hypot(event.clientX - started.x, event.clientY - started.y);
509
+ const kind = moved > DRAG_THRESHOLD_PX ? "drag" : "click";
510
+ const target = handle ? pickTarget(handle, pointerOf(started, rect)) : null;
511
+ void (async () => {
512
+ await settleFrame();
513
+ await emitShot(started.canvas, {
514
+ kind,
515
+ target,
516
+ pointer: pointerOf(started, rect),
517
+ cameraStartMatrix: started.cameraStartMatrix,
518
+ sampled: false,
519
+ detail: true
520
+ });
521
+ })();
522
+ markChanged();
523
+ };
524
+ const captureBaseline = () => {
525
+ void (async () => {
526
+ await settleFrame();
527
+ await runSweep({ sampled: true, detail: true, baseline: true });
528
+ })();
529
+ };
530
+ document.addEventListener("pointerdown", onPointerDown, true);
531
+ document.addEventListener("pointermove", onPointerMove, true);
532
+ document.addEventListener("pointerup", onPointerUp, true);
533
+ document.addEventListener("pointercancel", onPointerUp, true);
534
+ document.addEventListener("input", onContinuousInput, true);
535
+ document.addEventListener("keydown", onContinuousInput, true);
536
+ document.addEventListener("change", captureDiscrete, true);
537
+ const unsubscribeRegistration = onThreeRendererRegistered((canvas) => {
538
+ void (async () => {
539
+ await settleFrame();
540
+ await emitShot(canvas, {
541
+ kind: "external",
542
+ target: null,
543
+ pointer: null,
544
+ cameraStartMatrix: null,
545
+ sampled: true,
546
+ detail: true,
547
+ baseline: true
548
+ });
549
+ })();
550
+ });
551
+ const stop = () => {
552
+ clearTimers();
553
+ unsubscribeRegistration();
554
+ document.removeEventListener("pointerdown", onPointerDown, true);
555
+ document.removeEventListener("pointermove", onPointerMove, true);
556
+ document.removeEventListener("pointerup", onPointerUp, true);
557
+ document.removeEventListener("pointercancel", onPointerUp, true);
558
+ document.removeEventListener("input", onContinuousInput, true);
559
+ document.removeEventListener("keydown", onContinuousInput, true);
560
+ document.removeEventListener("change", captureDiscrete, true);
561
+ pending = null;
562
+ };
563
+ captureBaseline();
564
+ return Object.assign(stop, { captureBaseline });
300
565
  }
301
566
  function createSegments() {
302
567
  return [{ events: [] }];
@@ -329,264 +594,49 @@ function trimSegments(segments, windowMs, now, checkoutMs) {
329
594
  }
330
595
  return segments.slice(startIdx);
331
596
  }
597
+ function spanSeconds(segments) {
598
+ var _a, _b;
599
+ const events = flattenEvents(segments);
600
+ const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
601
+ const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
602
+ if (first === void 0 || last === void 0) return 0;
603
+ return Math.round((last - first) / 1e3);
604
+ }
332
605
  function flattenEvents(segments) {
333
606
  return segments.flatMap((s2) => s2.events);
334
607
  }
335
608
  function dropEventsAfter(segments, cutoff) {
336
609
  return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
337
610
  }
338
- async function sha256Hex(bytes) {
611
+ const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
612
+ async function sha256Hex$1(bytes) {
339
613
  const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
340
614
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
341
615
  }
342
616
  async function serializeSnapshot(input) {
343
- const events = (input.redactEvents ?? ((e2) => e2))(flattenEvents(input.segments));
344
- const json = JSON.stringify(events);
345
- const payload = gzip_1(json);
346
- const checksumSha256 = await sha256Hex(payload);
617
+ const redact = input.redactEvents ?? ((e2) => e2);
618
+ const maxBytes = input.maxBytes ?? REPLAY_SNAPSHOT_MAX_BYTES;
619
+ const compress2 = (segments2) => {
620
+ const events2 = redact(flattenEvents(segments2));
621
+ return { events: events2, payload: gzip_1(JSON.stringify(events2)) };
622
+ };
623
+ let segments = input.segments;
624
+ let { events, payload } = compress2(segments);
625
+ while (payload.byteLength > maxBytes && segments.length > 1) {
626
+ segments = segments.slice(1);
627
+ ({ events, payload } = compress2(segments));
628
+ }
629
+ const checksumSha256 = await sha256Hex$1(payload);
347
630
  return {
348
631
  payload,
349
632
  checksumSha256,
350
- windowSeconds: input.windowSeconds,
633
+ windowSeconds: spanSeconds(segments),
351
634
  eventCount: events.length,
352
635
  sizeBytes: payload.byteLength,
353
636
  rrwebVersion: input.rrwebVersion,
354
637
  privacyConfigVersion: input.privacyConfigVersion
355
638
  };
356
639
  }
357
- const REDACTED_HEADER_VALUE = "[redacted]";
358
- const SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
359
- "authorization",
360
- "proxy-authorization",
361
- "cookie",
362
- "set-cookie",
363
- "x-api-key",
364
- "api-key",
365
- "apikey",
366
- "x-auth-token",
367
- "x-access-token",
368
- "x-refresh-token",
369
- "x-session-token",
370
- "x-csrf-token",
371
- "x-xsrf-token",
372
- "x-amz-security-token",
373
- "x-goog-api-key"
374
- ]);
375
- function isSensitiveHeaderName(headerName) {
376
- return SENSITIVE_HEADER_NAMES.has(headerName.toLowerCase());
377
- }
378
- function redactSensitiveHeaders(headers) {
379
- if (!headers) return headers;
380
- let changed = false;
381
- const redacted = {};
382
- for (const [name, value] of Object.entries(headers)) {
383
- if (value && isSensitiveHeaderName(name)) {
384
- redacted[name] = REDACTED_HEADER_VALUE;
385
- changed = true;
386
- } else {
387
- redacted[name] = value;
388
- }
389
- }
390
- return changed ? redacted : headers;
391
- }
392
- const REDACTED_BODY_VALUE = "[redacted]";
393
- const SENSITIVE_KEY_MARKERS = [
394
- "password",
395
- "passwd",
396
- "pwd",
397
- "secret",
398
- "token",
399
- "apikey",
400
- "privatekey",
401
- "credential",
402
- // 결제·본인확인 계열. 토스페이먼츠 공식 문서의 필드명을 그대로 딴다.
403
- // cardNumber / customerIdentityNumber / businessNumber / accountNumber(가상계좌)
404
- "cardnumber",
405
- "identitynumber",
406
- "businessnumber",
407
- "registrationnumber",
408
- "accountnumber"
409
- ];
410
- const SENSITIVE_KEY_EXACT = /* @__PURE__ */ new Set([
411
- "otp",
412
- "auth",
413
- "authorization",
414
- "bearer",
415
- // 토스페이먼츠. billingKey + customerKey 조합이면 결제를 낼 수 있고, authKey 는 일회성
416
- // 인증 키다(우리 admin 결제 성공 URL 의 쿼리 파라미터로 실제로 실린다).
417
- // paymentKey 는 넣지 않는다 — 시크릿 키 없이는 아무것도 못 하는 식별자이고, 결제 장애
418
- // 진단의 출발점이라 가리면 로그가 쓸모를 잃는다.
419
- "billingkey",
420
- "customerkey",
421
- "authkey"
422
- ]);
423
- const MAX_WALK_DEPTH = 20;
424
- function isSensitiveFieldName(name) {
425
- const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
426
- if (SENSITIVE_KEY_EXACT.has(normalized)) {
427
- return true;
428
- }
429
- return SENSITIVE_KEY_MARKERS.some((marker) => normalized.includes(marker));
430
- }
431
- function redactJsonValue(value, depth) {
432
- if (typeof value === "string") {
433
- const redacted = redactSensitiveValues(value);
434
- return { value: redacted, changed: redacted !== value };
435
- }
436
- if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") {
437
- return { value, changed: false };
438
- }
439
- if (Array.isArray(value)) {
440
- let changed2 = false;
441
- const next2 = value.map((item) => {
442
- const result = redactJsonValue(item, depth + 1);
443
- changed2 || (changed2 = result.changed);
444
- return result.value;
445
- });
446
- return changed2 ? { value: next2, changed: changed2 } : { value, changed: changed2 };
447
- }
448
- let changed = false;
449
- const next = {};
450
- for (const [key, item] of Object.entries(value)) {
451
- if (item !== "" && item != null && typeof item !== "boolean" && isSensitiveFieldName(key)) {
452
- next[key] = REDACTED_BODY_VALUE;
453
- changed = true;
454
- continue;
455
- }
456
- const result = redactJsonValue(item, depth + 1);
457
- changed || (changed = result.changed);
458
- next[key] = result.value;
459
- }
460
- return changed ? { value: next, changed } : { value, changed };
461
- }
462
- function redactKeyValuePairs(body) {
463
- let changed = false;
464
- const redacted = body.split("&").map((pair) => {
465
- const separator = pair.indexOf("=");
466
- if (separator <= 0) {
467
- return pair;
468
- }
469
- const rawKey = pair.slice(0, separator);
470
- const rawValue = pair.slice(separator + 1);
471
- if (!rawValue) {
472
- return pair;
473
- }
474
- if (isSensitiveFieldName(decodeURIComponent(rawKey))) {
475
- changed = true;
476
- return `${rawKey}=${encodeURIComponent(REDACTED_BODY_VALUE)}`;
477
- }
478
- const redactedValue = redactSensitiveValues(decodeURIComponent(rawValue));
479
- if (redactedValue === decodeURIComponent(rawValue)) {
480
- return pair;
481
- }
482
- changed = true;
483
- return `${rawKey}=${encodeURIComponent(redactedValue)}`;
484
- }).join("&");
485
- return changed ? redacted : body;
486
- }
487
- function looksLikeFormUrlEncoded(body) {
488
- return /^[^=&\s]+=[^&]*(&[^=&\s]+=[^&]*)*$/.test(body);
489
- }
490
- function redactSensitiveBody(body) {
491
- if (!body) return body;
492
- const trimmed = body.trim();
493
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
494
- try {
495
- const parsed = JSON.parse(trimmed);
496
- const { value, changed } = redactJsonValue(parsed, 0);
497
- return changed ? JSON.stringify(value) : body;
498
- } catch {
499
- }
500
- }
501
- if (looksLikeFormUrlEncoded(trimmed)) {
502
- const redacted = redactKeyValuePairs(trimmed);
503
- return redacted === trimmed ? body : redacted;
504
- }
505
- return body;
506
- }
507
- const JWT_PATTERN = /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g;
508
- const BEARER_PATTERN = /(Bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/gi;
509
- const CARD_CANDIDATE_PATTERN = /\b[2-6](?:[ -]?\d){13,18}\b/g;
510
- const RRN_CANDIDATE_PATTERN = /\b(\d{2})(\d{2})(\d{2})-?([1-8])(\d{6})\b/g;
511
- const BRN_CANDIDATE_PATTERN = /\b(\d{3})-?(\d{2})-?(\d{5})\b/g;
512
- const BRN_WEIGHTS = [1, 3, 7, 1, 3, 7, 1, 3, 5];
513
- const KR_MOBILE_PATTERN = /\b01[016789]-?\d{3,4}-?\d{4}\b/g;
514
- const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
515
- function redactEmails(text) {
516
- if (!text) return text;
517
- const out = text.replace(EMAIL_PATTERN, REDACTED_BODY_VALUE);
518
- return out === text ? text : out;
519
- }
520
- function passesBrnChecksum(digits) {
521
- let sum = 0;
522
- for (let i2 = 0; i2 < 9; i2 += 1) {
523
- sum += (digits.charCodeAt(i2) - 48) * BRN_WEIGHTS[i2];
524
- }
525
- sum += Math.floor((digits.charCodeAt(8) - 48) * 5 / 10);
526
- return (10 - sum % 10) % 10 === digits.charCodeAt(9) - 48;
527
- }
528
- function passesLuhn(digits) {
529
- let sum = 0;
530
- let double = false;
531
- for (let i2 = digits.length - 1; i2 >= 0; i2 -= 1) {
532
- let digit = digits.charCodeAt(i2) - 48;
533
- if (double) {
534
- digit *= 2;
535
- if (digit > 9) digit -= 9;
536
- }
537
- sum += digit;
538
- double = !double;
539
- }
540
- return sum % 10 === 0;
541
- }
542
- function looksLikeBirthDate(month, day) {
543
- const monthValue = Number(month);
544
- const dayValue = Number(day);
545
- return monthValue >= 1 && monthValue <= 12 && dayValue >= 1 && dayValue <= 31;
546
- }
547
- function redactSensitiveValues(text) {
548
- if (!text) return text;
549
- let out = text;
550
- out = out.replace(JWT_PATTERN, REDACTED_BODY_VALUE);
551
- out = out.replace(BEARER_PATTERN, `$1${REDACTED_BODY_VALUE}`);
552
- out = out.replace(CARD_CANDIDATE_PATTERN, (match2) => {
553
- const digits = match2.replace(/[ -]/g, "");
554
- return digits.length >= 14 && digits.length <= 19 && passesLuhn(digits) ? REDACTED_BODY_VALUE : match2;
555
- });
556
- out = out.replace(
557
- RRN_CANDIDATE_PATTERN,
558
- (match2, _year, month, day) => looksLikeBirthDate(month, day) ? REDACTED_BODY_VALUE : match2
559
- );
560
- out = out.replace(BRN_CANDIDATE_PATTERN, (match2) => {
561
- const digits = match2.replace(/-/g, "");
562
- return passesBrnChecksum(digits) ? REDACTED_BODY_VALUE : match2;
563
- });
564
- out = out.replace(KR_MOBILE_PATTERN, REDACTED_BODY_VALUE);
565
- return out === text ? text : out;
566
- }
567
- function redactUrlSecrets(url) {
568
- if (!url) return url;
569
- const queryStart = url.indexOf("?");
570
- const fragmentStart = url.indexOf("#");
571
- if (queryStart < 0 && fragmentStart < 0) {
572
- return redactSensitiveValues(url);
573
- }
574
- const hasQuery = queryStart >= 0 && (fragmentStart < 0 || queryStart < fragmentStart);
575
- const base = url.slice(0, hasQuery ? queryStart : fragmentStart);
576
- const query = hasQuery ? url.slice(queryStart + 1, fragmentStart >= 0 ? fragmentStart : void 0) : "";
577
- const fragment = fragmentStart >= 0 ? url.slice(fragmentStart + 1) : "";
578
- const redactedQuery = redactKeyValuePairs(query);
579
- const redactedFragment = fragment.includes("=") ? redactKeyValuePairs(fragment) : fragment;
580
- const redactedBase = redactSensitiveValues(base);
581
- if (redactedBase === base && redactedQuery === query && redactedFragment === fragment) {
582
- return url;
583
- }
584
- return redactedBase + (hasQuery ? `?${redactedQuery}` : "") + (fragmentStart >= 0 ? `#${redactedFragment}` : "");
585
- }
586
- function redactSensitiveText(text) {
587
- if (!text) return text;
588
- return redactSensitiveValues(redactUrlSecrets(redactSensitiveBody(text)));
589
- }
590
640
  const META_EVENT_TYPE = 4;
591
641
  const FULL_SNAPSHOT_EVENT_TYPE = 2;
592
642
  const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
@@ -667,7 +717,13 @@ const RRWEB_VERSION = "2.1.0";
667
717
  const SESSION_REPLAY_WINDOW_MS = 12e4;
668
718
  const SESSION_REPLAY_CHECKOUT_MS = 6e4;
669
719
  const REPLAY_MASK_VALUE = "[redacted]";
670
- function maskReplayInput(text) {
720
+ const UNMASKED_INPUT_TYPES = /* @__PURE__ */ new Set(["range", "color", "checkbox", "radio"]);
721
+ function maskReplayInput(text, element) {
722
+ var _a;
723
+ const type = (_a = element == null ? void 0 : element.type) == null ? void 0 : _a.toLowerCase();
724
+ if (type && UNMASKED_INPUT_TYPES.has(type)) {
725
+ return text;
726
+ }
671
727
  return text ? REPLAY_MASK_VALUE : text;
672
728
  }
673
729
  function createMaskTextFn(fullMaskSelector, unmaskSelector) {
@@ -683,19 +739,12 @@ function createMaskTextFn(fullMaskSelector, unmaskSelector) {
683
739
  };
684
740
  }
685
741
  const IDLE_CHECKOUT_POLL_MS = 5e3;
686
- function spanSeconds(segments) {
687
- var _a, _b;
688
- const events = flattenEvents(segments);
689
- const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
690
- const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
691
- if (first === void 0 || last === void 0) return 0;
692
- return Math.round((last - first) / 1e3);
693
- }
694
742
  class RingBufferRecorder {
695
743
  constructor(options = {}) {
696
744
  this.options = options;
697
745
  this.segments = createSegments();
698
746
  this.stopFn = null;
747
+ this.stopCanvasTracking = null;
699
748
  this.idleCheckoutTimer = null;
700
749
  this.visibilityListener = null;
701
750
  this.tailCutoff = null;
@@ -746,10 +795,13 @@ class RingBufferRecorder {
746
795
  this.segments = createSegments();
747
796
  const stop = recordFn({
748
797
  emit: (event, isCheckout) => {
749
- var _a, _b;
798
+ var _a, _b, _c;
750
799
  this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
751
800
  this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
752
- (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a);
801
+ if (isCheckout) {
802
+ (_a = this.stopCanvasTracking) == null ? void 0 : _a.captureBaseline();
803
+ }
804
+ (_c = (_b = this.options).onChange) == null ? void 0 : _c.call(_b);
753
805
  },
754
806
  checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
755
807
  maskAllInputs: cfg.maskAllInputs,
@@ -761,6 +813,9 @@ class RingBufferRecorder {
761
813
  // rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
762
814
  });
763
815
  this.stopFn = typeof stop === "function" ? stop : null;
816
+ if (this.options.trackCanvasInteractions !== false) {
817
+ this.stopCanvasTracking = startCanvasInteractionTracking();
818
+ }
764
819
  this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
765
820
  if (typeof document !== "undefined") {
766
821
  this.visibilityListener = () => this.forceCheckoutIfIdle();
@@ -768,9 +823,11 @@ class RingBufferRecorder {
768
823
  }
769
824
  }
770
825
  stop() {
771
- var _a;
826
+ var _a, _b;
772
827
  (_a = this.stopFn) == null ? void 0 : _a.call(this);
773
828
  this.stopFn = null;
829
+ (_b = this.stopCanvasTracking) == null ? void 0 : _b.call(this);
830
+ this.stopCanvasTracking = null;
774
831
  if (this.idleCheckoutTimer !== null) {
775
832
  clearInterval(this.idleCheckoutTimer);
776
833
  this.idleCheckoutTimer = null;
@@ -837,7 +894,6 @@ class RingBufferRecorder {
837
894
  return serializeSnapshot({
838
895
  segments,
839
896
  redactEvents: redactReplayEvents,
840
- windowSeconds: spanSeconds(segments),
841
897
  rrwebVersion: RRWEB_VERSION,
842
898
  privacyConfigVersion: cfg.version
843
899
  });
@@ -870,17 +926,31 @@ function withoutBody(log) {
870
926
  }
871
927
  return stripped;
872
928
  }
929
+ function withoutFrameData(connection) {
930
+ if (connection.frames.length === 0) return connection;
931
+ return {
932
+ ...connection,
933
+ frames: connection.frames.map((frame) => {
934
+ if (frame.data === void 0) return frame;
935
+ const stripped = { ...frame };
936
+ delete stripped.data;
937
+ return stripped;
938
+ })
939
+ };
940
+ }
873
941
  function trimLogBackupPayload(payload, budgetBytes = LOG_BACKUP_BUDGET_BYTES) {
942
+ var _a;
874
943
  if (payloadSizeBytes(payload) <= budgetBytes) return payload;
875
944
  let trimmed = {
876
945
  network: payload.network.map(withoutBody),
877
- console: payload.console
946
+ console: payload.console,
947
+ socket: (_a = payload.socket) == null ? void 0 : _a.map(withoutFrameData)
878
948
  };
879
949
  while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.network.length > 0) {
880
- trimmed = { network: trimmed.network.slice(1), console: trimmed.console };
950
+ trimmed = { ...trimmed, network: trimmed.network.slice(1) };
881
951
  }
882
952
  while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.console.length > 0) {
883
- trimmed = { network: trimmed.network, console: trimmed.console.slice(1) };
953
+ trimmed = { ...trimmed, console: trimmed.console.slice(1) };
884
954
  }
885
955
  return trimmed;
886
956
  }
@@ -1029,13 +1099,29 @@ const DB_NAME = "issue-sticker-replay";
1029
1099
  const DB_VERSION = 1;
1030
1100
  const STORE_NAME = "segments";
1031
1101
  const REPLAY_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
1032
- const REPLAY_BACKUP_EVENT_BUDGET = 12e3;
1033
- function countEvents(segments) {
1034
- return segments.reduce((total, segment) => total + segment.events.length, 0);
1102
+ const REPLAY_BACKUP_SIZE_BUDGET = 32 * 1024 * 1024;
1103
+ const eventSizes = /* @__PURE__ */ new WeakMap();
1104
+ function sizeOfEvent(event) {
1105
+ const cached = eventSizes.get(event);
1106
+ if (cached !== void 0) return cached;
1107
+ let size;
1108
+ try {
1109
+ size = JSON.stringify(event).length;
1110
+ } catch {
1111
+ size = Number.MAX_SAFE_INTEGER;
1112
+ }
1113
+ eventSizes.set(event, size);
1114
+ return size;
1115
+ }
1116
+ function sizeOfSegments(segments) {
1117
+ return segments.reduce(
1118
+ (total, segment) => total + segment.events.reduce((sum, event) => sum + sizeOfEvent(event), 0),
1119
+ 0
1120
+ );
1035
1121
  }
1036
- function trimReplayBackup(segments, eventBudget = REPLAY_BACKUP_EVENT_BUDGET) {
1122
+ function trimReplayBackup(segments, sizeBudget = REPLAY_BACKUP_SIZE_BUDGET) {
1037
1123
  let trimmed = segments;
1038
- while (trimmed.length > 1 && countEvents(trimmed) > eventBudget) {
1124
+ while (trimmed.length > 1 && sizeOfSegments(trimmed) > sizeBudget) {
1039
1125
  trimmed = trimmed.slice(1);
1040
1126
  }
1041
1127
  return trimmed;
@@ -1236,6 +1322,8 @@ function createReplaySession(options = {}) {
1236
1322
  backup.schedule();
1237
1323
  };
1238
1324
  const recorder = options.recorder ?? new RingBufferRecorder({
1325
+ ...options.trackCanvasInteractions === false ? { trackCanvasInteractions: false } : {},
1326
+ ...options.windowMs === void 0 ? {} : { windowMs: options.windowMs },
1239
1327
  privacyConfig: options.privacyConfig,
1240
1328
  onChange: scheduleBackup
1241
1329
  });
@@ -1318,7 +1406,11 @@ class SessionReplayCaptureController {
1318
1406
  this.setSnapshot({ available: false, canAttach: false, error: null });
1319
1407
  return;
1320
1408
  }
1321
- const recorder = this.createRecorder((sessionReplay == null ? void 0 : sessionReplay.privacyConfig) ?? void 0);
1409
+ const windowSeconds = sessionReplay == null ? void 0 : sessionReplay.windowSeconds;
1410
+ const recorder = this.createRecorder({
1411
+ ...(sessionReplay == null ? void 0 : sessionReplay.privacyConfig) ? { privacyConfig: sessionReplay.privacyConfig } : {},
1412
+ ...typeof windowSeconds === "number" && windowSeconds > 0 ? { windowMs: windowSeconds * 1e3 } : {}
1413
+ });
1322
1414
  recorder.start();
1323
1415
  this.recorder = recorder;
1324
1416
  this.setSnapshot({ available: true, canAttach, error: null });
@@ -1354,8 +1446,8 @@ class SessionReplayCaptureController {
1354
1446
  this.stopRecorder();
1355
1447
  this.listeners.clear();
1356
1448
  }
1357
- createRecorder(privacyConfig) {
1358
- return this.options.createRecorder ? this.options.createRecorder(privacyConfig) : createReplaySession({ privacyConfig });
1449
+ createRecorder(config) {
1450
+ return this.options.createRecorder ? this.options.createRecorder(config) : createReplaySession(config);
1359
1451
  }
1360
1452
  stopRecorder() {
1361
1453
  var _a;
@@ -1370,179 +1462,6 @@ class SessionReplayCaptureController {
1370
1462
  this.listeners.forEach((listener) => listener(this.snapshot));
1371
1463
  }
1372
1464
  }
1373
- function forEachElementDeep(root, callback) {
1374
- if (root instanceof Element) {
1375
- callback(root);
1376
- if (root.shadowRoot) {
1377
- forEachElementDeep(root.shadowRoot, callback);
1378
- }
1379
- }
1380
- Array.from(root.children).forEach((child) => {
1381
- forEachElementDeep(child, callback);
1382
- });
1383
- }
1384
- const SCREENSHOT_FULL_MASK_SELECTOR = SESSION_REPLAY_FULL_MASK_SELECTOR;
1385
- const SCREENSHOT_BLOCK_SELECTOR = SESSION_REPLAY_BLOCK_SELECTOR;
1386
- const SCREENSHOT_MASK_BACKGROUND = "rgba(148, 148, 148, 0.28)";
1387
- const SCREENSHOT_BLOCK_BACKGROUND = "rgba(148, 148, 148, 0.85)";
1388
- const MASK_LABEL_WIDTH = 44;
1389
- const MASK_LABEL_HEIGHT = 16;
1390
- const MASK_LABEL_IMAGE = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
1391
- `<svg xmlns="http://www.w3.org/2000/svg" width="${MASK_LABEL_WIDTH}" height="${MASK_LABEL_HEIGHT}" viewBox="0 0 ${MASK_LABEL_WIDTH} ${MASK_LABEL_HEIGHT}"><text x="${MASK_LABEL_WIDTH / 2}" y="12" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="600" fill="#262626">가려짐</text></svg>`
1392
- )}")`;
1393
- const MASK_LABEL_MIN_WIDTH = MASK_LABEL_WIDTH + 8;
1394
- const MASK_LABEL_MIN_HEIGHT = MASK_LABEL_HEIGHT + 8;
1395
- const TEXT_HIDING_PROPERTIES = [
1396
- ["color", "transparent"],
1397
- ["-webkit-text-fill-color", "transparent"],
1398
- ["text-shadow", "none"],
1399
- ["fill", "transparent"]
1400
- ];
1401
- const REPLACED_MEDIA_SELECTOR = "img, svg, video, canvas, picture, iframe, object, embed";
1402
- const VALUE_ELEMENT_SELECTOR = "input, textarea, select";
1403
- const SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
1404
- // 자격증명
1405
- "current-password",
1406
- "new-password",
1407
- "one-time-code",
1408
- // 결제
1409
- "cc-number",
1410
- "cc-exp",
1411
- "cc-exp-month",
1412
- "cc-exp-year",
1413
- "cc-csc",
1414
- "cc-name",
1415
- "cc-type",
1416
- "cc-given-name",
1417
- "cc-additional-name",
1418
- "cc-family-name",
1419
- // 신원
1420
- "name",
1421
- "given-name",
1422
- "additional-name",
1423
- "family-name",
1424
- "honorific-prefix",
1425
- "honorific-suffix",
1426
- "nickname",
1427
- "bday",
1428
- "bday-day",
1429
- "bday-month",
1430
- "bday-year",
1431
- "sex",
1432
- // 주소
1433
- "street-address",
1434
- "address-line1",
1435
- "address-line2",
1436
- "address-line3",
1437
- "postal-code",
1438
- // 연락처
1439
- "tel",
1440
- "tel-national",
1441
- "tel-local",
1442
- "tel-area-code",
1443
- "email",
1444
- "impp"
1445
- ]);
1446
- function redactScreenshotText(text) {
1447
- return redactEmails(redactSensitiveValues(text));
1448
- }
1449
- function matchesSafely(element, selector) {
1450
- try {
1451
- return element.matches(selector);
1452
- } catch {
1453
- return false;
1454
- }
1455
- }
1456
- function closestSafely(element, selector) {
1457
- if (!element) return null;
1458
- try {
1459
- return element.closest(selector);
1460
- } catch {
1461
- return null;
1462
- }
1463
- }
1464
- function hasSensitiveValueHint(element) {
1465
- var _a;
1466
- if (((_a = element.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "password") {
1467
- return true;
1468
- }
1469
- const autocomplete = element.getAttribute("autocomplete");
1470
- if (!autocomplete) return false;
1471
- return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
1472
- }
1473
- const overwriteProperty = (element, name, value) => {
1474
- const style = element.style;
1475
- style == null ? void 0 : style.setProperty(name, value, "important");
1476
- };
1477
- function coverMaskedRegion(region, setProperty) {
1478
- setProperty(region, "background-color", SCREENSHOT_MASK_BACKGROUND);
1479
- forEachElementDeep(region, (node) => {
1480
- if (!isElementFullyMasked(node)) return;
1481
- TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(node, name, value));
1482
- if (matchesSafely(node, REPLACED_MEDIA_SELECTOR)) {
1483
- setProperty(node, "visibility", "hidden");
1484
- }
1485
- });
1486
- }
1487
- function parsePixels(value) {
1488
- const parsed = Number.parseFloat(value ?? "");
1489
- return Number.isFinite(parsed) ? parsed : 0;
1490
- }
1491
- function fitsMaskLabel(region) {
1492
- const rect = region.getBoundingClientRect();
1493
- const style = region.style;
1494
- const width = rect.width || parsePixels(style == null ? void 0 : style.width);
1495
- const height = rect.height || parsePixels(style == null ? void 0 : style.height);
1496
- return width >= MASK_LABEL_MIN_WIDTH && height >= MASK_LABEL_MIN_HEIGHT;
1497
- }
1498
- function coverBlockedRegion(region, setProperty) {
1499
- const labeled = fitsMaskLabel(region);
1500
- setProperty(region, "visibility", "visible");
1501
- setProperty(region, "background-color", SCREENSHOT_BLOCK_BACKGROUND);
1502
- setProperty(region, "background-image", labeled ? MASK_LABEL_IMAGE : "none");
1503
- if (labeled) {
1504
- setProperty(region, "background-repeat", "no-repeat");
1505
- setProperty(region, "background-position", "center");
1506
- setProperty(region, "background-size", "auto");
1507
- }
1508
- TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(region, name, value));
1509
- setProperty(region, "pointer-events", "none");
1510
- forEachElementDeep(region, (descendant) => {
1511
- if (descendant !== region) {
1512
- setProperty(descendant, "visibility", "hidden");
1513
- }
1514
- });
1515
- }
1516
- function coverPrivateRegions(root, setProperty) {
1517
- const blocked = [];
1518
- const masked = [];
1519
- forEachElementDeep(root, (element) => {
1520
- if (matchesSafely(element, SCREENSHOT_BLOCK_SELECTOR)) {
1521
- blocked.push(element);
1522
- return;
1523
- }
1524
- if (matchesSafely(element, SCREENSHOT_FULL_MASK_SELECTOR) && isElementFullyMasked(element)) {
1525
- masked.push(element);
1526
- }
1527
- });
1528
- masked.forEach((region) => coverMaskedRegion(region, setProperty));
1529
- blocked.forEach((region) => coverBlockedRegion(region, setProperty));
1530
- }
1531
- function applyScreenshotPrivacyMask(clonedRoot) {
1532
- coverPrivateRegions(clonedRoot, overwriteProperty);
1533
- const walker = clonedRoot.ownerDocument.createTreeWalker(clonedRoot, NodeFilter.SHOW_TEXT);
1534
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1535
- const text = node.nodeValue;
1536
- if (!text) continue;
1537
- node.nodeValue = redactScreenshotText(text);
1538
- }
1539
- clonedRoot.querySelectorAll(VALUE_ELEMENT_SELECTOR).forEach((element) => {
1540
- const value = element.getAttribute("value");
1541
- if (!value) return;
1542
- const hidden = closestSafely(element, SCREENSHOT_BLOCK_SELECTOR) !== null || isElementFullyMasked(element) || hasSensitiveValueHint(element);
1543
- element.setAttribute("value", hidden ? "" : redactScreenshotText(value));
1544
- });
1545
- }
1546
1465
  const SCREENSHOT_DATA_URL_QUALITY = 0.95;
1547
1466
  let nextCaptureRunId = 0;
1548
1467
  function createCaptureAttributeNames() {
@@ -1761,10 +1680,18 @@ function blobToDataUrl(blob) {
1761
1680
  reader.readAsDataURL(blob);
1762
1681
  });
1763
1682
  }
1683
+ function captureOrigin(element) {
1684
+ if (element === document.documentElement || element === document.body) {
1685
+ return { x: 0, y: 0 };
1686
+ }
1687
+ const rect = element.getBoundingClientRect();
1688
+ return { x: rect.left, y: rect.top };
1689
+ }
1764
1690
  async function captureWithModernScreenshot(options) {
1765
1691
  const { domToBlob } = await loadModernScreenshot();
1766
1692
  const { element, excludeSelectors = [], width, height, scale = getCaptureScale() } = options;
1767
1693
  await waitForDocumentFonts();
1694
+ const canvasFrames = await grabCanvasFrames(element, excludeSelectors);
1768
1695
  const attributeNames = createCaptureAttributeNames();
1769
1696
  const stickySnapshot = createModernStickySnapshot(element, attributeNames);
1770
1697
  const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors, attributeNames);
@@ -1791,8 +1718,21 @@ async function captureWithModernScreenshot(options) {
1791
1718
  if (height) {
1792
1719
  captureOptions.height = height;
1793
1720
  }
1794
- return await blobToDataUrl(await domToBlob(element, captureOptions));
1721
+ const captured = await domToBlob(element, captureOptions);
1722
+ if (canvasFrames.length === 0) {
1723
+ return await blobToDataUrl(captured);
1724
+ }
1725
+ return await blobToDataUrl(
1726
+ await overlayCanvasFrames(
1727
+ captured,
1728
+ canvasFrames,
1729
+ captureOrigin(element),
1730
+ width ?? element.getBoundingClientRect().width,
1731
+ SCREENSHOT_DATA_URL_QUALITY
1732
+ )
1733
+ );
1795
1734
  } finally {
1735
+ canvasFrames.forEach((frame) => frame.bitmap.close());
1796
1736
  excludeSnapshot.cleanup();
1797
1737
  stickySnapshot.cleanup();
1798
1738
  }
@@ -1877,7 +1817,7 @@ async function calculateSha256Hex(file) {
1877
1817
  }
1878
1818
  function normalizeUploadMimeType(mimeType, mediaType) {
1879
1819
  var _a;
1880
- const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" ? "application/gzip" : "video/webm";
1820
+ const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" || mediaType === "socket" ? "application/gzip" : "video/webm";
1881
1821
  const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
1882
1822
  return normalized && normalized.length > 0 ? normalized : fallback;
1883
1823
  }
@@ -4188,8 +4128,8 @@ function pristineFetch(input, init) {
4188
4128
  }
4189
4129
  return typeof window !== "undefined" ? window.fetch(input, init) : fetch(input, init);
4190
4130
  }
4191
- const INSTRUMENTATION_FAILURE_LIMIT = 5;
4192
- const REPATCH_LIMIT = 2;
4131
+ const INSTRUMENTATION_FAILURE_LIMIT$1 = 5;
4132
+ const REPATCH_LIMIT$1 = 2;
4193
4133
  function setInternalField(target, key, value) {
4194
4134
  Object.defineProperty(target, key, {
4195
4135
  value,
@@ -4286,7 +4226,7 @@ function omitTooLargeResponseBody(sizeBytes) {
4286
4226
  responseBodySizeBytes: sizeBytes
4287
4227
  };
4288
4228
  }
4289
- function bytesToBase64(bytes) {
4229
+ function bytesToBase64$1(bytes) {
4290
4230
  let binary = "";
4291
4231
  const chunkSize = 32768;
4292
4232
  for (let offset = 0; offset < bytes.length; offset += chunkSize) {
@@ -4296,7 +4236,7 @@ function bytesToBase64(bytes) {
4296
4236
  return btoa(binary);
4297
4237
  }
4298
4238
  async function arrayBufferToBase64(arrayBuffer) {
4299
- return bytesToBase64(new Uint8Array(arrayBuffer));
4239
+ return bytesToBase64$1(new Uint8Array(arrayBuffer));
4300
4240
  }
4301
4241
  function getResponseDetailLevel(response) {
4302
4242
  if (response.type === "opaque" || response.type === "opaqueredirect") return "opaque";
@@ -4413,7 +4353,7 @@ class NetworkCollector {
4413
4353
  ensurePatched() {
4414
4354
  if (!this.running) return;
4415
4355
  if (window.fetch === this.patchedFetch) return;
4416
- if (this.repatchCount >= REPATCH_LIMIT) return;
4356
+ if (this.repatchCount >= REPATCH_LIMIT$1) return;
4417
4357
  this.repatchCount += 1;
4418
4358
  this.interceptFetch();
4419
4359
  }
@@ -4459,7 +4399,7 @@ class NetworkCollector {
4459
4399
  */
4460
4400
  recordInstrumentationFailure() {
4461
4401
  this.instrumentationFailures += 1;
4462
- if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT) {
4402
+ if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT$1) {
4463
4403
  this.stop();
4464
4404
  }
4465
4405
  }
@@ -4681,6 +4621,422 @@ class NetworkCollector {
4681
4621
  this.patchedXHR = null;
4682
4622
  }
4683
4623
  }
4624
+ const SOCKET_FRAME_WINDOW_MS = 12e4;
4625
+ const SOCKET_FRAME_BUDGET_BYTES = 2 * 1024 * 1024;
4626
+ const SOCKET_FRAME_MAX_BYTES = 8 * 1024;
4627
+ const SOCKET_CONNECTION_LIMIT = 32;
4628
+ const SOCKET_TRANSCRIPT_MAX_BYTES = 5 * 1024 * 1024;
4629
+ const INSTRUMENTATION_FAILURE_LIMIT = 5;
4630
+ const REPATCH_LIMIT = 2;
4631
+ function toComparableOrigin(url) {
4632
+ return url.replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://");
4633
+ }
4634
+ function bytesToBase64(bytes) {
4635
+ let binary = "";
4636
+ for (const byte of bytes) binary += String.fromCharCode(byte);
4637
+ return btoa(binary);
4638
+ }
4639
+ function toFrameData(data, dir) {
4640
+ if (typeof data === "string") {
4641
+ return { opcode: "text", size: new TextEncoder().encode(data).length, text: data };
4642
+ }
4643
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
4644
+ return { opcode: "binary", size: data.size, bytes: new Uint8Array(0) };
4645
+ }
4646
+ if (data instanceof ArrayBuffer) {
4647
+ const bytes = new Uint8Array(data);
4648
+ return { opcode: "binary", size: bytes.length, bytes };
4649
+ }
4650
+ if (ArrayBuffer.isView(data)) {
4651
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
4652
+ return { opcode: "binary", size: bytes.length, bytes };
4653
+ }
4654
+ if (dir === "send") {
4655
+ const text = String(data);
4656
+ return { opcode: "text", size: new TextEncoder().encode(text).length, text };
4657
+ }
4658
+ throw new TypeError("WebSocketCollector: 지원하지 않는 프레임 데이터 형식");
4659
+ }
4660
+ const MASK_PRESLICE_MARGIN_BYTES = SOCKET_FRAME_MAX_BYTES * 4;
4661
+ function bytesNeededForBase64Length(charLength) {
4662
+ return Math.ceil(charLength * 3 / 4) + 3;
4663
+ }
4664
+ function maskFrameText(text) {
4665
+ return redactSensitiveValues(redactSensitiveBody(text));
4666
+ }
4667
+ class WebSocketCollector {
4668
+ constructor(apiBaseUrl, options) {
4669
+ this.connections = [];
4670
+ this.originalWebSocket = null;
4671
+ this.patchedWebSocket = null;
4672
+ this.originalSend = null;
4673
+ this.patchedSend = null;
4674
+ this.running = false;
4675
+ this.nextId = 1;
4676
+ this.totalBytes = 0;
4677
+ this.instrumentationFailures = 0;
4678
+ this.repatchCount = 0;
4679
+ this.sink = null;
4680
+ this.logByConnectionId = /* @__PURE__ */ new Map();
4681
+ this.apiBaseUrl = typeof apiBaseUrl === "string" && apiBaseUrl.trim().length > 0 ? apiBaseUrl.trim() : null;
4682
+ this.onChange = (options == null ? void 0 : options.onChange) ?? null;
4683
+ }
4684
+ start() {
4685
+ if (this.running) return;
4686
+ if (typeof globalThis.WebSocket !== "function") return;
4687
+ this.running = true;
4688
+ this.instrumentationFailures = 0;
4689
+ this.patch();
4690
+ }
4691
+ stop() {
4692
+ if (!this.running) return;
4693
+ this.running = false;
4694
+ this.unpatch();
4695
+ }
4696
+ /**
4697
+ * 우리 래퍼가 아직 걸려 있는지 확인하고, 아니면 다시 붙인다.
4698
+ *
4699
+ * NetworkCollector.ensurePatched 와 같은 이유다 — 다른 계측이 `window.WebSocket` 을
4700
+ * 통째로 갈아치우면 수집이 조용히 멈춘다.
4701
+ */
4702
+ ensurePatched() {
4703
+ if (!this.running) return;
4704
+ if (globalThis.WebSocket === this.patchedWebSocket) return;
4705
+ if (this.repatchCount >= REPATCH_LIMIT) return;
4706
+ this.repatchCount += 1;
4707
+ this.patch();
4708
+ }
4709
+ /**
4710
+ * 기존 네트워크 로그 목록에 소켓 연결을 한 줄로 흘려보낸다.
4711
+ *
4712
+ * `NetworkCollector.ingest` 를 그대로 받는다 — 새 읽기 경로를 만들지 않고 로그를 읽는
4713
+ * 세 소비자(SDK content·SDK headless·익스텐션 iife)가 이미 보는 목록에 얹는다.
4714
+ */
4715
+ bindNetworkLogSink(ingest) {
4716
+ this.sink = ingest;
4717
+ }
4718
+ /**
4719
+ * 연결이 열릴 때 줄을 한 번만 만들어 싱크로 내보낸다.
4720
+ *
4721
+ * 이후 프레임·닫힘 갱신은 `syncConnectionLog` 가 이 객체를 그대로 고친다 — 싱크
4722
+ * 소비자는 배열에 새 항목이 늘지 않고 이미 받은 참조가 갱신되는 것만 본다.
4723
+ */
4724
+ emitConnectionLog(connection) {
4725
+ if (!this.sink) return;
4726
+ const log = {
4727
+ url: connection.url,
4728
+ // 핸드셰이크는 GET 으로 나가고 성공이면 101 이다.
4729
+ method: "GET",
4730
+ statusCode: 101,
4731
+ timestamp: connection.openedAt,
4732
+ type: "websocket",
4733
+ socketConnectionId: connection.id,
4734
+ frameCount: 0,
4735
+ frameBytes: 0,
4736
+ droppedFrames: 0
4737
+ };
4738
+ this.logByConnectionId.set(connection.id, log);
4739
+ this.sink(log);
4740
+ }
4741
+ /**
4742
+ * `emitConnectionLog` 가 내보낸 줄을 연결의 최신 상태로 고친다.
4743
+ *
4744
+ * `frameCount` 는 버린 프레임까지 센다("실제로 몇 건 오갔나") — `frameBytes` 는 지금
4745
+ * 보관 중인 프레임의 크기 합이라 기준이 다르다. 버린 프레임의 바이트는 이미 사라졌으니
4746
+ * 더할 수 없다.
4747
+ */
4748
+ syncConnectionLog(connection) {
4749
+ const log = this.logByConnectionId.get(connection.id);
4750
+ if (!log) return;
4751
+ log.frameCount = connection.frames.length + connection.droppedFrames;
4752
+ log.frameBytes = connection.frames.reduce((sum, frame) => sum + frame.size, 0);
4753
+ log.droppedFrames = connection.droppedFrames;
4754
+ if (connection.closeCode != null) log.socketCloseCode = connection.closeCode;
4755
+ }
4756
+ getConnections() {
4757
+ return this.connections.map((connection) => ({
4758
+ ...connection,
4759
+ frames: [...connection.frames]
4760
+ }));
4761
+ }
4762
+ clear() {
4763
+ this.connections = [];
4764
+ this.totalBytes = 0;
4765
+ this.logByConnectionId.clear();
4766
+ }
4767
+ /**
4768
+ * 백업에서 되돌린 연결을 지금 목록 앞에 합친다.
4769
+ *
4770
+ * 합친 결과가 상한을 넘으면 앞쪽(오래된 쪽)부터 잘려나간다 — 그중엔 이미
4771
+ * `logByConnectionId` 에 줄이 등록된 연결(리로드 사이 짧게 살아있던 연결)이 섞여 있을
4772
+ * 수 있다. 맵에서도 같이 지우지 않으면 다시 채워질 일 없는 항목으로 계속 남는다.
4773
+ *
4774
+ * 창·예산도 복원 직후 바로 적용한다 — 안 하면 다음 프레임이 올 때까지 상한 없이
4775
+ * 방치된다. 리로드 직후 되돌린 연결이 정확히 이 경우다.
4776
+ *
4777
+ * **`nextId` 도 여기서 올린다.** `nextId` 는 매 세션 1 에서 시작하는데 복원은 그걸 모르고
4778
+ * 지나간다 — MPA 리로드 뒤 백업에서 `ws-1` 이 돌아오고, 그 문서에서 새로 여는 첫 소켓도
4779
+ * 똑같이 `ws-1` 을 받는다. 전사 파일에 같은 id 가 둘 생기고, 뷰어의
4780
+ * `find((c) => c.id === connectionId)` 가 배열 앞쪽(복원된 옛 연결)을 집어 새 연결을 눌러도
4781
+ * 옛 프레임이 보인다. id 에 세션 접두사를 박는 대안도 있지만, 그러면 MCP 조회의
4782
+ * url·direction 필터가 문자열 그대로 비교하는 기존 `ws-<n>` 형식이 깨진다. 복원된 id 의
4783
+ * 최댓값 다음부터 잇는 쪽이 형식을 그대로 지키면서 diff 도 가장 작다.
4784
+ */
4785
+ restore(connections) {
4786
+ if (connections.length === 0) return;
4787
+ this.connections = [...connections, ...this.connections].slice(-SOCKET_CONNECTION_LIMIT);
4788
+ this.totalBytes = this.sumBytes(this.connections);
4789
+ this.bumpNextIdPast(this.connections);
4790
+ const survivingIds = new Set(this.connections.map((connection) => connection.id));
4791
+ for (const id of this.logByConnectionId.keys()) {
4792
+ if (!survivingIds.has(id)) this.logByConnectionId.delete(id);
4793
+ }
4794
+ this.prune();
4795
+ }
4796
+ /** `ws-<n>` 형식 id 중 가장 큰 번호 다음부터 잇도록 `nextId` 를 올린다. 낮추지는 않는다. */
4797
+ bumpNextIdPast(connections) {
4798
+ for (const connection of connections) {
4799
+ const match2 = /^ws-(\d+)$/.exec(connection.id);
4800
+ if (!match2) continue;
4801
+ const numeric = Number(match2[1]);
4802
+ if (numeric >= this.nextId) this.nextId = numeric + 1;
4803
+ }
4804
+ }
4805
+ sumBytes(connections) {
4806
+ var _a;
4807
+ let sum = 0;
4808
+ for (const connection of connections) {
4809
+ for (const frame of connection.frames) sum += ((_a = frame.data) == null ? void 0 : _a.length) ?? 0;
4810
+ }
4811
+ return sum;
4812
+ }
4813
+ /**
4814
+ * 우리 백엔드로 가는 연결은 레코드를 아예 만들지 않는다.
4815
+ *
4816
+ * `byInstance` 에 등록되지 않으면 이후 `send` 프레임도 자동으로 따라오지 않는다
4817
+ * (patchedSend 가 `byInstance.get` 실패 시 아무것도 기록하지 않는다).
4818
+ */
4819
+ track(socket, url, protocols) {
4820
+ var _a;
4821
+ try {
4822
+ if (isSelfTrafficOrigin(toComparableOrigin(url), this.apiBaseUrl ? [this.apiBaseUrl] : []))
4823
+ return null;
4824
+ const connection = {
4825
+ id: `ws-${this.nextId++}`,
4826
+ url: redactUrlSecrets(url),
4827
+ // 서브프로토콜에 토큰을 싣는 패턴(`new WebSocket(url, [token])`)이 실제로 쓰인다.
4828
+ // 값 형태 스크럽까지 거는 이유는 프레임 본문과 같다.
4829
+ protocol: maskFrameText(Array.isArray(protocols) ? protocols[0] : protocols),
4830
+ openedAt: Date.now(),
4831
+ frames: [],
4832
+ droppedFrames: 0,
4833
+ droppedBytes: 0
4834
+ };
4835
+ this.connections.push(connection);
4836
+ if (this.connections.length > SOCKET_CONNECTION_LIMIT) {
4837
+ const closedIndex = this.connections.findIndex((c2) => c2.closedAt != null);
4838
+ const [evicted] = this.connections.splice(closedIndex >= 0 ? closedIndex : 0, 1);
4839
+ this.totalBytes -= this.sumBytes([evicted]);
4840
+ this.logByConnectionId.delete(evicted.id);
4841
+ }
4842
+ socket.addEventListener("message", (event) => {
4843
+ this.addFrame(connection, "recv", event.data);
4844
+ });
4845
+ socket.addEventListener("close", (event) => {
4846
+ var _a2;
4847
+ if (!this.running) return;
4848
+ try {
4849
+ const closeEvent = event;
4850
+ connection.closedAt = Date.now();
4851
+ connection.closeCode = closeEvent.code;
4852
+ connection.closeReason = closeEvent.reason;
4853
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
4854
+ this.syncConnectionLog(connection);
4855
+ } catch {
4856
+ this.recordInstrumentationFailure();
4857
+ }
4858
+ });
4859
+ socket.addEventListener("error", () => {
4860
+ var _a2;
4861
+ if (!this.running) return;
4862
+ try {
4863
+ connection.errored = true;
4864
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
4865
+ } catch {
4866
+ this.recordInstrumentationFailure();
4867
+ }
4868
+ });
4869
+ (_a = this.onChange) == null ? void 0 : _a.call(this);
4870
+ this.emitConnectionLog(connection);
4871
+ return connection;
4872
+ } catch {
4873
+ this.recordInstrumentationFailure();
4874
+ return null;
4875
+ }
4876
+ }
4877
+ /**
4878
+ * 계측이 스스로 꺼지는 차단기. NetworkCollector.recordInstrumentationFailure 와 같은 규칙이다.
4879
+ *
4880
+ * 계측에서 난 예외가 반복된다는 건 우리가 이해하지 못하는 환경에 들어와 있다는 뜻이다.
4881
+ * 고객 페이지에 계속 손대는 것보다 패치를 걷어내고 로그를 포기하는 쪽이 안전하다.
4882
+ */
4883
+ recordInstrumentationFailure() {
4884
+ this.instrumentationFailures += 1;
4885
+ if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT) {
4886
+ this.stop();
4887
+ }
4888
+ }
4889
+ addFrame(connection, dir, data) {
4890
+ var _a;
4891
+ if (!this.running) return;
4892
+ try {
4893
+ const captured = toFrameData(data, dir);
4894
+ const truncated = captured.size > SOCKET_FRAME_MAX_BYTES;
4895
+ const frameData = this.buildFrameData(captured, truncated);
4896
+ connection.frames.push({
4897
+ t: Date.now(),
4898
+ dir,
4899
+ opcode: captured.opcode,
4900
+ size: captured.size,
4901
+ data: frameData,
4902
+ ...truncated ? { truncated: true } : {}
4903
+ });
4904
+ this.totalBytes += frameData.length;
4905
+ this.prune();
4906
+ (_a = this.onChange) == null ? void 0 : _a.call(this);
4907
+ this.syncConnectionLog(connection);
4908
+ } catch {
4909
+ this.recordInstrumentationFailure();
4910
+ }
4911
+ }
4912
+ /**
4913
+ * 프레임 본문을 마스킹·절단해 저장용 문자열로 만든다.
4914
+ *
4915
+ * **상한 아래면 지금까지와 완전히 같다** — 아래 두 분기 모두 `truncated` 가 false 면
4916
+ * 원본 그대로 마스킹만 거치고 절단 코드를 타지 않는다.
4917
+ *
4918
+ * 상한을 넘을 때만 순서가 바뀐다: 절단은 항상 크기(`byteLength`/`Blob.size`/문자열
4919
+ * 길이)를 이미 아는 값으로 먼저 가늠하고, **변환(base64)·마스킹은 그 뒤 필요한 만큼만**
4920
+ * 돌린다. 텍스트는 마스킹이 끝난 뒤 최종 상한으로 한 번 더 자른다 — 마스킹이 앞부분을
4921
+ * 줄이거나 늘릴 수 있어서다(`MASK_PRESLICE_MARGIN_BYTES` 주석 참조).
4922
+ */
4923
+ buildFrameData(captured, truncated) {
4924
+ if (captured.opcode === "binary") {
4925
+ if (!truncated) return bytesToBase64(captured.bytes);
4926
+ const bytesNeeded = Math.min(
4927
+ captured.bytes.length,
4928
+ bytesNeededForBase64Length(SOCKET_FRAME_MAX_BYTES)
4929
+ );
4930
+ return bytesToBase64(captured.bytes.subarray(0, bytesNeeded)).slice(
4931
+ 0,
4932
+ SOCKET_FRAME_MAX_BYTES
4933
+ );
4934
+ }
4935
+ if (!truncated) return maskFrameText(captured.text);
4936
+ const preSliced = captured.text.slice(0, MASK_PRESLICE_MARGIN_BYTES);
4937
+ return maskFrameText(preSliced).slice(0, SOCKET_FRAME_MAX_BYTES);
4938
+ }
4939
+ /**
4940
+ * 시간 창 밖 프레임과 전역 바이트 예산 초과분을 버린다.
4941
+ *
4942
+ * 창 정리는 연결별로 본다 — 각 연결의 프레임은 시간순이라 앞에서부터 잘라내면 된다.
4943
+ * 예산은 전역이라 연결이 여럿이면 가장 오래된 프레임을 가진 연결부터 줄인다.
4944
+ */
4945
+ prune() {
4946
+ var _a, _b, _c;
4947
+ const cutoff = Date.now() - SOCKET_FRAME_WINDOW_MS;
4948
+ const touched = /* @__PURE__ */ new Set();
4949
+ for (const connection of this.connections) {
4950
+ let removed = 0;
4951
+ let removedBytes = 0;
4952
+ while (connection.frames.length > 0 && connection.frames[0].t < cutoff) {
4953
+ const [frame] = connection.frames.splice(0, 1);
4954
+ removed += 1;
4955
+ removedBytes += ((_a = frame.data) == null ? void 0 : _a.length) ?? 0;
4956
+ }
4957
+ if (removed > 0) {
4958
+ connection.droppedFrames += removed;
4959
+ connection.droppedBytes += removedBytes;
4960
+ this.totalBytes -= removedBytes;
4961
+ touched.add(connection);
4962
+ }
4963
+ }
4964
+ while (this.totalBytes > SOCKET_FRAME_BUDGET_BYTES) {
4965
+ const oldest = this.connections.filter((connection) => connection.frames.length > 0).sort((a2, b) => a2.frames[0].t - b.frames[0].t)[0];
4966
+ if (!oldest) break;
4967
+ const [frame] = oldest.frames.splice(0, 1);
4968
+ oldest.droppedFrames += 1;
4969
+ oldest.droppedBytes += ((_b = frame.data) == null ? void 0 : _b.length) ?? 0;
4970
+ this.totalBytes -= ((_c = frame.data) == null ? void 0 : _c.length) ?? 0;
4971
+ touched.add(oldest);
4972
+ }
4973
+ for (const connection of touched) this.syncConnectionLog(connection);
4974
+ }
4975
+ patch() {
4976
+ this.originalWebSocket = globalThis.WebSocket;
4977
+ const OriginalWebSocket = this.originalWebSocket;
4978
+ const track = this.track.bind(this);
4979
+ const addFrame = this.addFrame.bind(this);
4980
+ const byInstance = /* @__PURE__ */ new WeakMap();
4981
+ class PatchedWebSocket extends OriginalWebSocket {
4982
+ constructor(url, protocols) {
4983
+ super(url, protocols);
4984
+ const connection = track(this, typeof url === "string" ? url : url.href, protocols);
4985
+ if (connection) byInstance.set(this, connection);
4986
+ }
4987
+ }
4988
+ this.originalSend = OriginalWebSocket.prototype.send;
4989
+ const originalSend = this.originalSend;
4990
+ const patchedSend = function(data) {
4991
+ const connection = byInstance.get(this);
4992
+ if (connection) addFrame(connection, "send", data);
4993
+ return originalSend.call(this, data);
4994
+ };
4995
+ OriginalWebSocket.prototype.send = patchedSend;
4996
+ this.patchedSend = patchedSend;
4997
+ globalThis.WebSocket = PatchedWebSocket;
4998
+ this.patchedWebSocket = PatchedWebSocket;
4999
+ }
5000
+ /**
5001
+ * 우리 래퍼가 아직 현재 값일 때만 되돌린다.
5002
+ *
5003
+ * `NetworkCollector.restoreFetch`/`restoreXHR` 과 같은 규칙 — 무조건 되돌리면 우리
5004
+ * 뒤에 붙은 남의 계측(다른 APM 등)의 래퍼가 통째로 사라진다. 남의 것이 위에 있으면
5005
+ * 우리 래퍼는 체인에 남겨 두고 수집만 멈춘다.
5006
+ */
5007
+ unpatch() {
5008
+ if (this.originalWebSocket && globalThis.WebSocket === this.patchedWebSocket) {
5009
+ globalThis.WebSocket = this.originalWebSocket;
5010
+ }
5011
+ if (this.originalWebSocket && this.originalSend && this.originalWebSocket.prototype.send === this.patchedSend) {
5012
+ this.originalWebSocket.prototype.send = this.originalSend;
5013
+ }
5014
+ this.originalWebSocket = null;
5015
+ this.patchedWebSocket = null;
5016
+ this.originalSend = null;
5017
+ this.patchedSend = null;
5018
+ }
5019
+ }
5020
+ async function sha256Hex(bytes) {
5021
+ const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
5022
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
5023
+ }
5024
+ async function serializeSocketTranscript(connections) {
5025
+ const frameCount = connections.reduce(
5026
+ (sum, connection) => sum + connection.frames.length + connection.droppedFrames,
5027
+ 0
5028
+ );
5029
+ if (connections.length === 0 || frameCount === 0) return null;
5030
+ const json = JSON.stringify({ connections });
5031
+ const payload = gzip_1(json);
5032
+ return {
5033
+ payload,
5034
+ checksumSha256: await sha256Hex(payload),
5035
+ sizeBytes: payload.byteLength,
5036
+ connectionCount: connections.length,
5037
+ frameCount
5038
+ };
5039
+ }
4684
5040
  var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
4685
5041
  NetworkLogType2["MAIN_FRAME"] = "main_frame";
4686
5042
  NetworkLogType2["SUB_FRAME"] = "sub_frame";
@@ -13299,6 +13655,7 @@ function OnboardingOverlay({
13299
13655
  targetRect,
13300
13656
  targetRadius,
13301
13657
  avoidRects,
13658
+ affordanceRect,
13302
13659
  elevateHighlight,
13303
13660
  allowSkip = true,
13304
13661
  className,
@@ -13318,6 +13675,23 @@ function OnboardingOverlay({
13318
13675
  cutoutRect.width / 2,
13319
13676
  cutoutRect.height / 2
13320
13677
  ) : 0;
13678
+ const hasAffordanceTarget = !!step.affordanceTargetId;
13679
+ const affordanceCutout = affordanceRect ? padRect(affordanceRect, MASK_PADDING) : null;
13680
+ const affordanceDiameter = affordanceRect ? Math.max(affordanceRect.width, affordanceRect.height) + MASK_PADDING * 2 : 0;
13681
+ const affordanceCircle = affordanceRect ? (() => {
13682
+ const centerX = (affordanceRect.left + affordanceRect.right) / 2;
13683
+ const centerY = (affordanceRect.top + affordanceRect.bottom) / 2;
13684
+ const radius = affordanceDiameter / 2;
13685
+ return {
13686
+ left: centerX - radius,
13687
+ top: centerY - radius,
13688
+ right: centerX + radius,
13689
+ bottom: centerY + radius,
13690
+ width: affordanceDiameter,
13691
+ height: affordanceDiameter
13692
+ };
13693
+ })() : null;
13694
+ const popoverAnchorRect = hasAffordanceTarget && affordanceCutout ? affordanceCutout : cutoutRect;
13321
13695
  const popoverRef = React.useRef(null);
13322
13696
  const [popoverSize, setPopoverSize] = React.useState(
13323
13697
  null
@@ -13339,17 +13713,20 @@ function OnboardingOverlay({
13339
13713
  }, [step.id, step.body]);
13340
13714
  const effectiveSize = popoverSize && popoverSize.width > 0 && popoverSize.height > 0 ? popoverSize : DEFAULT_POPOVER_SIZE;
13341
13715
  const placement = resolvePopoverPlacement(
13342
- step.position ?? (cutoutRect ? "bottom" : "center"),
13343
- cutoutRect ?? getViewportRect(),
13716
+ step.position ?? (popoverAnchorRect ? "bottom" : "center"),
13717
+ popoverAnchorRect ?? getViewportRect(),
13344
13718
  effectiveSize,
13345
- cutoutRect ? avoidRects ?? [] : []
13719
+ popoverAnchorRect ? avoidRects ?? [] : []
13346
13720
  );
13347
13721
  const { left: popoverLeft, top: popoverTop } = placement;
13348
- const shouldCenterPopover = !cutoutRect && (step.position ?? "center") === "center";
13722
+ const shouldCenterPopover = !popoverAnchorRect && (step.position ?? "center") === "center";
13349
13723
  const maskColor = isTargetInteractionStep(step) ? "rgba(8, 13, 18, 0.74)" : "rgba(8, 13, 18, 0.62)";
13350
13724
  const isPassthrough = !!step.advanceOn;
13351
13725
  const isTargetInteraction = isTargetInteractionStep(step);
13352
13726
  const clickHint = isTargetInteraction ? step.clickHint ?? labels.clickHint : null;
13727
+ const isCutoutInteractionStyled = isTargetInteraction && !hasAffordanceTarget;
13728
+ const showAffordanceEmphasis = isTargetInteraction && hasAffordanceTarget && !!affordanceCircle;
13729
+ const ringCursorRect = !isTargetInteraction ? null : hasAffordanceTarget ? affordanceCircle : cutoutRect;
13353
13730
  return /* @__PURE__ */ jsxs(
13354
13731
  "div",
13355
13732
  {
@@ -13442,7 +13819,7 @@ function OnboardingOverlay({
13442
13819
  "data-testid": "onboarding-cutout-outline",
13443
13820
  className: cn(
13444
13821
  "issue-sticker-onboarding-highlight fixed",
13445
- isTargetInteraction ? "border-2 border-brand" : "border border-white/90"
13822
+ isCutoutInteractionStyled ? "border-2 border-brand" : "border border-white/90"
13446
13823
  ),
13447
13824
  style: {
13448
13825
  left: cutoutRect.left,
@@ -13457,7 +13834,31 @@ function OnboardingOverlay({
13457
13834
  step.id
13458
13835
  )
13459
13836
  ) : null,
13460
- cutoutRect && isTargetInteraction ? renderHighlight(
13837
+ showAffordanceEmphasis && affordanceCircle ? renderHighlight(
13838
+ /* 강조(컷아웃)와 클릭 유도 위치가 분리된 단계의 브랜드 링. 컷아웃 테두리는
13839
+ * 위에서 이미 순수 강조 스타일로 그렸으니, 여기서는 실제로 눌러야 할 요소
13840
+ * 중심에 원을 그린다 — 사각형 컷아웃과 구별돼야 "강조 대상이 아니라 이걸
13841
+ * 누르라"는 뜻이 선다. */
13842
+ /* @__PURE__ */ jsx(
13843
+ "div",
13844
+ {
13845
+ "aria-hidden": "true",
13846
+ "data-testid": "onboarding-affordance-ring",
13847
+ className: "issue-sticker-onboarding-highlight fixed rounded-full border-2 border-brand",
13848
+ style: {
13849
+ left: affordanceCircle.left,
13850
+ top: affordanceCircle.top,
13851
+ width: affordanceCircle.width,
13852
+ height: affordanceCircle.height,
13853
+ borderRadius: 9999,
13854
+ pointerEvents: "none",
13855
+ zIndex: elevateHighlight ? OVERLAY_LAYER_INDEX.runtimeHost : void 0
13856
+ }
13857
+ },
13858
+ `${step.id}-affordance`
13859
+ )
13860
+ ) : null,
13861
+ ringCursorRect ? renderHighlight(
13461
13862
  /* 클릭하는 커서. 테두리만으로는 "봐야 할 곳"까지만 전달되고 "클릭하라"가
13462
13863
  * 전달되지 않는다. 손 모양 반복 동작이 그 자리를 맡는다. */
13463
13864
  /* @__PURE__ */ jsxs(
@@ -13467,8 +13868,8 @@ function OnboardingOverlay({
13467
13868
  "data-testid": "onboarding-cutout-cursor",
13468
13869
  className: "issue-sticker-onboarding-tap fixed",
13469
13870
  style: {
13470
- left: cutoutRect.right - TAP_INSET,
13471
- top: cutoutRect.bottom - TAP_INSET,
13871
+ left: ringCursorRect.right - TAP_INSET,
13872
+ top: ringCursorRect.bottom - TAP_INSET,
13472
13873
  pointerEvents: "none",
13473
13874
  zIndex: elevateHighlight ? OVERLAY_LAYER_INDEX.runtimeHost : void 0
13474
13875
  },
@@ -13705,18 +14106,24 @@ function OnboardingProvider({
13705
14106
  () => activeTargetIds.map((id) => targetMapRef.current.get(id) ?? null).filter((el) => el !== null),
13706
14107
  [activeTargetIds]
13707
14108
  );
14109
+ const activeAffordanceTargetId = activeStep == null ? void 0 : activeStep.affordanceTargetId;
14110
+ const getObservedElements = React.useCallback(() => {
14111
+ const elements = getActiveTargetElements();
14112
+ const affordanceElement = activeAffordanceTargetId ? targetMapRef.current.get(activeAffordanceTargetId) ?? null : null;
14113
+ return affordanceElement ? [...elements, affordanceElement] : elements;
14114
+ }, [activeAffordanceTargetId, getActiveTargetElements]);
13708
14115
  const scheduleLayoutUpdate = React.useCallback(() => {
13709
14116
  clearLayoutFrame();
13710
14117
  frameRef.current = window.requestAnimationFrame(() => {
13711
14118
  frameRef.current = null;
13712
14119
  const nextRect = unionRect(
13713
- getActiveTargetElements().map(getElementRect).filter((r2) => r2 !== null)
14120
+ getObservedElements().map(getElementRect).filter((r2) => r2 !== null)
13714
14121
  );
13715
14122
  if (areRectsEqual(lastRectRef.current, nextRect)) return;
13716
14123
  lastRectRef.current = nextRect;
13717
14124
  setLayoutVersion((version2) => version2 + 1);
13718
14125
  });
13719
- }, [clearLayoutFrame, getActiveTargetElements]);
14126
+ }, [clearLayoutFrame, getObservedElements]);
13720
14127
  const openAtStep = React.useCallback(
13721
14128
  (stepIndex) => {
13722
14129
  var _a;
@@ -13874,7 +14281,7 @@ function OnboardingProvider({
13874
14281
  }, [activeStep, advanceFromTargetClick, goToStep, isActiveRoute, isOpen]);
13875
14282
  React.useEffect(() => {
13876
14283
  if (!isOpen || !isActiveRoute) return;
13877
- const elements = getActiveTargetElements();
14284
+ const elements = getObservedElements();
13878
14285
  if (elements.length === 0) return;
13879
14286
  scheduleLayoutUpdate();
13880
14287
  window.addEventListener("resize", scheduleLayoutUpdate);
@@ -13889,7 +14296,7 @@ function OnboardingProvider({
13889
14296
  };
13890
14297
  }, [
13891
14298
  clearLayoutFrame,
13892
- getActiveTargetElements,
14299
+ getObservedElements,
13893
14300
  isActiveRoute,
13894
14301
  isOpen,
13895
14302
  scheduleLayoutUpdate,
@@ -13902,6 +14309,7 @@ function OnboardingProvider({
13902
14309
  const targetRect = isOpen && isActiveRoute && allTargetsResolved ? unionRect(resolvedTargetRects.filter((r2) => r2 !== null)) : null;
13903
14310
  const targetRadius = activeTargetIds.length === 1 ? getElementRadius(targetMapRef.current.get(activeTargetIds[0]) ?? null) : null;
13904
14311
  const avoidRects = ((activeStep == null ? void 0 : activeStep.avoidTargetIds) ?? []).map((id) => getElementRect(targetMapRef.current.get(id) ?? null)).filter((rect) => rect !== null);
14312
+ const affordanceRect = (activeStep == null ? void 0 : activeStep.affordanceTargetId) ? getElementRect(targetMapRef.current.get(activeStep.affordanceTargetId) ?? null) : null;
13905
14313
  const shouldRenderOverlay = isOpen && activeStep && isActiveRoute && !suppressed && !activeStep.silent && (!stepHasTarget || targetRect);
13906
14314
  const isTargetReady = !stepHasTarget || targetRect !== null;
13907
14315
  React.useEffect(() => {
@@ -13954,6 +14362,7 @@ function OnboardingProvider({
13954
14362
  targetRect,
13955
14363
  targetRadius,
13956
14364
  avoidRects,
14365
+ affordanceRect,
13957
14366
  elevateHighlight,
13958
14367
  allowSkip,
13959
14368
  className,
@@ -14175,6 +14584,20 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
14175
14584
  return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
14176
14585
  });
14177
14586
  }
14587
+ const TRANSCRIPT_CONTENT_TYPE = "application/gzip";
14588
+ async function uploadSocketTranscript(api, snapshot, options = {}) {
14589
+ if (snapshot.sizeBytes > SOCKET_TRANSCRIPT_MAX_BYTES) return null;
14590
+ try {
14591
+ const file = new File([snapshot.payload], "socket-transcript.json.gz", {
14592
+ type: TRANSCRIPT_CONTENT_TYPE
14593
+ });
14594
+ const uploaded = await uploadSignedMediaWithApi(api, file, "socket", options);
14595
+ return { uploadId: uploaded.id };
14596
+ } catch (error) {
14597
+ devLogger.error("socketTranscriptUpload", "소켓 전사 업로드 실패:", error);
14598
+ return null;
14599
+ }
14600
+ }
14178
14601
  function buildExternalUserPayload(user) {
14179
14602
  if (!user) return {};
14180
14603
  return {
@@ -14183,60 +14606,63 @@ function buildExternalUserPayload(user) {
14183
14606
  };
14184
14607
  }
14185
14608
  export {
14186
- getNotionPageMetadata as $,
14187
- getLogBackupSessionId as A,
14188
- createIndexedDbLogBackup as B,
14609
+ Check as $,
14610
+ trimLogBackupPayload as A,
14611
+ createSafeLogBackup as B,
14189
14612
  ConsoleCollector as C,
14190
- PopoverTrigger as D,
14191
- Input as E,
14192
- PopoverContent as F,
14193
- Select$1 as G,
14194
- SelectTrigger as H,
14613
+ getLogBackupSessionId as D,
14614
+ createIndexedDbLogBackup as E,
14615
+ PopoverTrigger as F,
14616
+ Input as G,
14617
+ PopoverContent as H,
14195
14618
  ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
14196
- SelectValue as J,
14197
- SelectContent as K,
14619
+ Select$1 as J,
14620
+ SelectTrigger as K,
14198
14621
  Label as L,
14199
- SelectItem as M,
14622
+ SelectValue as M,
14200
14623
  NetworkCollector as N,
14201
- getJiraMetadata as O,
14624
+ SelectContent as O,
14202
14625
  Popover as P,
14203
- buildJiraIssueFields as Q,
14204
- Button$1 as R,
14626
+ SelectItem as Q,
14627
+ getJiraMetadata as R,
14205
14628
  ScreenRecordingController as S,
14206
- Root2 as T,
14207
- Trigger as U,
14208
- ChevronDown as V,
14209
- Portal as W,
14210
- Content2 as X,
14211
- Check as Y,
14212
- X as Z,
14213
- DatePicker as _,
14629
+ buildJiraIssueFields as T,
14630
+ Button$1 as U,
14631
+ Root2 as V,
14632
+ WebSocketCollector as W,
14633
+ Trigger as X,
14634
+ ChevronDown as Y,
14635
+ Portal as Z,
14636
+ Content2 as _,
14214
14637
  uploadSignedMediaWithApi as a,
14215
- buildNotionFieldViews as a0,
14216
- DirectUploadError as a1,
14217
- toast as a2,
14218
- getSnapshot as a3,
14219
- subscribe as a4,
14220
- getRegisteredUploadId as a5,
14221
- ChevronRight as a6,
14222
- ReactRemoveScroll as a7,
14223
- hideOthers as a8,
14224
- useFocusGuards as a9,
14225
- FocusScope as aa,
14226
- getExposedJiraFields as ab,
14227
- isJiraFieldExposed as ac,
14228
- normalizeExposedJiraFields as ad,
14229
- getPrimaryJiraMapping as ae,
14230
- createCollection as af,
14231
- useDirection as ag,
14232
- getPrimaryNotionMapping as ah,
14233
- buttonVariants as ai,
14234
- findElementBySelector as aj,
14235
- findElementByXPath as ak,
14236
- OnboardingProvider as al,
14237
- useOnboardingTarget as am,
14238
- useOnboardingFlow as an,
14239
- PopoverAnchor as ao,
14638
+ X as a0,
14639
+ DatePicker as a1,
14640
+ getNotionPageMetadata as a2,
14641
+ buildNotionFieldViews as a3,
14642
+ DirectUploadError as a4,
14643
+ toast as a5,
14644
+ getSnapshot as a6,
14645
+ subscribe as a7,
14646
+ getRegisteredUploadId as a8,
14647
+ ChevronRight as a9,
14648
+ ReactRemoveScroll as aa,
14649
+ hideOthers as ab,
14650
+ useFocusGuards as ac,
14651
+ FocusScope as ad,
14652
+ getExposedJiraFields as ae,
14653
+ isJiraFieldExposed as af,
14654
+ normalizeExposedJiraFields as ag,
14655
+ getPrimaryJiraMapping as ah,
14656
+ createCollection as ai,
14657
+ useDirection as aj,
14658
+ getPrimaryNotionMapping as ak,
14659
+ buttonVariants as al,
14660
+ findElementBySelector as am,
14661
+ findElementByXPath as an,
14662
+ OnboardingProvider as ao,
14663
+ useOnboardingTarget as ap,
14664
+ useOnboardingFlow as aq,
14665
+ PopoverAnchor as ar,
14240
14666
  buildJiraCreateFields as b,
14241
14667
  captureWithModernScreenshot as c,
14242
14668
  directUploadToSignedUrl as d,
@@ -14257,9 +14683,9 @@ export {
14257
14683
  selectNetworkLogsForSubmit as s,
14258
14684
  sanitizeString as t,
14259
14685
  uploadScreenshotDataUrls as u,
14260
- getCurrentPageUrl as v,
14261
- SessionReplayCaptureController as w,
14262
- preloadModernScreenshot as x,
14263
- trimLogBackupPayload as y,
14264
- createSafeLogBackup as z
14686
+ serializeSocketTranscript as v,
14687
+ uploadSocketTranscript as w,
14688
+ getCurrentPageUrl as x,
14689
+ SessionReplayCaptureController as y,
14690
+ preloadModernScreenshot as z
14265
14691
  };