issue-sticker 0.8.1 → 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";
@@ -207,98 +207,361 @@ class ScreenRecordingController {
207
207
  this.listeners.forEach((listener) => listener(this.snapshot));
208
208
  }
209
209
  }
210
- const SESSION_REPLAY_BLOCK_CLASS = "rr-block";
211
- const SESSION_REPLAY_MASK_CLASS = "rr-mask";
212
- const SESSION_REPLAY_UNMASK_CLASS = "rr-unmask";
213
- const FOREIGN_BLOCK_SELECTORS = [
214
- // FullStory
215
- ".fs-exclude",
216
- ".fs-block",
217
- // Sentry — ignore 는 의미가 "입력 이벤트를 기록하지 않는다"라 차단과 어긋나지만,
218
- // 빼면 이미 그것에 기대고 있는 고객에게 회귀다. 과잉 마스킹은 안전한 방향이라 둔다.
219
- ".sentry-block",
220
- ".sentry-ignore",
221
- "[data-sentry-block]",
222
- // PostHog
223
- ".ph-no-capture",
224
- // Hotjar
225
- "[data-hj-suppress]",
226
- // OpenReplay
227
- "[data-openreplay-hidden]",
228
- // Highlight.io
229
- ".highlight-block",
230
- ".highlight-ignore",
231
- // Heap
232
- "[data-heap-ignore]",
233
- ".heap-ignore"
234
- ];
235
- const FOREIGN_MASK_SELECTORS = [
236
- // FullStory
237
- ".fs-mask",
238
- // Sentry
239
- ".sentry-mask",
240
- "[data-sentry-mask]",
241
- // LogRocket
242
- "[data-private]",
243
- // Microsoft Clarity
244
- "[data-clarity-mask]",
245
- // Hotjar
246
- "[data-hj-masked]",
247
- // OpenReplay
248
- "[data-openreplay-obscured]",
249
- // Highlight.io
250
- ".highlight-mask",
251
- // ContentSquare
252
- "[data-cs-mask]",
253
- "[data-cs-encrypt]",
254
- // Heap
255
- "[data-heap-redact-text]",
256
- "[data-heap-redact-attributes]",
257
- // Amplitude
258
- "[data-amp-mask]",
259
- // Matomo
260
- "[data-matomo-mask]"
261
- ];
262
- const FOREIGN_UNMASK_SELECTORS = [
263
- ".fs-unmask",
264
- ".sentry-unmask",
265
- "[data-clarity-unmask]",
266
- "[data-amp-unmask]",
267
- "[data-cs-capture]"
268
- ];
269
- const SESSION_REPLAY_FULL_MASK_SELECTOR = [
270
- `.${SESSION_REPLAY_MASK_CLASS}`,
271
- ...FOREIGN_MASK_SELECTORS
272
- ].join(",");
273
- const SESSION_REPLAY_BLOCK_SELECTOR = [
274
- `.${SESSION_REPLAY_BLOCK_CLASS}`,
275
- ...FOREIGN_BLOCK_SELECTORS
276
- ].join(",");
277
- const SESSION_REPLAY_UNMASK_SELECTOR = [
278
- `.${SESSION_REPLAY_UNMASK_CLASS}`,
279
- ...FOREIGN_UNMASK_SELECTORS
280
- ].join(",");
281
- function isElementFullyMasked(element, maskSelector = SESSION_REPLAY_FULL_MASK_SELECTOR, unmaskSelector = SESSION_REPLAY_UNMASK_SELECTOR) {
282
- 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
+ }
283
252
  try {
284
- const nearest = element.closest(`${maskSelector},${unmaskSelector}`);
285
- 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;
286
263
  } catch {
287
- return true;
264
+ return null;
288
265
  }
289
266
  }
290
- const DEFAULT_PRIVACY_CONFIG_V2 = {
291
- version: "v2",
292
- maskAllInputs: true,
293
- maskTextSelector: "*",
294
- fullMaskSelector: SESSION_REPLAY_FULL_MASK_SELECTOR,
295
- unmaskSelector: SESSION_REPLAY_UNMASK_SELECTOR,
296
- blockSelector: SESSION_REPLAY_BLOCK_SELECTOR,
297
- allowCanvasRecording: false
298
- };
299
- function resolvePrivacyConfig(override) {
300
- if (!override) return DEFAULT_PRIVACY_CONFIG_V2;
301
- 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 });
302
565
  }
303
566
  function createSegments() {
304
567
  return [{ events: [] }];
@@ -331,264 +594,49 @@ function trimSegments(segments, windowMs, now, checkoutMs) {
331
594
  }
332
595
  return segments.slice(startIdx);
333
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
+ }
334
605
  function flattenEvents(segments) {
335
606
  return segments.flatMap((s2) => s2.events);
336
607
  }
337
608
  function dropEventsAfter(segments, cutoff) {
338
609
  return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
339
610
  }
340
- async function sha256Hex(bytes) {
611
+ const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
612
+ async function sha256Hex$1(bytes) {
341
613
  const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
342
614
  return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
343
615
  }
344
616
  async function serializeSnapshot(input) {
345
- const events = (input.redactEvents ?? ((e2) => e2))(flattenEvents(input.segments));
346
- const json = JSON.stringify(events);
347
- const payload = gzip_1(json);
348
- 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);
349
630
  return {
350
631
  payload,
351
632
  checksumSha256,
352
- windowSeconds: input.windowSeconds,
633
+ windowSeconds: spanSeconds(segments),
353
634
  eventCount: events.length,
354
635
  sizeBytes: payload.byteLength,
355
636
  rrwebVersion: input.rrwebVersion,
356
637
  privacyConfigVersion: input.privacyConfigVersion
357
638
  };
358
639
  }
359
- const REDACTED_HEADER_VALUE = "[redacted]";
360
- const SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
361
- "authorization",
362
- "proxy-authorization",
363
- "cookie",
364
- "set-cookie",
365
- "x-api-key",
366
- "api-key",
367
- "apikey",
368
- "x-auth-token",
369
- "x-access-token",
370
- "x-refresh-token",
371
- "x-session-token",
372
- "x-csrf-token",
373
- "x-xsrf-token",
374
- "x-amz-security-token",
375
- "x-goog-api-key"
376
- ]);
377
- function isSensitiveHeaderName(headerName) {
378
- return SENSITIVE_HEADER_NAMES.has(headerName.toLowerCase());
379
- }
380
- function redactSensitiveHeaders(headers) {
381
- if (!headers) return headers;
382
- let changed = false;
383
- const redacted = {};
384
- for (const [name, value] of Object.entries(headers)) {
385
- if (value && isSensitiveHeaderName(name)) {
386
- redacted[name] = REDACTED_HEADER_VALUE;
387
- changed = true;
388
- } else {
389
- redacted[name] = value;
390
- }
391
- }
392
- return changed ? redacted : headers;
393
- }
394
- const REDACTED_BODY_VALUE = "[redacted]";
395
- const SENSITIVE_KEY_MARKERS = [
396
- "password",
397
- "passwd",
398
- "pwd",
399
- "secret",
400
- "token",
401
- "apikey",
402
- "privatekey",
403
- "credential",
404
- // 결제·본인확인 계열. 토스페이먼츠 공식 문서의 필드명을 그대로 딴다.
405
- // cardNumber / customerIdentityNumber / businessNumber / accountNumber(가상계좌)
406
- "cardnumber",
407
- "identitynumber",
408
- "businessnumber",
409
- "registrationnumber",
410
- "accountnumber"
411
- ];
412
- const SENSITIVE_KEY_EXACT = /* @__PURE__ */ new Set([
413
- "otp",
414
- "auth",
415
- "authorization",
416
- "bearer",
417
- // 토스페이먼츠. billingKey + customerKey 조합이면 결제를 낼 수 있고, authKey 는 일회성
418
- // 인증 키다(우리 admin 결제 성공 URL 의 쿼리 파라미터로 실제로 실린다).
419
- // paymentKey 는 넣지 않는다 — 시크릿 키 없이는 아무것도 못 하는 식별자이고, 결제 장애
420
- // 진단의 출발점이라 가리면 로그가 쓸모를 잃는다.
421
- "billingkey",
422
- "customerkey",
423
- "authkey"
424
- ]);
425
- const MAX_WALK_DEPTH = 20;
426
- function isSensitiveFieldName(name) {
427
- const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
428
- if (SENSITIVE_KEY_EXACT.has(normalized)) {
429
- return true;
430
- }
431
- return SENSITIVE_KEY_MARKERS.some((marker) => normalized.includes(marker));
432
- }
433
- function redactJsonValue(value, depth) {
434
- if (typeof value === "string") {
435
- const redacted = redactSensitiveValues(value);
436
- return { value: redacted, changed: redacted !== value };
437
- }
438
- if (depth > MAX_WALK_DEPTH || value === null || typeof value !== "object") {
439
- return { value, changed: false };
440
- }
441
- if (Array.isArray(value)) {
442
- let changed2 = false;
443
- const next2 = value.map((item) => {
444
- const result = redactJsonValue(item, depth + 1);
445
- changed2 || (changed2 = result.changed);
446
- return result.value;
447
- });
448
- return changed2 ? { value: next2, changed: changed2 } : { value, changed: changed2 };
449
- }
450
- let changed = false;
451
- const next = {};
452
- for (const [key, item] of Object.entries(value)) {
453
- if (item !== "" && item != null && typeof item !== "boolean" && isSensitiveFieldName(key)) {
454
- next[key] = REDACTED_BODY_VALUE;
455
- changed = true;
456
- continue;
457
- }
458
- const result = redactJsonValue(item, depth + 1);
459
- changed || (changed = result.changed);
460
- next[key] = result.value;
461
- }
462
- return changed ? { value: next, changed } : { value, changed };
463
- }
464
- function redactKeyValuePairs(body) {
465
- let changed = false;
466
- const redacted = body.split("&").map((pair) => {
467
- const separator = pair.indexOf("=");
468
- if (separator <= 0) {
469
- return pair;
470
- }
471
- const rawKey = pair.slice(0, separator);
472
- const rawValue = pair.slice(separator + 1);
473
- if (!rawValue) {
474
- return pair;
475
- }
476
- if (isSensitiveFieldName(decodeURIComponent(rawKey))) {
477
- changed = true;
478
- return `${rawKey}=${encodeURIComponent(REDACTED_BODY_VALUE)}`;
479
- }
480
- const redactedValue = redactSensitiveValues(decodeURIComponent(rawValue));
481
- if (redactedValue === decodeURIComponent(rawValue)) {
482
- return pair;
483
- }
484
- changed = true;
485
- return `${rawKey}=${encodeURIComponent(redactedValue)}`;
486
- }).join("&");
487
- return changed ? redacted : body;
488
- }
489
- function looksLikeFormUrlEncoded(body) {
490
- return /^[^=&\s]+=[^&]*(&[^=&\s]+=[^&]*)*$/.test(body);
491
- }
492
- function redactSensitiveBody(body) {
493
- if (!body) return body;
494
- const trimmed = body.trim();
495
- if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
496
- try {
497
- const parsed = JSON.parse(trimmed);
498
- const { value, changed } = redactJsonValue(parsed, 0);
499
- return changed ? JSON.stringify(value) : body;
500
- } catch {
501
- }
502
- }
503
- if (looksLikeFormUrlEncoded(trimmed)) {
504
- const redacted = redactKeyValuePairs(trimmed);
505
- return redacted === trimmed ? body : redacted;
506
- }
507
- return body;
508
- }
509
- const JWT_PATTERN = /eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{4,}\.[A-Za-z0-9_-]*/g;
510
- const BEARER_PATTERN = /(Bearer\s+)[A-Za-z0-9._~+/-]{12,}=*/gi;
511
- const CARD_CANDIDATE_PATTERN = /\b[2-6](?:[ -]?\d){13,18}\b/g;
512
- const RRN_CANDIDATE_PATTERN = /\b(\d{2})(\d{2})(\d{2})-?([1-8])(\d{6})\b/g;
513
- const BRN_CANDIDATE_PATTERN = /\b(\d{3})-?(\d{2})-?(\d{5})\b/g;
514
- const BRN_WEIGHTS = [1, 3, 7, 1, 3, 7, 1, 3, 5];
515
- const KR_MOBILE_PATTERN = /\b01[016789]-?\d{3,4}-?\d{4}\b/g;
516
- const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
517
- function redactEmails(text) {
518
- if (!text) return text;
519
- const out = text.replace(EMAIL_PATTERN, REDACTED_BODY_VALUE);
520
- return out === text ? text : out;
521
- }
522
- function passesBrnChecksum(digits) {
523
- let sum = 0;
524
- for (let i2 = 0; i2 < 9; i2 += 1) {
525
- sum += (digits.charCodeAt(i2) - 48) * BRN_WEIGHTS[i2];
526
- }
527
- sum += Math.floor((digits.charCodeAt(8) - 48) * 5 / 10);
528
- return (10 - sum % 10) % 10 === digits.charCodeAt(9) - 48;
529
- }
530
- function passesLuhn(digits) {
531
- let sum = 0;
532
- let double = false;
533
- for (let i2 = digits.length - 1; i2 >= 0; i2 -= 1) {
534
- let digit = digits.charCodeAt(i2) - 48;
535
- if (double) {
536
- digit *= 2;
537
- if (digit > 9) digit -= 9;
538
- }
539
- sum += digit;
540
- double = !double;
541
- }
542
- return sum % 10 === 0;
543
- }
544
- function looksLikeBirthDate(month, day) {
545
- const monthValue = Number(month);
546
- const dayValue = Number(day);
547
- return monthValue >= 1 && monthValue <= 12 && dayValue >= 1 && dayValue <= 31;
548
- }
549
- function redactSensitiveValues(text) {
550
- if (!text) return text;
551
- let out = text;
552
- out = out.replace(JWT_PATTERN, REDACTED_BODY_VALUE);
553
- out = out.replace(BEARER_PATTERN, `$1${REDACTED_BODY_VALUE}`);
554
- out = out.replace(CARD_CANDIDATE_PATTERN, (match2) => {
555
- const digits = match2.replace(/[ -]/g, "");
556
- return digits.length >= 14 && digits.length <= 19 && passesLuhn(digits) ? REDACTED_BODY_VALUE : match2;
557
- });
558
- out = out.replace(
559
- RRN_CANDIDATE_PATTERN,
560
- (match2, _year, month, day) => looksLikeBirthDate(month, day) ? REDACTED_BODY_VALUE : match2
561
- );
562
- out = out.replace(BRN_CANDIDATE_PATTERN, (match2) => {
563
- const digits = match2.replace(/-/g, "");
564
- return passesBrnChecksum(digits) ? REDACTED_BODY_VALUE : match2;
565
- });
566
- out = out.replace(KR_MOBILE_PATTERN, REDACTED_BODY_VALUE);
567
- return out === text ? text : out;
568
- }
569
- function redactUrlSecrets(url) {
570
- if (!url) return url;
571
- const queryStart = url.indexOf("?");
572
- const fragmentStart = url.indexOf("#");
573
- if (queryStart < 0 && fragmentStart < 0) {
574
- return redactSensitiveValues(url);
575
- }
576
- const hasQuery = queryStart >= 0 && (fragmentStart < 0 || queryStart < fragmentStart);
577
- const base = url.slice(0, hasQuery ? queryStart : fragmentStart);
578
- const query = hasQuery ? url.slice(queryStart + 1, fragmentStart >= 0 ? fragmentStart : void 0) : "";
579
- const fragment = fragmentStart >= 0 ? url.slice(fragmentStart + 1) : "";
580
- const redactedQuery = redactKeyValuePairs(query);
581
- const redactedFragment = fragment.includes("=") ? redactKeyValuePairs(fragment) : fragment;
582
- const redactedBase = redactSensitiveValues(base);
583
- if (redactedBase === base && redactedQuery === query && redactedFragment === fragment) {
584
- return url;
585
- }
586
- return redactedBase + (hasQuery ? `?${redactedQuery}` : "") + (fragmentStart >= 0 ? `#${redactedFragment}` : "");
587
- }
588
- function redactSensitiveText(text) {
589
- if (!text) return text;
590
- return redactSensitiveValues(redactUrlSecrets(redactSensitiveBody(text)));
591
- }
592
640
  const META_EVENT_TYPE = 4;
593
641
  const FULL_SNAPSHOT_EVENT_TYPE = 2;
594
642
  const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
@@ -669,7 +717,13 @@ const RRWEB_VERSION = "2.1.0";
669
717
  const SESSION_REPLAY_WINDOW_MS = 12e4;
670
718
  const SESSION_REPLAY_CHECKOUT_MS = 6e4;
671
719
  const REPLAY_MASK_VALUE = "[redacted]";
672
- 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
+ }
673
727
  return text ? REPLAY_MASK_VALUE : text;
674
728
  }
675
729
  function createMaskTextFn(fullMaskSelector, unmaskSelector) {
@@ -685,19 +739,12 @@ function createMaskTextFn(fullMaskSelector, unmaskSelector) {
685
739
  };
686
740
  }
687
741
  const IDLE_CHECKOUT_POLL_MS = 5e3;
688
- function spanSeconds(segments) {
689
- var _a, _b;
690
- const events = flattenEvents(segments);
691
- const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
692
- const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
693
- if (first === void 0 || last === void 0) return 0;
694
- return Math.round((last - first) / 1e3);
695
- }
696
742
  class RingBufferRecorder {
697
743
  constructor(options = {}) {
698
744
  this.options = options;
699
745
  this.segments = createSegments();
700
746
  this.stopFn = null;
747
+ this.stopCanvasTracking = null;
701
748
  this.idleCheckoutTimer = null;
702
749
  this.visibilityListener = null;
703
750
  this.tailCutoff = null;
@@ -748,10 +795,13 @@ class RingBufferRecorder {
748
795
  this.segments = createSegments();
749
796
  const stop = recordFn({
750
797
  emit: (event, isCheckout) => {
751
- var _a, _b;
798
+ var _a, _b, _c;
752
799
  this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
753
800
  this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
754
- (_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);
755
805
  },
756
806
  checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
757
807
  maskAllInputs: cfg.maskAllInputs,
@@ -763,6 +813,9 @@ class RingBufferRecorder {
763
813
  // rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
764
814
  });
765
815
  this.stopFn = typeof stop === "function" ? stop : null;
816
+ if (this.options.trackCanvasInteractions !== false) {
817
+ this.stopCanvasTracking = startCanvasInteractionTracking();
818
+ }
766
819
  this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
767
820
  if (typeof document !== "undefined") {
768
821
  this.visibilityListener = () => this.forceCheckoutIfIdle();
@@ -770,9 +823,11 @@ class RingBufferRecorder {
770
823
  }
771
824
  }
772
825
  stop() {
773
- var _a;
826
+ var _a, _b;
774
827
  (_a = this.stopFn) == null ? void 0 : _a.call(this);
775
828
  this.stopFn = null;
829
+ (_b = this.stopCanvasTracking) == null ? void 0 : _b.call(this);
830
+ this.stopCanvasTracking = null;
776
831
  if (this.idleCheckoutTimer !== null) {
777
832
  clearInterval(this.idleCheckoutTimer);
778
833
  this.idleCheckoutTimer = null;
@@ -839,7 +894,6 @@ class RingBufferRecorder {
839
894
  return serializeSnapshot({
840
895
  segments,
841
896
  redactEvents: redactReplayEvents,
842
- windowSeconds: spanSeconds(segments),
843
897
  rrwebVersion: RRWEB_VERSION,
844
898
  privacyConfigVersion: cfg.version
845
899
  });
@@ -872,17 +926,31 @@ function withoutBody(log) {
872
926
  }
873
927
  return stripped;
874
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
+ }
875
941
  function trimLogBackupPayload(payload, budgetBytes = LOG_BACKUP_BUDGET_BYTES) {
942
+ var _a;
876
943
  if (payloadSizeBytes(payload) <= budgetBytes) return payload;
877
944
  let trimmed = {
878
945
  network: payload.network.map(withoutBody),
879
- console: payload.console
946
+ console: payload.console,
947
+ socket: (_a = payload.socket) == null ? void 0 : _a.map(withoutFrameData)
880
948
  };
881
949
  while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.network.length > 0) {
882
- trimmed = { network: trimmed.network.slice(1), console: trimmed.console };
950
+ trimmed = { ...trimmed, network: trimmed.network.slice(1) };
883
951
  }
884
952
  while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.console.length > 0) {
885
- trimmed = { network: trimmed.network, console: trimmed.console.slice(1) };
953
+ trimmed = { ...trimmed, console: trimmed.console.slice(1) };
886
954
  }
887
955
  return trimmed;
888
956
  }
@@ -1031,13 +1099,29 @@ const DB_NAME = "issue-sticker-replay";
1031
1099
  const DB_VERSION = 1;
1032
1100
  const STORE_NAME = "segments";
1033
1101
  const REPLAY_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
1034
- const REPLAY_BACKUP_EVENT_BUDGET = 12e3;
1035
- function countEvents(segments) {
1036
- 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
+ );
1037
1121
  }
1038
- function trimReplayBackup(segments, eventBudget = REPLAY_BACKUP_EVENT_BUDGET) {
1122
+ function trimReplayBackup(segments, sizeBudget = REPLAY_BACKUP_SIZE_BUDGET) {
1039
1123
  let trimmed = segments;
1040
- while (trimmed.length > 1 && countEvents(trimmed) > eventBudget) {
1124
+ while (trimmed.length > 1 && sizeOfSegments(trimmed) > sizeBudget) {
1041
1125
  trimmed = trimmed.slice(1);
1042
1126
  }
1043
1127
  return trimmed;
@@ -1238,6 +1322,8 @@ function createReplaySession(options = {}) {
1238
1322
  backup.schedule();
1239
1323
  };
1240
1324
  const recorder = options.recorder ?? new RingBufferRecorder({
1325
+ ...options.trackCanvasInteractions === false ? { trackCanvasInteractions: false } : {},
1326
+ ...options.windowMs === void 0 ? {} : { windowMs: options.windowMs },
1241
1327
  privacyConfig: options.privacyConfig,
1242
1328
  onChange: scheduleBackup
1243
1329
  });
@@ -1320,7 +1406,11 @@ class SessionReplayCaptureController {
1320
1406
  this.setSnapshot({ available: false, canAttach: false, error: null });
1321
1407
  return;
1322
1408
  }
1323
- 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
+ });
1324
1414
  recorder.start();
1325
1415
  this.recorder = recorder;
1326
1416
  this.setSnapshot({ available: true, canAttach, error: null });
@@ -1356,8 +1446,8 @@ class SessionReplayCaptureController {
1356
1446
  this.stopRecorder();
1357
1447
  this.listeners.clear();
1358
1448
  }
1359
- createRecorder(privacyConfig) {
1360
- return this.options.createRecorder ? this.options.createRecorder(privacyConfig) : createReplaySession({ privacyConfig });
1449
+ createRecorder(config) {
1450
+ return this.options.createRecorder ? this.options.createRecorder(config) : createReplaySession(config);
1361
1451
  }
1362
1452
  stopRecorder() {
1363
1453
  var _a;
@@ -1372,179 +1462,6 @@ class SessionReplayCaptureController {
1372
1462
  this.listeners.forEach((listener) => listener(this.snapshot));
1373
1463
  }
1374
1464
  }
1375
- function forEachElementDeep(root, callback) {
1376
- if (root instanceof Element) {
1377
- callback(root);
1378
- if (root.shadowRoot) {
1379
- forEachElementDeep(root.shadowRoot, callback);
1380
- }
1381
- }
1382
- Array.from(root.children).forEach((child) => {
1383
- forEachElementDeep(child, callback);
1384
- });
1385
- }
1386
- const SCREENSHOT_FULL_MASK_SELECTOR = SESSION_REPLAY_FULL_MASK_SELECTOR;
1387
- const SCREENSHOT_BLOCK_SELECTOR = SESSION_REPLAY_BLOCK_SELECTOR;
1388
- const SCREENSHOT_MASK_BACKGROUND = "rgba(148, 148, 148, 0.28)";
1389
- const SCREENSHOT_BLOCK_BACKGROUND = "rgba(148, 148, 148, 0.85)";
1390
- const MASK_LABEL_WIDTH = 44;
1391
- const MASK_LABEL_HEIGHT = 16;
1392
- const MASK_LABEL_IMAGE = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
1393
- `<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>`
1394
- )}")`;
1395
- const MASK_LABEL_MIN_WIDTH = MASK_LABEL_WIDTH + 8;
1396
- const MASK_LABEL_MIN_HEIGHT = MASK_LABEL_HEIGHT + 8;
1397
- const TEXT_HIDING_PROPERTIES = [
1398
- ["color", "transparent"],
1399
- ["-webkit-text-fill-color", "transparent"],
1400
- ["text-shadow", "none"],
1401
- ["fill", "transparent"]
1402
- ];
1403
- const REPLACED_MEDIA_SELECTOR = "img, svg, video, canvas, picture, iframe, object, embed";
1404
- const VALUE_ELEMENT_SELECTOR = "input, textarea, select";
1405
- const SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
1406
- // 자격증명
1407
- "current-password",
1408
- "new-password",
1409
- "one-time-code",
1410
- // 결제
1411
- "cc-number",
1412
- "cc-exp",
1413
- "cc-exp-month",
1414
- "cc-exp-year",
1415
- "cc-csc",
1416
- "cc-name",
1417
- "cc-type",
1418
- "cc-given-name",
1419
- "cc-additional-name",
1420
- "cc-family-name",
1421
- // 신원
1422
- "name",
1423
- "given-name",
1424
- "additional-name",
1425
- "family-name",
1426
- "honorific-prefix",
1427
- "honorific-suffix",
1428
- "nickname",
1429
- "bday",
1430
- "bday-day",
1431
- "bday-month",
1432
- "bday-year",
1433
- "sex",
1434
- // 주소
1435
- "street-address",
1436
- "address-line1",
1437
- "address-line2",
1438
- "address-line3",
1439
- "postal-code",
1440
- // 연락처
1441
- "tel",
1442
- "tel-national",
1443
- "tel-local",
1444
- "tel-area-code",
1445
- "email",
1446
- "impp"
1447
- ]);
1448
- function redactScreenshotText(text) {
1449
- return redactEmails(redactSensitiveValues(text));
1450
- }
1451
- function matchesSafely(element, selector) {
1452
- try {
1453
- return element.matches(selector);
1454
- } catch {
1455
- return false;
1456
- }
1457
- }
1458
- function closestSafely(element, selector) {
1459
- if (!element) return null;
1460
- try {
1461
- return element.closest(selector);
1462
- } catch {
1463
- return null;
1464
- }
1465
- }
1466
- function hasSensitiveValueHint(element) {
1467
- var _a;
1468
- if (((_a = element.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "password") {
1469
- return true;
1470
- }
1471
- const autocomplete = element.getAttribute("autocomplete");
1472
- if (!autocomplete) return false;
1473
- return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
1474
- }
1475
- const overwriteProperty = (element, name, value) => {
1476
- const style = element.style;
1477
- style == null ? void 0 : style.setProperty(name, value, "important");
1478
- };
1479
- function coverMaskedRegion(region, setProperty) {
1480
- setProperty(region, "background-color", SCREENSHOT_MASK_BACKGROUND);
1481
- forEachElementDeep(region, (node) => {
1482
- if (!isElementFullyMasked(node)) return;
1483
- TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(node, name, value));
1484
- if (matchesSafely(node, REPLACED_MEDIA_SELECTOR)) {
1485
- setProperty(node, "visibility", "hidden");
1486
- }
1487
- });
1488
- }
1489
- function parsePixels(value) {
1490
- const parsed = Number.parseFloat(value ?? "");
1491
- return Number.isFinite(parsed) ? parsed : 0;
1492
- }
1493
- function fitsMaskLabel(region) {
1494
- const rect = region.getBoundingClientRect();
1495
- const style = region.style;
1496
- const width = rect.width || parsePixels(style == null ? void 0 : style.width);
1497
- const height = rect.height || parsePixels(style == null ? void 0 : style.height);
1498
- return width >= MASK_LABEL_MIN_WIDTH && height >= MASK_LABEL_MIN_HEIGHT;
1499
- }
1500
- function coverBlockedRegion(region, setProperty) {
1501
- const labeled = fitsMaskLabel(region);
1502
- setProperty(region, "visibility", "visible");
1503
- setProperty(region, "background-color", SCREENSHOT_BLOCK_BACKGROUND);
1504
- setProperty(region, "background-image", labeled ? MASK_LABEL_IMAGE : "none");
1505
- if (labeled) {
1506
- setProperty(region, "background-repeat", "no-repeat");
1507
- setProperty(region, "background-position", "center");
1508
- setProperty(region, "background-size", "auto");
1509
- }
1510
- TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(region, name, value));
1511
- setProperty(region, "pointer-events", "none");
1512
- forEachElementDeep(region, (descendant) => {
1513
- if (descendant !== region) {
1514
- setProperty(descendant, "visibility", "hidden");
1515
- }
1516
- });
1517
- }
1518
- function coverPrivateRegions(root, setProperty) {
1519
- const blocked = [];
1520
- const masked = [];
1521
- forEachElementDeep(root, (element) => {
1522
- if (matchesSafely(element, SCREENSHOT_BLOCK_SELECTOR)) {
1523
- blocked.push(element);
1524
- return;
1525
- }
1526
- if (matchesSafely(element, SCREENSHOT_FULL_MASK_SELECTOR) && isElementFullyMasked(element)) {
1527
- masked.push(element);
1528
- }
1529
- });
1530
- masked.forEach((region) => coverMaskedRegion(region, setProperty));
1531
- blocked.forEach((region) => coverBlockedRegion(region, setProperty));
1532
- }
1533
- function applyScreenshotPrivacyMask(clonedRoot) {
1534
- coverPrivateRegions(clonedRoot, overwriteProperty);
1535
- const walker = clonedRoot.ownerDocument.createTreeWalker(clonedRoot, NodeFilter.SHOW_TEXT);
1536
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1537
- const text = node.nodeValue;
1538
- if (!text) continue;
1539
- node.nodeValue = redactScreenshotText(text);
1540
- }
1541
- clonedRoot.querySelectorAll(VALUE_ELEMENT_SELECTOR).forEach((element) => {
1542
- const value = element.getAttribute("value");
1543
- if (!value) return;
1544
- const hidden = closestSafely(element, SCREENSHOT_BLOCK_SELECTOR) !== null || isElementFullyMasked(element) || hasSensitiveValueHint(element);
1545
- element.setAttribute("value", hidden ? "" : redactScreenshotText(value));
1546
- });
1547
- }
1548
1465
  const SCREENSHOT_DATA_URL_QUALITY = 0.95;
1549
1466
  let nextCaptureRunId = 0;
1550
1467
  function createCaptureAttributeNames() {
@@ -1763,10 +1680,18 @@ function blobToDataUrl(blob) {
1763
1680
  reader.readAsDataURL(blob);
1764
1681
  });
1765
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
+ }
1766
1690
  async function captureWithModernScreenshot(options) {
1767
1691
  const { domToBlob } = await loadModernScreenshot();
1768
1692
  const { element, excludeSelectors = [], width, height, scale = getCaptureScale() } = options;
1769
1693
  await waitForDocumentFonts();
1694
+ const canvasFrames = await grabCanvasFrames(element, excludeSelectors);
1770
1695
  const attributeNames = createCaptureAttributeNames();
1771
1696
  const stickySnapshot = createModernStickySnapshot(element, attributeNames);
1772
1697
  const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors, attributeNames);
@@ -1793,8 +1718,21 @@ async function captureWithModernScreenshot(options) {
1793
1718
  if (height) {
1794
1719
  captureOptions.height = height;
1795
1720
  }
1796
- 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
+ );
1797
1734
  } finally {
1735
+ canvasFrames.forEach((frame) => frame.bitmap.close());
1798
1736
  excludeSnapshot.cleanup();
1799
1737
  stickySnapshot.cleanup();
1800
1738
  }
@@ -1879,7 +1817,7 @@ async function calculateSha256Hex(file) {
1879
1817
  }
1880
1818
  function normalizeUploadMimeType(mimeType, mediaType) {
1881
1819
  var _a;
1882
- 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";
1883
1821
  const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
1884
1822
  return normalized && normalized.length > 0 ? normalized : fallback;
1885
1823
  }
@@ -4190,8 +4128,8 @@ function pristineFetch(input, init) {
4190
4128
  }
4191
4129
  return typeof window !== "undefined" ? window.fetch(input, init) : fetch(input, init);
4192
4130
  }
4193
- const INSTRUMENTATION_FAILURE_LIMIT = 5;
4194
- const REPATCH_LIMIT = 2;
4131
+ const INSTRUMENTATION_FAILURE_LIMIT$1 = 5;
4132
+ const REPATCH_LIMIT$1 = 2;
4195
4133
  function setInternalField(target, key, value) {
4196
4134
  Object.defineProperty(target, key, {
4197
4135
  value,
@@ -4288,7 +4226,7 @@ function omitTooLargeResponseBody(sizeBytes) {
4288
4226
  responseBodySizeBytes: sizeBytes
4289
4227
  };
4290
4228
  }
4291
- function bytesToBase64(bytes) {
4229
+ function bytesToBase64$1(bytes) {
4292
4230
  let binary = "";
4293
4231
  const chunkSize = 32768;
4294
4232
  for (let offset = 0; offset < bytes.length; offset += chunkSize) {
@@ -4298,7 +4236,7 @@ function bytesToBase64(bytes) {
4298
4236
  return btoa(binary);
4299
4237
  }
4300
4238
  async function arrayBufferToBase64(arrayBuffer) {
4301
- return bytesToBase64(new Uint8Array(arrayBuffer));
4239
+ return bytesToBase64$1(new Uint8Array(arrayBuffer));
4302
4240
  }
4303
4241
  function getResponseDetailLevel(response) {
4304
4242
  if (response.type === "opaque" || response.type === "opaqueredirect") return "opaque";
@@ -4415,7 +4353,7 @@ class NetworkCollector {
4415
4353
  ensurePatched() {
4416
4354
  if (!this.running) return;
4417
4355
  if (window.fetch === this.patchedFetch) return;
4418
- if (this.repatchCount >= REPATCH_LIMIT) return;
4356
+ if (this.repatchCount >= REPATCH_LIMIT$1) return;
4419
4357
  this.repatchCount += 1;
4420
4358
  this.interceptFetch();
4421
4359
  }
@@ -4461,7 +4399,7 @@ class NetworkCollector {
4461
4399
  */
4462
4400
  recordInstrumentationFailure() {
4463
4401
  this.instrumentationFailures += 1;
4464
- if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT) {
4402
+ if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT$1) {
4465
4403
  this.stop();
4466
4404
  }
4467
4405
  }
@@ -4683,6 +4621,422 @@ class NetworkCollector {
4683
4621
  this.patchedXHR = null;
4684
4622
  }
4685
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
+ }
4686
5040
  var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
4687
5041
  NetworkLogType2["MAIN_FRAME"] = "main_frame";
4688
5042
  NetworkLogType2["SUB_FRAME"] = "sub_frame";
@@ -14230,6 +14584,20 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
14230
14584
  return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
14231
14585
  });
14232
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
+ }
14233
14601
  function buildExternalUserPayload(user) {
14234
14602
  if (!user) return {};
14235
14603
  return {
@@ -14238,60 +14606,63 @@ function buildExternalUserPayload(user) {
14238
14606
  };
14239
14607
  }
14240
14608
  export {
14241
- getNotionPageMetadata as $,
14242
- getLogBackupSessionId as A,
14243
- createIndexedDbLogBackup as B,
14609
+ Check as $,
14610
+ trimLogBackupPayload as A,
14611
+ createSafeLogBackup as B,
14244
14612
  ConsoleCollector as C,
14245
- PopoverTrigger as D,
14246
- Input as E,
14247
- PopoverContent as F,
14248
- Select$1 as G,
14249
- SelectTrigger as H,
14613
+ getLogBackupSessionId as D,
14614
+ createIndexedDbLogBackup as E,
14615
+ PopoverTrigger as F,
14616
+ Input as G,
14617
+ PopoverContent as H,
14250
14618
  ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
14251
- SelectValue as J,
14252
- SelectContent as K,
14619
+ Select$1 as J,
14620
+ SelectTrigger as K,
14253
14621
  Label as L,
14254
- SelectItem as M,
14622
+ SelectValue as M,
14255
14623
  NetworkCollector as N,
14256
- getJiraMetadata as O,
14624
+ SelectContent as O,
14257
14625
  Popover as P,
14258
- buildJiraIssueFields as Q,
14259
- Button$1 as R,
14626
+ SelectItem as Q,
14627
+ getJiraMetadata as R,
14260
14628
  ScreenRecordingController as S,
14261
- Root2 as T,
14262
- Trigger as U,
14263
- ChevronDown as V,
14264
- Portal as W,
14265
- Content2 as X,
14266
- Check as Y,
14267
- X as Z,
14268
- 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 _,
14269
14637
  uploadSignedMediaWithApi as a,
14270
- buildNotionFieldViews as a0,
14271
- DirectUploadError as a1,
14272
- toast as a2,
14273
- getSnapshot as a3,
14274
- subscribe as a4,
14275
- getRegisteredUploadId as a5,
14276
- ChevronRight as a6,
14277
- ReactRemoveScroll as a7,
14278
- hideOthers as a8,
14279
- useFocusGuards as a9,
14280
- FocusScope as aa,
14281
- getExposedJiraFields as ab,
14282
- isJiraFieldExposed as ac,
14283
- normalizeExposedJiraFields as ad,
14284
- getPrimaryJiraMapping as ae,
14285
- createCollection as af,
14286
- useDirection as ag,
14287
- getPrimaryNotionMapping as ah,
14288
- buttonVariants as ai,
14289
- findElementBySelector as aj,
14290
- findElementByXPath as ak,
14291
- OnboardingProvider as al,
14292
- useOnboardingTarget as am,
14293
- useOnboardingFlow as an,
14294
- 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,
14295
14666
  buildJiraCreateFields as b,
14296
14667
  captureWithModernScreenshot as c,
14297
14668
  directUploadToSignedUrl as d,
@@ -14312,9 +14683,9 @@ export {
14312
14683
  selectNetworkLogsForSubmit as s,
14313
14684
  sanitizeString as t,
14314
14685
  uploadScreenshotDataUrls as u,
14315
- getCurrentPageUrl as v,
14316
- SessionReplayCaptureController as w,
14317
- preloadModernScreenshot as x,
14318
- trimLogBackupPayload as y,
14319
- createSafeLogBackup as z
14686
+ serializeSocketTranscript as v,
14687
+ uploadSocketTranscript as w,
14688
+ getCurrentPageUrl as x,
14689
+ SessionReplayCaptureController as y,
14690
+ preloadModernScreenshot as z
14320
14691
  };