issue-sticker 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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 { t as getCanvasSource, v as onCanvasSourceRegistered, C as CANVAS_INTERACTION_TAG, w as gzip_1, R as REPLAY_EVENT_TYPE, x as record, a as cn, p as useCallbackRef$1, i as useComposedRefs, f as Primitive, P as Presence, j as Portal$1, g as composeEventHandlers, k as createContextScope, l as createSlot, D as DismissableLayer, y as Content, z as createPopperScope, m as useControllableState, A as Root2$1, n as useId, B as Anchor, E as Arrow, d as usePortalContainer, e as OVERLAY_LAYER, s as stopEscapePropagation, o as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, F as clsx, S as Slot$2, O as OVERLAY_LAYER_INDEX, M as MousePointer2 } from "./three-_bdgDAmI.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,6 +207,17 @@ class ScreenRecordingController {
207
207
  this.listeners.forEach((listener) => listener(this.snapshot));
208
208
  }
209
209
  }
210
+ function forEachElementDeep(root, callback) {
211
+ if (root instanceof Element) {
212
+ callback(root);
213
+ if (root.shadowRoot) {
214
+ forEachElementDeep(root.shadowRoot, callback);
215
+ }
216
+ }
217
+ Array.from(root.children).forEach((child) => {
218
+ forEachElementDeep(child, callback);
219
+ });
220
+ }
210
221
  const SESSION_REPLAY_BLOCK_CLASS = "rr-block";
211
222
  const SESSION_REPLAY_MASK_CLASS = "rr-mask";
212
223
  const SESSION_REPLAY_UNMASK_CLASS = "rr-unmask";
@@ -300,62 +311,6 @@ function resolvePrivacyConfig(override) {
300
311
  if (!override) return DEFAULT_PRIVACY_CONFIG_V2;
301
312
  return { ...DEFAULT_PRIVACY_CONFIG_V2, ...override };
302
313
  }
303
- function createSegments() {
304
- return [{ events: [] }];
305
- }
306
- function appendEvent(segments, event, isCheckout) {
307
- if (isCheckout) {
308
- return [...segments.map((s2) => ({ events: [...s2.events] })), { events: [event] }];
309
- }
310
- const copy = segments.map((s2) => ({ events: [...s2.events] }));
311
- const last = copy[copy.length - 1];
312
- if (last) {
313
- last.events.push(event);
314
- }
315
- return copy;
316
- }
317
- function trimSegments(segments, windowMs, now, checkoutMs) {
318
- var _a, _b;
319
- const cutoff = now - windowMs;
320
- let startIdx = 0;
321
- for (let i2 = 0; i2 < segments.length; i2 += 1) {
322
- const seg = segments[i2];
323
- const first = seg == null ? void 0 : seg.events[0];
324
- if (first && first.timestamp <= cutoff) {
325
- startIdx = i2;
326
- }
327
- }
328
- const headStart = (_b = (_a = segments[startIdx]) == null ? void 0 : _a.events[0]) == null ? void 0 : _b.timestamp;
329
- if (checkoutMs !== void 0 && headStart !== void 0 && headStart < cutoff - checkoutMs && startIdx < segments.length - 1) {
330
- startIdx += 1;
331
- }
332
- return segments.slice(startIdx);
333
- }
334
- function flattenEvents(segments) {
335
- return segments.flatMap((s2) => s2.events);
336
- }
337
- function dropEventsAfter(segments, cutoff) {
338
- return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
339
- }
340
- async function sha256Hex(bytes) {
341
- const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
342
- return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
343
- }
344
- 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);
349
- return {
350
- payload,
351
- checksumSha256,
352
- windowSeconds: input.windowSeconds,
353
- eventCount: events.length,
354
- sizeBytes: payload.byteLength,
355
- rrwebVersion: input.rrwebVersion,
356
- privacyConfigVersion: input.privacyConfigVersion
357
- };
358
- }
359
314
  const REDACTED_HEADER_VALUE = "[redacted]";
360
315
  const SENSITIVE_HEADER_NAMES = /* @__PURE__ */ new Set([
361
316
  "authorization",
@@ -589,260 +544,1523 @@ function redactSensitiveText(text) {
589
544
  if (!text) return text;
590
545
  return redactSensitiveValues(redactUrlSecrets(redactSensitiveBody(text)));
591
546
  }
592
- const META_EVENT_TYPE = 4;
593
- const FULL_SNAPSHOT_EVENT_TYPE = 2;
594
- const INCREMENTAL_SNAPSHOT_EVENT_TYPE = 3;
595
- const MUTATION_SOURCE = 0;
596
- const URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "poster", "data", "cite"]);
597
- function isRecord(value) {
598
- return typeof value === "object" && value !== null && !Array.isArray(value);
599
- }
600
- function redactAttributeValue(name, value) {
601
- if (typeof value !== "string" || !value) return value;
602
- return URL_ATTRIBUTES.has(name.toLowerCase()) ? redactUrlSecrets(value) : redactSensitiveValues(value);
547
+ const SCREENSHOT_FULL_MASK_SELECTOR = SESSION_REPLAY_FULL_MASK_SELECTOR;
548
+ const SCREENSHOT_BLOCK_SELECTOR = SESSION_REPLAY_BLOCK_SELECTOR;
549
+ const SCREENSHOT_MASK_BACKGROUND = "rgba(148, 148, 148, 0.28)";
550
+ const SCREENSHOT_BLOCK_BACKGROUND = "rgba(148, 148, 148, 0.85)";
551
+ const MASK_LABEL_WIDTH = 44;
552
+ const MASK_LABEL_HEIGHT = 16;
553
+ const MASK_LABEL_IMAGE = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
554
+ `<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>`
555
+ )}")`;
556
+ const MASK_LABEL_MIN_WIDTH = MASK_LABEL_WIDTH + 8;
557
+ const MASK_LABEL_MIN_HEIGHT = MASK_LABEL_HEIGHT + 8;
558
+ const TEXT_HIDING_PROPERTIES = [
559
+ ["color", "transparent"],
560
+ ["-webkit-text-fill-color", "transparent"],
561
+ ["text-shadow", "none"],
562
+ ["fill", "transparent"]
563
+ ];
564
+ const REPLACED_MEDIA_SELECTOR = "img, svg, video, canvas, picture, iframe, object, embed";
565
+ const VALUE_ELEMENT_SELECTOR = "input, textarea, select";
566
+ const SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
567
+ // 자격증명
568
+ "current-password",
569
+ "new-password",
570
+ "one-time-code",
571
+ // 결제
572
+ "cc-number",
573
+ "cc-exp",
574
+ "cc-exp-month",
575
+ "cc-exp-year",
576
+ "cc-csc",
577
+ "cc-name",
578
+ "cc-type",
579
+ "cc-given-name",
580
+ "cc-additional-name",
581
+ "cc-family-name",
582
+ // 신원
583
+ "name",
584
+ "given-name",
585
+ "additional-name",
586
+ "family-name",
587
+ "honorific-prefix",
588
+ "honorific-suffix",
589
+ "nickname",
590
+ "bday",
591
+ "bday-day",
592
+ "bday-month",
593
+ "bday-year",
594
+ "sex",
595
+ // 주소
596
+ "street-address",
597
+ "address-line1",
598
+ "address-line2",
599
+ "address-line3",
600
+ "postal-code",
601
+ // 연락처
602
+ "tel",
603
+ "tel-national",
604
+ "tel-local",
605
+ "tel-area-code",
606
+ "email",
607
+ "impp"
608
+ ]);
609
+ function redactScreenshotText(text) {
610
+ return redactEmails(redactSensitiveValues(text));
603
611
  }
604
- function redactAttributes(attributes) {
605
- if (!isRecord(attributes)) return;
606
- for (const [name, value] of Object.entries(attributes)) {
607
- const redacted = redactAttributeValue(name, value);
608
- if (redacted !== value) {
609
- attributes[name] = redacted;
610
- }
612
+ function matchesSafely(element, selector) {
613
+ try {
614
+ return element.matches(selector);
615
+ } catch {
616
+ return false;
611
617
  }
612
618
  }
613
- function redactSerializedNode(node) {
614
- if (!isRecord(node)) return;
615
- redactAttributes(node.attributes);
616
- if (typeof node.textContent === "string" && node.textContent) {
617
- const redacted = redactSensitiveValues(node.textContent);
618
- if (redacted !== node.textContent) {
619
- node.textContent = redacted;
620
- }
621
- }
622
- if (Array.isArray(node.childNodes)) {
623
- for (const child of node.childNodes) {
624
- redactSerializedNode(child);
625
- }
619
+ function closestSafely(element, selector) {
620
+ if (!element) return null;
621
+ try {
622
+ return element.closest(selector);
623
+ } catch {
624
+ return null;
626
625
  }
627
626
  }
628
- function redactMutationData(data) {
629
- if (Array.isArray(data.adds)) {
630
- for (const add of data.adds) {
631
- if (isRecord(add)) redactSerializedNode(add.node);
632
- }
627
+ function hasSensitiveValueHint(element) {
628
+ var _a;
629
+ if (((_a = element.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "password") {
630
+ return true;
633
631
  }
634
- if (Array.isArray(data.attributes)) {
635
- for (const mutation of data.attributes) {
636
- if (isRecord(mutation)) redactAttributes(mutation.attributes);
632
+ const autocomplete = element.getAttribute("autocomplete");
633
+ if (!autocomplete) return false;
634
+ return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
635
+ }
636
+ const overwriteProperty = (element, name, value) => {
637
+ const style = element.style;
638
+ style == null ? void 0 : style.setProperty(name, value, "important");
639
+ };
640
+ function coverMaskedRegion(region, setProperty) {
641
+ setProperty(region, "background-color", SCREENSHOT_MASK_BACKGROUND);
642
+ forEachElementDeep(region, (node) => {
643
+ if (!isElementFullyMasked(node)) return;
644
+ TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(node, name, value));
645
+ if (matchesSafely(node, REPLACED_MEDIA_SELECTOR)) {
646
+ setProperty(node, "visibility", "hidden");
637
647
  }
648
+ });
649
+ }
650
+ function parsePixels(value) {
651
+ const parsed = Number.parseFloat(value ?? "");
652
+ return Number.isFinite(parsed) ? parsed : 0;
653
+ }
654
+ function fitsMaskLabel(region) {
655
+ const rect = region.getBoundingClientRect();
656
+ const style = region.style;
657
+ const width = rect.width || parsePixels(style == null ? void 0 : style.width);
658
+ const height = rect.height || parsePixels(style == null ? void 0 : style.height);
659
+ return width >= MASK_LABEL_MIN_WIDTH && height >= MASK_LABEL_MIN_HEIGHT;
660
+ }
661
+ function coverBlockedRegion(region, setProperty) {
662
+ const labeled = fitsMaskLabel(region);
663
+ setProperty(region, "visibility", "visible");
664
+ setProperty(region, "background-color", SCREENSHOT_BLOCK_BACKGROUND);
665
+ setProperty(region, "background-image", labeled ? MASK_LABEL_IMAGE : "none");
666
+ if (labeled) {
667
+ setProperty(region, "background-repeat", "no-repeat");
668
+ setProperty(region, "background-position", "center");
669
+ setProperty(region, "background-size", "auto");
638
670
  }
639
- if (Array.isArray(data.texts)) {
640
- for (const mutation of data.texts) {
641
- if (isRecord(mutation) && typeof mutation.value === "string" && mutation.value) {
642
- const redacted = redactSensitiveValues(mutation.value);
643
- if (redacted !== mutation.value) {
644
- mutation.value = redacted;
645
- }
646
- }
671
+ TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(region, name, value));
672
+ setProperty(region, "pointer-events", "none");
673
+ forEachElementDeep(region, (descendant) => {
674
+ if (descendant !== region) {
675
+ setProperty(descendant, "visibility", "hidden");
647
676
  }
648
- }
677
+ });
649
678
  }
650
- function redactReplayEvents(events) {
651
- for (const event of events) {
652
- const data = event.data;
653
- if (!isRecord(data)) continue;
654
- if (event.type === META_EVENT_TYPE && typeof data.href === "string") {
655
- data.href = redactUrlSecrets(data.href);
656
- continue;
657
- }
658
- if (event.type === FULL_SNAPSHOT_EVENT_TYPE) {
659
- redactSerializedNode(data.node);
660
- continue;
679
+ function coverPrivateRegions(root, setProperty) {
680
+ const blocked = [];
681
+ const masked = [];
682
+ forEachElementDeep(root, (element) => {
683
+ if (matchesSafely(element, SCREENSHOT_BLOCK_SELECTOR)) {
684
+ blocked.push(element);
685
+ return;
661
686
  }
662
- if (event.type === INCREMENTAL_SNAPSHOT_EVENT_TYPE && data.source === MUTATION_SOURCE) {
663
- redactMutationData(data);
687
+ if (matchesSafely(element, SCREENSHOT_FULL_MASK_SELECTOR) && isElementFullyMasked(element)) {
688
+ masked.push(element);
664
689
  }
665
- }
666
- return events;
690
+ });
691
+ masked.forEach((region) => coverMaskedRegion(region, setProperty));
692
+ blocked.forEach((region) => coverBlockedRegion(region, setProperty));
667
693
  }
668
- const RRWEB_VERSION = "2.1.0";
669
- const SESSION_REPLAY_WINDOW_MS = 12e4;
670
- const SESSION_REPLAY_CHECKOUT_MS = 6e4;
694
+ function applyScreenshotPrivacyMask(clonedRoot) {
695
+ coverPrivateRegions(clonedRoot, overwriteProperty);
696
+ const walker = clonedRoot.ownerDocument.createTreeWalker(clonedRoot, NodeFilter.SHOW_TEXT);
697
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
698
+ const text = node.nodeValue;
699
+ if (!text) continue;
700
+ node.nodeValue = redactScreenshotText(text);
701
+ }
702
+ clonedRoot.querySelectorAll(VALUE_ELEMENT_SELECTOR).forEach((element) => {
703
+ const value = element.getAttribute("value");
704
+ if (!value) return;
705
+ const hidden = closestSafely(element, SCREENSHOT_BLOCK_SELECTOR) !== null || isElementFullyMasked(element) || hasSensitiveValueHint(element);
706
+ element.setAttribute("value", hidden ? "" : redactScreenshotText(value));
707
+ });
708
+ }
709
+ const FRAME_GRAB_TIMEOUT_MS = 300;
710
+ const PROBE_SIZE = 8;
711
+ function matchesSelfOrAncestor(element, selector) {
712
+ try {
713
+ return element.closest(selector) !== null;
714
+ } catch {
715
+ return false;
716
+ }
717
+ }
718
+ function shouldOverlayCanvas(canvas, excludeSelectors) {
719
+ const rect = canvas.getBoundingClientRect();
720
+ if (rect.width < 1 || rect.height < 1) {
721
+ return false;
722
+ }
723
+ if (matchesSelfOrAncestor(canvas, SCREENSHOT_BLOCK_SELECTOR) || matchesSelfOrAncestor(canvas, SCREENSHOT_FULL_MASK_SELECTOR)) {
724
+ return false;
725
+ }
726
+ return !excludeSelectors.some((selector) => matchesSelfOrAncestor(canvas, selector));
727
+ }
728
+ function rendersBlank(canvas) {
729
+ const probe = document.createElement("canvas");
730
+ probe.width = PROBE_SIZE;
731
+ probe.height = PROBE_SIZE;
732
+ const context = probe.getContext("2d", { willReadFrequently: true });
733
+ if (!context) {
734
+ return false;
735
+ }
736
+ try {
737
+ context.drawImage(canvas, 0, 0, PROBE_SIZE, PROBE_SIZE);
738
+ const { data } = context.getImageData(0, 0, PROBE_SIZE, PROBE_SIZE);
739
+ for (let offset = 4; offset < data.length; offset += 4) {
740
+ if (data[offset] !== data[0] || data[offset + 1] !== data[1] || data[offset + 2] !== data[2] || data[offset + 3] !== data[3]) {
741
+ return false;
742
+ }
743
+ }
744
+ return true;
745
+ } catch {
746
+ return false;
747
+ }
748
+ }
749
+ async function grabFrame(canvas) {
750
+ const FrameGrabber = globalThis.ImageCapture;
751
+ if (!FrameGrabber || typeof canvas.captureStream !== "function") {
752
+ return null;
753
+ }
754
+ let track;
755
+ try {
756
+ track = canvas.captureStream().getVideoTracks()[0];
757
+ if (!track) {
758
+ return null;
759
+ }
760
+ return await Promise.race([
761
+ new FrameGrabber(track).grabFrame(),
762
+ new Promise((resolve) => {
763
+ setTimeout(() => resolve(null), FRAME_GRAB_TIMEOUT_MS);
764
+ })
765
+ ]);
766
+ } catch {
767
+ return null;
768
+ } finally {
769
+ track == null ? void 0 : track.stop();
770
+ }
771
+ }
772
+ async function redrawAndGrab(canvas, source) {
773
+ try {
774
+ source.redraw();
775
+ return await createImageBitmap(canvas);
776
+ } catch {
777
+ return null;
778
+ }
779
+ }
780
+ async function grabCanvasFrames(root, excludeSelectors) {
781
+ const targets = [];
782
+ forEachElementDeep(root, (element) => {
783
+ if (!(element instanceof HTMLCanvasElement) || !shouldOverlayCanvas(element, excludeSelectors)) {
784
+ return;
785
+ }
786
+ if (getCanvasSource(element) || rendersBlank(element)) {
787
+ targets.push(element);
788
+ }
789
+ });
790
+ if (targets.length === 0) {
791
+ return [];
792
+ }
793
+ const frames = await Promise.all(
794
+ targets.map(async (canvas) => {
795
+ const source = getCanvasSource(canvas);
796
+ const bitmap = (source ? await redrawAndGrab(canvas, source) : null) ?? await grabFrame(canvas);
797
+ return bitmap ? { bitmap, rect: canvas.getBoundingClientRect() } : null;
798
+ })
799
+ );
800
+ return frames.filter((frame) => frame !== null);
801
+ }
802
+ function frameDestRect(rect, origin, scale) {
803
+ return [
804
+ (rect.left - origin.x) * scale,
805
+ (rect.top - origin.y) * scale,
806
+ rect.width * scale,
807
+ rect.height * scale
808
+ ];
809
+ }
810
+ async function overlayCanvasFrames(capture, frames, origin, cssWidth, quality) {
811
+ let captured = null;
812
+ try {
813
+ captured = await createImageBitmap(capture);
814
+ const output = document.createElement("canvas");
815
+ output.width = captured.width;
816
+ output.height = captured.height;
817
+ const context = output.getContext("2d");
818
+ if (!context) {
819
+ return capture;
820
+ }
821
+ context.drawImage(captured, 0, 0);
822
+ const scale = cssWidth > 0 ? captured.width / cssWidth : 1;
823
+ frames.forEach(({ bitmap, rect }) => {
824
+ context.drawImage(bitmap, ...frameDestRect(rect, origin, scale));
825
+ });
826
+ const blob = await new Promise((resolve) => {
827
+ output.toBlob(resolve, capture.type || "image/webp", quality);
828
+ });
829
+ return blob ?? capture;
830
+ } catch {
831
+ return capture;
832
+ } finally {
833
+ captured == null ? void 0 : captured.close();
834
+ }
835
+ }
836
+ const SNAPSHOT_MAX_WIDTH = 640;
837
+ const SNAPSHOT_QUALITY = 0.8;
838
+ const SAMPLE_INTERVAL_MS = 250;
839
+ const SAMPLED_MAX_WIDTH = 480;
840
+ const SAMPLED_QUALITY = 0.6;
841
+ const DRAG_THRESHOLD_PX = 6;
842
+ const DISCRETE_MIN_GAP_MS = 60;
843
+ const SETTLE_TAIL_MS = 300;
844
+ const FINGERPRINT_WIDTH = 32;
845
+ const FINGERPRINT_HEIGHT = 18;
846
+ async function captureSnapshot(canvas, source, maxWidth, quality) {
847
+ try {
848
+ if (source) {
849
+ source.redraw();
850
+ } else if (rendersBlank(canvas)) {
851
+ return null;
852
+ }
853
+ const bitmap = await createImageBitmap(canvas);
854
+ try {
855
+ const scale = Math.min(1, maxWidth / bitmap.width);
856
+ const output = document.createElement("canvas");
857
+ output.width = Math.max(1, Math.round(bitmap.width * scale));
858
+ output.height = Math.max(1, Math.round(bitmap.height * scale));
859
+ const context = output.getContext("2d");
860
+ if (!context) {
861
+ return null;
862
+ }
863
+ context.drawImage(bitmap, 0, 0, output.width, output.height);
864
+ const thumb = document.createElement("canvas");
865
+ thumb.width = FINGERPRINT_WIDTH;
866
+ thumb.height = FINGERPRINT_HEIGHT;
867
+ const thumbContext = thumb.getContext("2d");
868
+ thumbContext == null ? void 0 : thumbContext.drawImage(bitmap, 0, 0, FINGERPRINT_WIDTH, FINGERPRINT_HEIGHT);
869
+ const fingerprint = thumbContext ? Array.from(
870
+ thumbContext.getImageData(0, 0, FINGERPRINT_WIDTH, FINGERPRINT_HEIGHT).data
871
+ ).join(",") : "";
872
+ return { dataUrl: output.toDataURL("image/webp", quality), fingerprint };
873
+ } finally {
874
+ bitmap.close();
875
+ }
876
+ } catch {
877
+ return null;
878
+ }
879
+ }
880
+ const FRAME_WAIT_MS = 120;
881
+ function settleFrame() {
882
+ return new Promise((resolve) => {
883
+ let done = false;
884
+ const finish = () => {
885
+ if (!done) {
886
+ done = true;
887
+ resolve();
888
+ }
889
+ };
890
+ requestAnimationFrame(finish);
891
+ setTimeout(finish, FRAME_WAIT_MS);
892
+ });
893
+ }
894
+ async function resolveNodeId(canvas) {
895
+ var _a;
896
+ try {
897
+ const { record: record2 } = await import("./three-_bdgDAmI.js").then((n) => n.I);
898
+ const id = (_a = record2.mirror) == null ? void 0 : _a.getId(canvas);
899
+ return typeof id === "number" && id > 0 ? id : null;
900
+ } catch {
901
+ return null;
902
+ }
903
+ }
904
+ function canvasIndexOf(canvas) {
905
+ return Array.prototype.indexOf.call(document.querySelectorAll("canvas"), canvas);
906
+ }
907
+ function startCanvasInteractionTracking(options = {}) {
908
+ if (typeof document === "undefined") {
909
+ return Object.assign(() => void 0, { captureBaseline: () => void 0 });
910
+ }
911
+ const snapshotMaxWidth = options.snapshotMaxWidth ?? SNAPSHOT_MAX_WIDTH;
912
+ const sampleIntervalMs = options.sampleIntervalMs ?? SAMPLE_INTERVAL_MS;
913
+ const lastSnapshot = /* @__PURE__ */ new WeakMap();
914
+ let pending = null;
915
+ const emit2 = options.emit ?? ((tag, payload) => {
916
+ void import("./three-_bdgDAmI.js").then((n) => n.I).then(({ addCustomEvent }) => addCustomEvent(tag, payload));
917
+ });
918
+ const pointerOf = (started, rect) => [
919
+ (started.x - rect.left) / rect.width * 2 - 1,
920
+ -((started.y - rect.top) / rect.height) * 2 + 1
921
+ ];
922
+ const trackedCanvases = () => Array.from(document.querySelectorAll("canvas")).filter((canvas) => {
923
+ const rect = canvas.getBoundingClientRect();
924
+ return rect.width >= 1 && rect.height >= 1 && shouldOverlayCanvas(canvas, []);
925
+ });
926
+ const emitShot = async (canvas, shot) => {
927
+ var _a;
928
+ const source = getCanvasSource(canvas);
929
+ const snapshot = await captureSnapshot(
930
+ canvas,
931
+ source,
932
+ shot.detail ? snapshotMaxWidth : SAMPLED_MAX_WIDTH,
933
+ shot.detail ? SNAPSHOT_QUALITY : SAMPLED_QUALITY
934
+ );
935
+ if (snapshot && shot.sampled && !shot.baseline && lastSnapshot.get(canvas) === snapshot.fingerprint) {
936
+ return;
937
+ }
938
+ if (snapshot) {
939
+ lastSnapshot.set(canvas, snapshot.fingerprint);
940
+ }
941
+ const cameraMatrix = ((_a = source == null ? void 0 : source.cameraMatrix) == null ? void 0 : _a.call(source)) ?? null;
942
+ if (!snapshot && !shot.target && !cameraMatrix) {
943
+ return;
944
+ }
945
+ const payload = {
946
+ kind: shot.kind,
947
+ target: shot.target,
948
+ nodeId: await resolveNodeId(canvas),
949
+ canvasIndex: canvasIndexOf(canvas),
950
+ pointer: shot.pointer,
951
+ cameraStartMatrix: shot.cameraStartMatrix,
952
+ cameraMatrix,
953
+ snapshot: (snapshot == null ? void 0 : snapshot.dataUrl) ?? null,
954
+ ...shot.sampled ? { sampled: true } : {},
955
+ ...shot.baseline ? { baseline: true } : {}
956
+ };
957
+ try {
958
+ emit2(CANVAS_INTERACTION_TAG, payload);
959
+ } catch {
960
+ }
961
+ };
962
+ const sweep = async (shot) => {
963
+ for (const canvas of trackedCanvases()) {
964
+ await emitShot(canvas, {
965
+ kind: "external",
966
+ target: null,
967
+ pointer: null,
968
+ cameraStartMatrix: null,
969
+ ...shot
970
+ });
971
+ }
972
+ };
973
+ let sweeping = false;
974
+ let pendingSweep = null;
975
+ const runSweep = async (shot) => {
976
+ if (sweeping) {
977
+ pendingSweep = shot;
978
+ return;
979
+ }
980
+ sweeping = true;
981
+ try {
982
+ await sweep(shot);
983
+ while (pendingSweep) {
984
+ const next = pendingSweep;
985
+ pendingSweep = null;
986
+ await sweep(next);
987
+ }
988
+ } finally {
989
+ sweeping = false;
990
+ }
991
+ };
992
+ let sampleTimer = null;
993
+ let tailTimer = null;
994
+ let lastDiscreteAt = 0;
995
+ const clearTimers = () => {
996
+ if (sampleTimer !== null) {
997
+ window.clearInterval(sampleTimer);
998
+ sampleTimer = null;
999
+ }
1000
+ if (tailTimer !== null) {
1001
+ window.clearTimeout(tailTimer);
1002
+ tailTimer = null;
1003
+ }
1004
+ };
1005
+ const markChanged = () => {
1006
+ if (sampleTimer === null && sampleIntervalMs > 0) {
1007
+ sampleTimer = window.setInterval(() => {
1008
+ void runSweep({ sampled: true, detail: false });
1009
+ }, sampleIntervalMs);
1010
+ }
1011
+ if (tailTimer !== null) {
1012
+ window.clearTimeout(tailTimer);
1013
+ }
1014
+ tailTimer = window.setTimeout(() => {
1015
+ tailTimer = null;
1016
+ if (sampleTimer !== null) {
1017
+ window.clearInterval(sampleTimer);
1018
+ sampleTimer = null;
1019
+ }
1020
+ void (async () => {
1021
+ await settleFrame();
1022
+ await runSweep({ sampled: true, detail: true });
1023
+ })();
1024
+ }, SETTLE_TAIL_MS);
1025
+ };
1026
+ const onPointerDown = (event) => {
1027
+ var _a;
1028
+ const target = event.target;
1029
+ if (!(target instanceof HTMLCanvasElement) || !shouldOverlayCanvas(target, [])) {
1030
+ return;
1031
+ }
1032
+ const source = getCanvasSource(target);
1033
+ pending = {
1034
+ canvas: target,
1035
+ x: event.clientX,
1036
+ y: event.clientY,
1037
+ cameraStartMatrix: ((_a = source == null ? void 0 : source.cameraMatrix) == null ? void 0 : _a.call(source)) ?? null
1038
+ };
1039
+ };
1040
+ const onPointerMove = (event) => {
1041
+ const started = pending;
1042
+ if (!started) {
1043
+ return;
1044
+ }
1045
+ if (Math.hypot(event.clientX - started.x, event.clientY - started.y) <= DRAG_THRESHOLD_PX) {
1046
+ return;
1047
+ }
1048
+ markChanged();
1049
+ };
1050
+ const captureDiscrete = () => {
1051
+ const now = Date.now();
1052
+ if (now - lastDiscreteAt >= DISCRETE_MIN_GAP_MS) {
1053
+ lastDiscreteAt = now;
1054
+ void (async () => {
1055
+ await settleFrame();
1056
+ await runSweep({ sampled: true, detail: false });
1057
+ })();
1058
+ }
1059
+ markChanged();
1060
+ };
1061
+ const onContinuousInput = () => {
1062
+ markChanged();
1063
+ };
1064
+ const onPointerUp = (event) => {
1065
+ var _a;
1066
+ const started = pending;
1067
+ pending = null;
1068
+ if (!started) {
1069
+ captureDiscrete();
1070
+ return;
1071
+ }
1072
+ clearTimers();
1073
+ const source = getCanvasSource(started.canvas);
1074
+ const rect = started.canvas.getBoundingClientRect();
1075
+ if (rect.width < 1 || rect.height < 1) {
1076
+ return;
1077
+ }
1078
+ const moved = Math.hypot(event.clientX - started.x, event.clientY - started.y);
1079
+ const kind = moved > DRAG_THRESHOLD_PX ? "drag" : "click";
1080
+ const target = ((_a = source == null ? void 0 : source.pickTarget) == null ? void 0 : _a.call(source, pointerOf(started, rect))) ?? null;
1081
+ void (async () => {
1082
+ await settleFrame();
1083
+ await emitShot(started.canvas, {
1084
+ kind,
1085
+ target,
1086
+ pointer: pointerOf(started, rect),
1087
+ cameraStartMatrix: started.cameraStartMatrix,
1088
+ sampled: false,
1089
+ detail: true
1090
+ });
1091
+ })();
1092
+ markChanged();
1093
+ };
1094
+ const captureBaseline = () => {
1095
+ void (async () => {
1096
+ await settleFrame();
1097
+ await runSweep({ sampled: true, detail: true, baseline: true });
1098
+ })();
1099
+ };
1100
+ document.addEventListener("pointerdown", onPointerDown, true);
1101
+ document.addEventListener("pointermove", onPointerMove, true);
1102
+ document.addEventListener("pointerup", onPointerUp, true);
1103
+ document.addEventListener("pointercancel", onPointerUp, true);
1104
+ document.addEventListener("input", onContinuousInput, true);
1105
+ document.addEventListener("keydown", onContinuousInput, true);
1106
+ document.addEventListener("change", captureDiscrete, true);
1107
+ const unsubscribeRegistration = onCanvasSourceRegistered((canvas) => {
1108
+ void (async () => {
1109
+ await settleFrame();
1110
+ await emitShot(canvas, {
1111
+ kind: "external",
1112
+ target: null,
1113
+ pointer: null,
1114
+ cameraStartMatrix: null,
1115
+ sampled: true,
1116
+ detail: true,
1117
+ baseline: true
1118
+ });
1119
+ })();
1120
+ });
1121
+ const stop = () => {
1122
+ clearTimers();
1123
+ unsubscribeRegistration();
1124
+ document.removeEventListener("pointerdown", onPointerDown, true);
1125
+ document.removeEventListener("pointermove", onPointerMove, true);
1126
+ document.removeEventListener("pointerup", onPointerUp, true);
1127
+ document.removeEventListener("pointercancel", onPointerUp, true);
1128
+ document.removeEventListener("input", onContinuousInput, true);
1129
+ document.removeEventListener("keydown", onContinuousInput, true);
1130
+ document.removeEventListener("change", captureDiscrete, true);
1131
+ pending = null;
1132
+ };
1133
+ return Object.assign(stop, { captureBaseline });
1134
+ }
1135
+ function createSegments() {
1136
+ return [{ events: [] }];
1137
+ }
1138
+ const META_EVENT_TYPE = 4;
1139
+ function endsWithBareMeta(segments) {
1140
+ var _a, _b;
1141
+ const events = (_a = segments[segments.length - 1]) == null ? void 0 : _a.events;
1142
+ return (events == null ? void 0 : events.length) === 1 && ((_b = events[0]) == null ? void 0 : _b.type) === META_EVENT_TYPE;
1143
+ }
1144
+ function appendEvent(segments, event, isCheckout) {
1145
+ if (isCheckout && !endsWithBareMeta(segments)) {
1146
+ return [...segments.map((s2) => ({ events: [...s2.events] })), { events: [event] }];
1147
+ }
1148
+ const copy = segments.map((s2) => ({ events: [...s2.events] }));
1149
+ const last = copy[copy.length - 1];
1150
+ if (last) {
1151
+ last.events.push(event);
1152
+ }
1153
+ return copy;
1154
+ }
1155
+ function trimSegments(segments, windowMs, now, checkoutMs) {
1156
+ var _a, _b;
1157
+ const cutoff = now - windowMs;
1158
+ let startIdx = 0;
1159
+ for (let i2 = 0; i2 < segments.length; i2 += 1) {
1160
+ const seg = segments[i2];
1161
+ const first = seg == null ? void 0 : seg.events[0];
1162
+ if (first && first.timestamp <= cutoff) {
1163
+ startIdx = i2;
1164
+ }
1165
+ }
1166
+ const headStart = (_b = (_a = segments[startIdx]) == null ? void 0 : _a.events[0]) == null ? void 0 : _b.timestamp;
1167
+ if (checkoutMs !== void 0 && headStart !== void 0 && headStart < cutoff - checkoutMs && startIdx < segments.length - 1) {
1168
+ startIdx += 1;
1169
+ }
1170
+ return segments.slice(startIdx);
1171
+ }
1172
+ function spanSeconds(segments) {
1173
+ var _a, _b;
1174
+ const events = flattenEvents(segments);
1175
+ const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
1176
+ const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
1177
+ if (first === void 0 || last === void 0) return 0;
1178
+ return Math.round((last - first) / 1e3);
1179
+ }
1180
+ function flattenEvents(segments) {
1181
+ return segments.flatMap((s2) => s2.events);
1182
+ }
1183
+ function dropEventsAfter(segments, cutoff) {
1184
+ return segments.map((s2) => ({ events: s2.events.filter((e2) => e2.timestamp <= cutoff) })).filter((s2) => s2.events.length > 0);
1185
+ }
1186
+ const REPLAY_SNAPSHOT_MAX_BYTES = 16 * 1024 * 1024;
1187
+ async function sha256Hex$1(bytes) {
1188
+ const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
1189
+ return Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("");
1190
+ }
1191
+ async function serializeSnapshot(input) {
1192
+ const redact = input.redactEvents ?? ((e2) => e2);
1193
+ const maxBytes = input.maxBytes ?? REPLAY_SNAPSHOT_MAX_BYTES;
1194
+ const compress2 = (segments2) => {
1195
+ const events2 = redact(flattenEvents(segments2));
1196
+ return { events: events2, payload: gzip_1(JSON.stringify(events2)) };
1197
+ };
1198
+ let segments = input.segments;
1199
+ let { events, payload } = compress2(segments);
1200
+ while (payload.byteLength > maxBytes && segments.length > 1) {
1201
+ segments = segments.slice(1);
1202
+ ({ events, payload } = compress2(segments));
1203
+ }
1204
+ const checksumSha256 = await sha256Hex$1(payload);
1205
+ return {
1206
+ payload,
1207
+ checksumSha256,
1208
+ windowSeconds: spanSeconds(segments),
1209
+ eventCount: events.length,
1210
+ sizeBytes: payload.byteLength,
1211
+ rrwebVersion: input.rrwebVersion,
1212
+ privacyConfigVersion: input.privacyConfigVersion
1213
+ };
1214
+ }
1215
+ const MUTATION_SOURCE = 0;
1216
+ const URL_ATTRIBUTES = /* @__PURE__ */ new Set(["href", "src", "action", "formaction", "poster", "data", "cite"]);
1217
+ function isRecord(value) {
1218
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1219
+ }
1220
+ function redactAttributeValue(name, value) {
1221
+ if (typeof value !== "string" || !value) return value;
1222
+ return URL_ATTRIBUTES.has(name.toLowerCase()) ? redactUrlSecrets(value) : redactSensitiveValues(value);
1223
+ }
1224
+ function redactAttributes(attributes) {
1225
+ if (!isRecord(attributes)) return;
1226
+ for (const [name, value] of Object.entries(attributes)) {
1227
+ const redacted = redactAttributeValue(name, value);
1228
+ if (redacted !== value) {
1229
+ attributes[name] = redacted;
1230
+ }
1231
+ }
1232
+ }
1233
+ function redactSerializedNode(node) {
1234
+ if (!isRecord(node)) return;
1235
+ redactAttributes(node.attributes);
1236
+ if (typeof node.textContent === "string" && node.textContent) {
1237
+ const redacted = redactSensitiveValues(node.textContent);
1238
+ if (redacted !== node.textContent) {
1239
+ node.textContent = redacted;
1240
+ }
1241
+ }
1242
+ if (Array.isArray(node.childNodes)) {
1243
+ for (const child of node.childNodes) {
1244
+ redactSerializedNode(child);
1245
+ }
1246
+ }
1247
+ }
1248
+ function redactMutationData(data) {
1249
+ if (Array.isArray(data.adds)) {
1250
+ for (const add of data.adds) {
1251
+ if (isRecord(add)) redactSerializedNode(add.node);
1252
+ }
1253
+ }
1254
+ if (Array.isArray(data.attributes)) {
1255
+ for (const mutation of data.attributes) {
1256
+ if (isRecord(mutation)) redactAttributes(mutation.attributes);
1257
+ }
1258
+ }
1259
+ if (Array.isArray(data.texts)) {
1260
+ for (const mutation of data.texts) {
1261
+ if (isRecord(mutation) && typeof mutation.value === "string" && mutation.value) {
1262
+ const redacted = redactSensitiveValues(mutation.value);
1263
+ if (redacted !== mutation.value) {
1264
+ mutation.value = redacted;
1265
+ }
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ function redactReplayEvents(events) {
1271
+ for (const event of events) {
1272
+ const data = event.data;
1273
+ if (!isRecord(data)) continue;
1274
+ if (event.type === REPLAY_EVENT_TYPE.Meta && typeof data.href === "string") {
1275
+ data.href = redactUrlSecrets(data.href);
1276
+ continue;
1277
+ }
1278
+ if (event.type === REPLAY_EVENT_TYPE.FullSnapshot) {
1279
+ redactSerializedNode(data.node);
1280
+ continue;
1281
+ }
1282
+ if (event.type === REPLAY_EVENT_TYPE.IncrementalSnapshot && data.source === MUTATION_SOURCE) {
1283
+ redactMutationData(data);
1284
+ }
1285
+ }
1286
+ return events;
1287
+ }
1288
+ const RRWEB_VERSION = "2.1.0";
1289
+ const SESSION_REPLAY_WINDOW_MS = 12e4;
1290
+ const SESSION_REPLAY_CHECKOUT_MS = 6e4;
671
1291
  const REPLAY_MASK_VALUE = "[redacted]";
672
- function maskReplayInput(text) {
1292
+ const UNMASKED_INPUT_TYPES = /* @__PURE__ */ new Set(["range", "color", "checkbox", "radio"]);
1293
+ function maskReplayInput(text, element) {
1294
+ var _a;
1295
+ const type = (_a = element == null ? void 0 : element.type) == null ? void 0 : _a.toLowerCase();
1296
+ if (type && UNMASKED_INPUT_TYPES.has(type)) {
1297
+ return text;
1298
+ }
673
1299
  return text ? REPLAY_MASK_VALUE : text;
674
1300
  }
675
- function createMaskTextFn(fullMaskSelector, unmaskSelector) {
676
- return (text, element) => {
1301
+ function createMaskTextFn(fullMaskSelector, unmaskSelector) {
1302
+ return (text, element) => {
1303
+ try {
1304
+ if (fullMaskSelector && isElementFullyMasked(element, fullMaskSelector, unmaskSelector)) {
1305
+ return REPLAY_MASK_VALUE;
1306
+ }
1307
+ return redactSensitiveValues(text);
1308
+ } catch {
1309
+ return REPLAY_MASK_VALUE;
1310
+ }
1311
+ };
1312
+ }
1313
+ const IDLE_CHECKOUT_POLL_MS = 5e3;
1314
+ class RingBufferRecorder {
1315
+ constructor(options = {}) {
1316
+ this.options = options;
1317
+ this.segments = createSegments();
1318
+ this.stopFn = null;
1319
+ this.stopCanvasTracking = null;
1320
+ this.idleCheckoutTimer = null;
1321
+ this.visibilityListener = null;
1322
+ this.tailCutoff = null;
1323
+ this.windowMs = options.windowMs ?? SESSION_REPLAY_WINDOW_MS;
1324
+ }
1325
+ /**
1326
+ * 기록 구간 길이를 바꾼다. 다음 이벤트가 들어올 때와 `snapshot()` 에서 새 길이로 자른다.
1327
+ *
1328
+ * 줄이는 쪽은 그대로 반영되고, 늘리는 쪽은 이미 잘려 나간 앞부분까지 되살리지는 못한다.
1329
+ */
1330
+ setWindowMs(windowMs) {
1331
+ if (Number.isFinite(windowMs) && windowMs > 0) {
1332
+ this.windowMs = windowMs;
1333
+ }
1334
+ }
1335
+ get nowFn() {
1336
+ return this.options.now ?? (() => Date.now());
1337
+ }
1338
+ get checkoutEveryMs() {
1339
+ return this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS;
1340
+ }
1341
+ /** 마지막 segment 가 시작한 시각. 비어 있으면 null. */
1342
+ get lastSegmentStart() {
1343
+ var _a;
1344
+ const last = this.segments[this.segments.length - 1];
1345
+ return ((_a = last == null ? void 0 : last.events[0]) == null ? void 0 : _a.timestamp) ?? null;
1346
+ }
1347
+ /**
1348
+ * 유휴가 길어 경계가 없으면 FullSnapshot 을 강제로 찍는다.
1349
+ *
1350
+ * 숨은 탭에서는 주기 검사를 건너뛴다 — 보이지도 않는 페이지의 DOM 을 주기적으로 직렬화할
1351
+ * 이유가 없다. 대신 다시 보이는 순간 `visibilitychange` 로 여기를 한 번 더 부른다.
1352
+ * 다음 폴링을 기다리면 그 사이에 스냅샷을 뜰 때 과길이 버퍼가 그대로 올라간다.
1353
+ *
1354
+ * rrweb 은 `recording` 이 false 면 던진다. 그 값은 모듈 전역이라 옛 recorder 의 stop 이
1355
+ * 늦게 돌면 살아 있는 recorder 의 인터벌이 5초마다 던지게 된다. 고객 페이지에서 도는
1356
+ * 코드라 밖으로 새면 안 된다.
1357
+ */
1358
+ forceCheckoutIfIdle() {
1359
+ var _a;
1360
+ if (typeof document !== "undefined" && document.hidden) return;
1361
+ const start = this.lastSegmentStart;
1362
+ if (start === null) return;
1363
+ if (this.nowFn() - start < this.checkoutEveryMs) return;
1364
+ const recordFn = this.options.recordFn ?? record;
1365
+ try {
1366
+ (_a = recordFn.takeFullSnapshot) == null ? void 0 : _a.call(recordFn, true);
1367
+ } catch {
1368
+ }
1369
+ }
1370
+ start() {
1371
+ if (this.stopFn) return;
1372
+ const cfg = resolvePrivacyConfig(this.options.privacyConfig);
1373
+ const recordFn = this.options.recordFn ?? record;
1374
+ const now = this.nowFn;
1375
+ this.segments = createSegments();
1376
+ if (this.options.trackCanvasInteractions !== false) {
1377
+ this.stopCanvasTracking = startCanvasInteractionTracking();
1378
+ }
1379
+ const stop = recordFn({
1380
+ emit: (event, isCheckout) => {
1381
+ var _a, _b, _c, _d, _e;
1382
+ this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
1383
+ this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
1384
+ if (event.type === REPLAY_EVENT_TYPE.FullSnapshot) {
1385
+ (_a = this.stopCanvasTracking) == null ? void 0 : _a.captureBaseline();
1386
+ (_c = (_b = this.options).onFullSnapshot) == null ? void 0 : _c.call(_b);
1387
+ }
1388
+ (_e = (_d = this.options).onChange) == null ? void 0 : _e.call(_d);
1389
+ },
1390
+ checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
1391
+ maskAllInputs: cfg.maskAllInputs,
1392
+ blockSelector: cfg.blockSelector,
1393
+ maskTextSelector: cfg.maskTextSelector,
1394
+ maskTextFn: createMaskTextFn(cfg.fullMaskSelector, cfg.unmaskSelector),
1395
+ maskInputFn: maskReplayInput,
1396
+ recordCanvas: cfg.allowCanvasRecording
1397
+ // rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
1398
+ });
1399
+ this.stopFn = typeof stop === "function" ? stop : null;
1400
+ this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
1401
+ if (typeof document !== "undefined") {
1402
+ this.visibilityListener = () => this.forceCheckoutIfIdle();
1403
+ document.addEventListener("visibilitychange", this.visibilityListener);
1404
+ }
1405
+ }
1406
+ stop() {
1407
+ var _a, _b;
1408
+ (_a = this.stopFn) == null ? void 0 : _a.call(this);
1409
+ this.stopFn = null;
1410
+ (_b = this.stopCanvasTracking) == null ? void 0 : _b.call(this);
1411
+ this.stopCanvasTracking = null;
1412
+ if (this.idleCheckoutTimer !== null) {
1413
+ clearInterval(this.idleCheckoutTimer);
1414
+ this.idleCheckoutTimer = null;
1415
+ }
1416
+ if (this.visibilityListener !== null) {
1417
+ document.removeEventListener("visibilitychange", this.visibilityListener);
1418
+ this.visibilityListener = null;
1419
+ }
1420
+ }
1421
+ /**
1422
+ * 이슈 생성 단계의 시작을 표시한다. 이 시각 이후는 스냅샷에서 빠진다.
1423
+ *
1424
+ * 녹화를 멈추지 않고 스냅샷에서 잘라내는 이유는, 멈췄다 다시 켜면 그 사이 mutation 이
1425
+ * 빠져 이후 이벤트가 어긋난 DOM 에 적용되기 때문이다. 폼을 닫고 계속 쓰는 경우가 있으므로
1426
+ * 되돌릴 수 있어야 한다.
1427
+ */
1428
+ markTailCutoff(timestamp = this.nowFn()) {
1429
+ this.tailCutoff = timestamp;
1430
+ }
1431
+ /** 이슈 폼이 제출 없이 닫혔을 때. 다시 전체 구간을 담는다. */
1432
+ clearTailCutoff() {
1433
+ this.tailCutoff = null;
1434
+ }
1435
+ isRecording() {
1436
+ return this.stopFn !== null;
1437
+ }
1438
+ /** 백업이 읽어갈 현재 버퍼. */
1439
+ getSegments() {
1440
+ return this.segments;
1441
+ }
1442
+ /**
1443
+ * 이전 문서의 segment 를 버퍼 앞에 되돌린다.
1444
+ *
1445
+ * **`start()` 뒤에 불러야 한다** — `start()` 는 버퍼를 새로 만들기 때문에 순서가 바뀌면
1446
+ * 되돌린 것이 그대로 지워진다.
1447
+ *
1448
+ * 되돌린 뒤에도 윈도 규칙은 그대로 적용된다. "최근 120초" 라는 정의는 바뀌지 않고,
1449
+ * 문서 경계에서 시계가 0 으로 되돌아가지 않게 되는 것뿐이다. 이전 문서에 오래 머물렀다면
1450
+ * 그 앞부분은 여기서 잘려 나가는 게 맞다.
1451
+ */
1452
+ restore(segments) {
1453
+ var _a, _b;
1454
+ if (segments.length === 0) return;
1455
+ this.segments = trimSegments(
1456
+ [...segments, ...this.segments],
1457
+ this.windowMs,
1458
+ this.nowFn(),
1459
+ this.checkoutEveryMs
1460
+ );
1461
+ (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a);
1462
+ }
1463
+ /**
1464
+ * 현재 버퍼를 업로드 가능한 스냅샷으로 직렬화한다.
1465
+ *
1466
+ * 여기서도 한 번 더 트림한다. emit 시점의 트림만으로는 마지막 이벤트 이후 흐른 시간이
1467
+ * 무제한으로 실린다. 기준 시각은 꼬리 컷오프가 있으면 그 시점이다 — 폼을 채우는 동안
1468
+ * 흐른 시간 때문에 버그 직전 구간이 밀려나면 안 된다.
1469
+ */
1470
+ async snapshot() {
1471
+ const cfg = resolvePrivacyConfig(this.options.privacyConfig);
1472
+ const until = this.tailCutoff ?? this.nowFn();
1473
+ const trimmed = trimSegments(this.segments, this.windowMs, until, this.checkoutEveryMs);
1474
+ const segments = this.tailCutoff === null ? trimmed : dropEventsAfter(trimmed, this.tailCutoff);
1475
+ return serializeSnapshot({
1476
+ segments,
1477
+ redactEvents: redactReplayEvents,
1478
+ rrwebVersion: RRWEB_VERSION,
1479
+ privacyConfigVersion: cfg.version
1480
+ });
1481
+ }
1482
+ }
1483
+ const SOCKET_FRAME_BUDGET_BYTES = 6 * 1024 * 1024;
1484
+ const SOCKET_FRAME_OVERHEAD_BYTES = 100;
1485
+ const SOCKET_FRAME_MAX_BYTES = 256 * 1024;
1486
+ const SOCKET_FRAME_BACKUP_MAX_BYTES = 8 * 1024;
1487
+ const SOCKET_FRAME_BACKUP_MAX_FRAMES = 200;
1488
+ const SOCKET_CONNECTION_LIMIT = 32;
1489
+ const SOCKET_TRANSCRIPT_MAX_BYTES = 5 * 1024 * 1024;
1490
+ const NETWORK_LOG_IMAGE_BODY_LIMIT_BYTES = 64 * 1024;
1491
+ const BODY_DENIED_MIME_PREFIXES = ["font/", "video/", "audio/", "model/", "multipart/"];
1492
+ const STREAMING_MIME_TYPES = /* @__PURE__ */ new Set(["text/event-stream", "application/x-ndjson"]);
1493
+ const BODY_DENIED_MIME_TYPES = /* @__PURE__ */ new Set([
1494
+ "application/wasm",
1495
+ "application/font-woff",
1496
+ "application/font-woff2",
1497
+ "application/x-font-ttf",
1498
+ "application/x-font-otf",
1499
+ "application/vnd.ms-fontobject",
1500
+ "application/pdf",
1501
+ "application/zip",
1502
+ "application/gzip",
1503
+ "application/x-gzip",
1504
+ "application/x-tar"
1505
+ ]);
1506
+ const OCTET_STREAM_MIME = "application/octet-stream";
1507
+ const OCTET_STREAM_BODY_LIMIT_BYTES = 32 * 1024;
1508
+ function isImageMimeType(mimeType) {
1509
+ return Boolean(mimeType == null ? void 0 : mimeType.startsWith("image/"));
1510
+ }
1511
+ function isBodyDeniedMimeType(mimeType) {
1512
+ if (!mimeType) return false;
1513
+ return BODY_DENIED_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix)) || BODY_DENIED_MIME_TYPES.has(mimeType) || STREAMING_MIME_TYPES.has(mimeType);
1514
+ }
1515
+ function getResponseBodyLimitBytes(mimeType) {
1516
+ if (isBodyDeniedMimeType(mimeType)) return 0;
1517
+ if (isImageMimeType(mimeType)) return NETWORK_LOG_IMAGE_BODY_LIMIT_BYTES;
1518
+ if (mimeType === OCTET_STREAM_MIME) return OCTET_STREAM_BODY_LIMIT_BYTES;
1519
+ return NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES;
1520
+ }
1521
+ const NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES = 256 * 1024;
1522
+ const NETWORK_LOG_TRANSPORT_BODY_BUDGET_BYTES = 2 * 1024 * 1024;
1523
+ const NON_ASCII_RUN = /[\u0080-\uffff]+/g;
1524
+ function getUtf8ByteLength(value) {
1525
+ let bytes = value.length;
1526
+ NON_ASCII_RUN.lastIndex = 0;
1527
+ let match2;
1528
+ while ((match2 = NON_ASCII_RUN.exec(value)) !== null) {
1529
+ const end = match2.index + match2[0].length;
1530
+ for (let i2 = match2.index; i2 < end; i2 += 1) {
1531
+ const code = value.charCodeAt(i2);
1532
+ if (code < 2048) {
1533
+ bytes += 1;
1534
+ continue;
1535
+ }
1536
+ bytes += 2;
1537
+ if (code >= 55296 && code <= 56319 && i2 + 1 < end) {
1538
+ const next = value.charCodeAt(i2 + 1);
1539
+ if (next >= 56320 && next <= 57343) i2 += 1;
1540
+ }
1541
+ }
1542
+ }
1543
+ return bytes;
1544
+ }
1545
+ function estimateBase64DecodedBytes(value) {
1546
+ const compact = value.replace(/^data:[^,]*,/i, "").replace(/\s/g, "");
1547
+ if (!compact) return 0;
1548
+ const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
1549
+ return Math.max(0, Math.floor(compact.length * 3 / 4) - padding);
1550
+ }
1551
+ function getCapturedBodySizeBytes(body, encoding) {
1552
+ return encoding === "base64" ? estimateBase64DecodedBytes(body) : getUtf8ByteLength(body);
1553
+ }
1554
+ function isBodyWithinCaptureLimit(sizeBytes) {
1555
+ return sizeBytes <= NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES;
1556
+ }
1557
+ function setOmittedBodyMetadata(log, field, reason, sizeBytes) {
1558
+ if (field === "requestBody") {
1559
+ delete log.requestBody;
1560
+ log.requestBodyOmittedReason = reason;
1561
+ log.requestBodySizeBytes = sizeBytes;
1562
+ return;
1563
+ }
1564
+ delete log.responseBody;
1565
+ delete log.responseBodyEncoding;
1566
+ log.responseBodyOmittedReason = reason;
1567
+ log.responseBodySizeBytes = sizeBytes;
1568
+ }
1569
+ function omitOversizedIndividualBodies(logs) {
1570
+ return logs.map((log) => {
1571
+ const nextLog = { ...log };
1572
+ if (nextLog.requestBody != null) {
1573
+ const sizeBytes = getUtf8ByteLength(nextLog.requestBody);
1574
+ if (sizeBytes > NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES) {
1575
+ setOmittedBodyMetadata(nextLog, "requestBody", "too-large", sizeBytes);
1576
+ }
1577
+ }
1578
+ if (nextLog.responseBody != null) {
1579
+ const sizeBytes = getCapturedBodySizeBytes(
1580
+ nextLog.responseBody,
1581
+ nextLog.responseBodyEncoding
1582
+ );
1583
+ const limitBytes = getResponseBodyLimitBytes(nextLog.responseMimeType);
1584
+ if (limitBytes === 0) {
1585
+ setOmittedBodyMetadata(nextLog, "responseBody", "mime-type", sizeBytes);
1586
+ } else if (sizeBytes > limitBytes) {
1587
+ setOmittedBodyMetadata(nextLog, "responseBody", "too-large", sizeBytes);
1588
+ }
1589
+ }
1590
+ return nextLog;
1591
+ });
1592
+ }
1593
+ function omitNetworkLogBody(log, field, reason, sizeBytes) {
1594
+ const nextLog = { ...log };
1595
+ setOmittedBodyMetadata(nextLog, field, reason, sizeBytes);
1596
+ return nextLog;
1597
+ }
1598
+ function compactNetworkLogsForTransport(logs, budgetBytes = NETWORK_LOG_TRANSPORT_BODY_BUDGET_BYTES) {
1599
+ if (!logs) return void 0;
1600
+ const compacted = omitOversizedIndividualBodies(logs);
1601
+ const candidates = [];
1602
+ compacted.forEach((log, index) => {
1603
+ if (log.requestBody != null) {
1604
+ candidates.push({
1605
+ index,
1606
+ field: "requestBody",
1607
+ timestamp: log.timestamp,
1608
+ sizeBytes: getUtf8ByteLength(log.requestBody)
1609
+ });
1610
+ }
1611
+ if (log.responseBody != null) {
1612
+ candidates.push({
1613
+ index,
1614
+ field: "responseBody",
1615
+ timestamp: log.timestamp,
1616
+ sizeBytes: getUtf8ByteLength(log.responseBody)
1617
+ });
1618
+ }
1619
+ });
1620
+ candidates.sort((a2, b) => {
1621
+ const timestampDiff = b.timestamp - a2.timestamp;
1622
+ if (timestampDiff !== 0) return timestampDiff;
1623
+ const indexDiff = b.index - a2.index;
1624
+ if (indexDiff !== 0) return indexDiff;
1625
+ return a2.field === "responseBody" ? -1 : 1;
1626
+ });
1627
+ let usedBytes = 0;
1628
+ for (const candidate of candidates) {
1629
+ if (usedBytes + candidate.sizeBytes > budgetBytes) {
1630
+ const log = compacted[candidate.index];
1631
+ if (!log) continue;
1632
+ setOmittedBodyMetadata(log, candidate.field, "transport-budget", candidate.sizeBytes);
1633
+ continue;
1634
+ }
1635
+ usedBytes += candidate.sizeBytes;
1636
+ }
1637
+ return compacted;
1638
+ }
1639
+ const SELF_TRAFFIC_CLIENT_HEADER = "x-client-info";
1640
+ const SELF_TRAFFIC_CLIENT_VALUES = ["issue-sticker-sdk", "issue-sticker-extension"];
1641
+ const SELF_TRAFFIC_CLIENT_VALUE_SET = new Set(SELF_TRAFFIC_CLIENT_VALUES);
1642
+ function isSharedLocalHost(hostname) {
1643
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0" || hostname.endsWith(".local") || hostname.endsWith(".localhost");
1644
+ }
1645
+ function readHeader(headers, headerName) {
1646
+ if (!headers) return void 0;
1647
+ const target = headerName.toLowerCase();
1648
+ for (const [key, value] of Object.entries(headers)) {
1649
+ if (key.toLowerCase() === target) return value;
1650
+ }
1651
+ return void 0;
1652
+ }
1653
+ function hasSelfTrafficHeader(headers) {
1654
+ const clientInfo = readHeader(headers, SELF_TRAFFIC_CLIENT_HEADER);
1655
+ return Boolean(clientInfo && SELF_TRAFFIC_CLIENT_VALUE_SET.has(clientInfo));
1656
+ }
1657
+ function isSelfTrafficOrigin(url, apiOrigins) {
1658
+ if (apiOrigins.length === 0) return false;
1659
+ let origin;
1660
+ let hostname;
1661
+ try {
1662
+ const parsed = new URL(url);
1663
+ origin = parsed.origin;
1664
+ hostname = parsed.hostname;
1665
+ } catch {
1666
+ return false;
1667
+ }
1668
+ if (isSharedLocalHost(hostname)) return false;
1669
+ return apiOrigins.some((candidate) => {
677
1670
  try {
678
- if (fullMaskSelector && isElementFullyMasked(element, fullMaskSelector, unmaskSelector)) {
679
- return REPLAY_MASK_VALUE;
680
- }
681
- return redactSensitiveValues(text);
1671
+ return new URL(candidate).origin === origin;
682
1672
  } catch {
683
- return REPLAY_MASK_VALUE;
1673
+ return false;
684
1674
  }
685
- };
1675
+ });
686
1676
  }
687
- 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);
1677
+ const INSTRUMENTATION_FAILURE_LIMIT$1 = 5;
1678
+ const REPATCH_LIMIT$1 = 2;
1679
+ function storedFrameBytes(frames) {
1680
+ var _a;
1681
+ let sum = 0;
1682
+ for (const frame of frames) sum += ((_a = frame.data) == null ? void 0 : _a.length) ?? 0;
1683
+ return sum;
695
1684
  }
696
- class RingBufferRecorder {
697
- constructor(options = {}) {
698
- this.options = options;
699
- this.segments = createSegments();
700
- this.stopFn = null;
701
- this.idleCheckoutTimer = null;
702
- this.visibilityListener = null;
703
- this.tailCutoff = null;
1685
+ const PRUNE_TARGET_RATIO = 0.9;
1686
+ function frameBudgetCost(frames) {
1687
+ return storedFrameBytes(frames) + frames.length * SOCKET_FRAME_OVERHEAD_BYTES;
1688
+ }
1689
+ function toComparableOrigin(url) {
1690
+ return url.replace(/^wss:\/\//i, "https://").replace(/^ws:\/\//i, "http://");
1691
+ }
1692
+ function bytesToBase64$1(bytes) {
1693
+ let binary = "";
1694
+ for (const byte of bytes) binary += String.fromCharCode(byte);
1695
+ return btoa(binary);
1696
+ }
1697
+ function toFrameData(data, dir) {
1698
+ if (typeof data === "string") {
1699
+ return { opcode: "text", size: getUtf8ByteLength(data), text: data };
704
1700
  }
705
- get windowMs() {
706
- return this.options.windowMs ?? SESSION_REPLAY_WINDOW_MS;
1701
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
1702
+ return { opcode: "binary", size: data.size, bytes: new Uint8Array(0) };
707
1703
  }
708
- get nowFn() {
709
- return this.options.now ?? (() => Date.now());
1704
+ if (data instanceof ArrayBuffer) {
1705
+ const bytes = new Uint8Array(data);
1706
+ return { opcode: "binary", size: bytes.length, bytes };
710
1707
  }
711
- get checkoutEveryMs() {
712
- return this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS;
1708
+ if (ArrayBuffer.isView(data)) {
1709
+ const bytes = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1710
+ return { opcode: "binary", size: bytes.length, bytes };
713
1711
  }
714
- /** 마지막 segment 가 시작한 시각. 비어 있으면 null. */
715
- get lastSegmentStart() {
716
- var _a;
717
- const last = this.segments[this.segments.length - 1];
718
- return ((_a = last == null ? void 0 : last.events[0]) == null ? void 0 : _a.timestamp) ?? null;
1712
+ if (dir === "send") {
1713
+ const text = String(data);
1714
+ return { opcode: "text", size: getUtf8ByteLength(text), text };
1715
+ }
1716
+ throw new TypeError("WebSocketCollector: 지원하지 않는 프레임 데이터 형식");
1717
+ }
1718
+ const MASK_PRESLICE_MARGIN_BYTES = SOCKET_FRAME_MAX_BYTES + 16 * 1024;
1719
+ function bytesNeededForBase64Length(charLength) {
1720
+ return Math.ceil(charLength * 3 / 4) + 3;
1721
+ }
1722
+ function maskFrameText(text) {
1723
+ return redactSensitiveValues(redactSensitiveBody(text));
1724
+ }
1725
+ class WebSocketCollector {
1726
+ constructor(apiBaseUrl, options) {
1727
+ this.connections = [];
1728
+ this.originalWebSocket = null;
1729
+ this.patchedWebSocket = null;
1730
+ this.originalSend = null;
1731
+ this.patchedSend = null;
1732
+ this.running = false;
1733
+ this.nextId = 1;
1734
+ this.budgetUsed = 0;
1735
+ this.instrumentationFailures = 0;
1736
+ this.repatchCount = 0;
1737
+ this.apiBaseUrl = typeof apiBaseUrl === "string" && apiBaseUrl.trim().length > 0 ? apiBaseUrl.trim() : null;
1738
+ this.onChange = (options == null ? void 0 : options.onChange) ?? null;
1739
+ }
1740
+ start() {
1741
+ if (this.running) return;
1742
+ if (typeof globalThis.WebSocket !== "function") return;
1743
+ this.running = true;
1744
+ this.instrumentationFailures = 0;
1745
+ this.patch();
1746
+ }
1747
+ stop() {
1748
+ if (!this.running) return;
1749
+ this.running = false;
1750
+ this.unpatch();
719
1751
  }
720
1752
  /**
721
- * 유휴가 길어 경계가 없으면 FullSnapshot 을 강제로 찍는다.
1753
+ * 우리 래퍼가 아직 걸려 있는지 확인하고, 아니면 다시 붙인다.
722
1754
  *
723
- * 숨은 탭에서는 주기 검사를 건너뛴다 — 보이지도 않는 페이지의 DOM 을 주기적으로 직렬화할
724
- * 이유가 없다. 대신 다시 보이는 순간 `visibilitychange` 로 여기를 한 번 더 부른다.
725
- * 다음 폴링을 기다리면 그 사이에 스냅샷을 뜰 때 과길이 버퍼가 그대로 올라간다.
1755
+ * NetworkCollector.ensurePatched 와 같은 이유다 — 다른 계측이 `window.WebSocket` 을
1756
+ * 통째로 갈아치우면 수집이 조용히 멈춘다.
1757
+ */
1758
+ ensurePatched() {
1759
+ if (!this.running) return;
1760
+ if (globalThis.WebSocket === this.patchedWebSocket) return;
1761
+ if (this.repatchCount >= REPATCH_LIMIT$1) return;
1762
+ this.repatchCount += 1;
1763
+ this.patch();
1764
+ }
1765
+ /**
1766
+ * 연결마다 네트워크 로그 한 줄. 네트워크 수집기가 읽을 때마다 여기서 만든다.
726
1767
  *
727
- * rrweb 은 `recording` 이 false 면 던진다. 그 값은 모듈 전역이라 옛 recorder 의 stop 이
728
- * 늦게 돌면 살아 있는 recorder 의 인터벌이 5초마다 던지게 된다. 고객 페이지에서 도는
729
- * 코드라 밖으로 새면 안 된다.
1768
+ * 네트워크 버퍼에 한 번 넣어 두고 고치는 방식은 쓰지 않는다. 줄의 시각이 연결이 열린 시각이라
1769
+ * 오래 열어 둔 연결일수록 버퍼 상한(최근 200건)에 먼저 밀려나, 연결은 살아 있는데 줄만 사라진다.
1770
+ *
1771
+ * `frameCount` 는 버린 프레임까지 센다("실제로 몇 건 오갔나"). `frameBytes` 는 지금 보관 중인
1772
+ * 프레임의 크기 합이다. 버린 프레임의 바이트는 이미 사라졌으니 더할 수 없다.
730
1773
  */
731
- forceCheckoutIfIdle() {
1774
+ getNetworkLogs() {
1775
+ return this.connections.map((connection) => ({
1776
+ url: connection.url,
1777
+ // 핸드셰이크는 GET 으로 나가고 성공이면 101 이다.
1778
+ method: "GET",
1779
+ statusCode: 101,
1780
+ timestamp: connection.openedAt,
1781
+ type: "websocket",
1782
+ socketConnectionId: connection.id,
1783
+ frameCount: connection.frames.length + connection.droppedFrames,
1784
+ frameBytes: connection.frames.reduce((sum, frame) => sum + frame.size, 0),
1785
+ droppedFrames: connection.droppedFrames,
1786
+ ...connection.closeCode != null ? { socketCloseCode: connection.closeCode } : {}
1787
+ }));
1788
+ }
1789
+ getConnections() {
1790
+ return this.connections.map((connection) => ({
1791
+ ...connection,
1792
+ frames: [...connection.frames]
1793
+ }));
1794
+ }
1795
+ clear() {
1796
+ this.connections = [];
1797
+ this.budgetUsed = 0;
1798
+ }
1799
+ /**
1800
+ * 백업에서 되돌린 연결을 지금 목록 앞에 합친다. 상한을 넘으면 오래된 쪽부터 잘린다.
1801
+ *
1802
+ * 예산도 복원 직후 바로 적용한다. 안 하면 다음 프레임이 올 때까지 상한 없이 방치된다.
1803
+ *
1804
+ * **연결 번호가 겹치지 않게 한다.** `nextId` 는 문서마다 1 에서 시작해서, MPA 리로드 뒤
1805
+ * 되돌린 `ws-1` 과 새 문서의 첫 소켓 `ws-1` 이 겹친다. 전사 파일에 같은 id 가 둘 생기면
1806
+ * 뷰어가 옛 연결의 프레임을 보여준다. 복원은 비동기라 그 사이 새 연결이 이미 번호를 받았을 수
1807
+ * 있으므로, `nextId` 를 올리는 것과 별개로 겹친 되돌린 연결은 새 번호로 바꾼다. 형식은 MCP
1808
+ * 조회가 비교하는 `ws-<n>` 그대로 둔다.
1809
+ */
1810
+ restore(connections) {
1811
+ if (connections.length === 0) return;
1812
+ const liveIds = new Set(this.connections.map((connection) => connection.id));
1813
+ this.bumpNextIdPast([...connections, ...this.connections]);
1814
+ const restored = connections.map(
1815
+ (connection) => liveIds.has(connection.id) ? { ...connection, id: `ws-${this.nextId++}` } : connection
1816
+ );
1817
+ this.connections = [...restored, ...this.connections].slice(-SOCKET_CONNECTION_LIMIT);
1818
+ this.budgetUsed = this.sumBudgetCost(this.connections);
1819
+ this.prune();
1820
+ }
1821
+ /** `ws-<n>` 형식 id 중 가장 큰 번호 다음부터 잇도록 `nextId` 를 올린다. 낮추지는 않는다. */
1822
+ bumpNextIdPast(connections) {
1823
+ for (const connection of connections) {
1824
+ const match2 = /^ws-(\d+)$/.exec(connection.id);
1825
+ if (!match2) continue;
1826
+ const numeric = Number(match2[1]);
1827
+ if (numeric >= this.nextId) this.nextId = numeric + 1;
1828
+ }
1829
+ }
1830
+ sumBudgetCost(connections) {
1831
+ return connections.reduce((sum, connection) => sum + frameBudgetCost(connection.frames), 0);
1832
+ }
1833
+ /**
1834
+ * 우리 백엔드로 가는 연결은 레코드를 아예 만들지 않는다.
1835
+ *
1836
+ * 그런 소켓은 `byInstance` 에 null 로 남아, 이후 `send` 프레임도 기록되지 않고 첫 송신 때
1837
+ * 다시 붙잡지도 않는다.
1838
+ */
1839
+ track(socket, url, protocols) {
732
1840
  var _a;
733
- if (typeof document !== "undefined" && document.hidden) return;
734
- const start = this.lastSegmentStart;
735
- if (start === null) return;
736
- if (this.nowFn() - start < this.checkoutEveryMs) return;
737
- const recordFn = this.options.recordFn ?? record;
738
1841
  try {
739
- (_a = recordFn.takeFullSnapshot) == null ? void 0 : _a.call(recordFn, true);
1842
+ if (isSelfTrafficOrigin(toComparableOrigin(url), this.apiBaseUrl ? [this.apiBaseUrl] : []))
1843
+ return null;
1844
+ const connection = {
1845
+ id: `ws-${this.nextId++}`,
1846
+ url: redactUrlSecrets(url),
1847
+ // 서브프로토콜에 토큰을 싣는 패턴(`new WebSocket(url, [token])`)이 실제로 쓰인다.
1848
+ // 값 형태 스크럽까지 거는 이유는 프레임 본문과 같다.
1849
+ protocol: maskFrameText(Array.isArray(protocols) ? protocols[0] : protocols),
1850
+ openedAt: Date.now(),
1851
+ frames: [],
1852
+ droppedFrames: 0,
1853
+ droppedBytes: 0
1854
+ };
1855
+ this.connections.push(connection);
1856
+ if (this.connections.length > SOCKET_CONNECTION_LIMIT) {
1857
+ const closedIndex = this.connections.findIndex((c2) => c2.closedAt != null);
1858
+ const [evicted] = this.connections.splice(closedIndex >= 0 ? closedIndex : 0, 1);
1859
+ this.budgetUsed -= this.sumBudgetCost([evicted]);
1860
+ }
1861
+ socket.addEventListener("message", (event) => {
1862
+ this.addFrame(connection, "recv", event.data);
1863
+ });
1864
+ socket.addEventListener("close", (event) => {
1865
+ var _a2;
1866
+ if (!this.running) return;
1867
+ try {
1868
+ const closeEvent = event;
1869
+ connection.closedAt = Date.now();
1870
+ connection.closeCode = closeEvent.code;
1871
+ connection.closeReason = closeEvent.reason;
1872
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
1873
+ } catch {
1874
+ this.recordInstrumentationFailure();
1875
+ }
1876
+ });
1877
+ socket.addEventListener("error", () => {
1878
+ var _a2;
1879
+ if (!this.running) return;
1880
+ try {
1881
+ connection.errored = true;
1882
+ (_a2 = this.onChange) == null ? void 0 : _a2.call(this);
1883
+ } catch {
1884
+ this.recordInstrumentationFailure();
1885
+ }
1886
+ });
1887
+ (_a = this.onChange) == null ? void 0 : _a.call(this);
1888
+ return connection;
740
1889
  } catch {
1890
+ this.recordInstrumentationFailure();
1891
+ return null;
741
1892
  }
742
1893
  }
743
- start() {
744
- if (this.stopFn) return;
745
- const cfg = resolvePrivacyConfig(this.options.privacyConfig);
746
- const recordFn = this.options.recordFn ?? record;
747
- const now = this.nowFn;
748
- this.segments = createSegments();
749
- const stop = recordFn({
750
- emit: (event, isCheckout) => {
751
- var _a, _b;
752
- this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
753
- this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
754
- (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a);
755
- },
756
- checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
757
- maskAllInputs: cfg.maskAllInputs,
758
- blockSelector: cfg.blockSelector,
759
- maskTextSelector: cfg.maskTextSelector,
760
- maskTextFn: createMaskTextFn(cfg.fullMaskSelector, cfg.unmaskSelector),
761
- maskInputFn: maskReplayInput,
762
- recordCanvas: cfg.allowCanvasRecording
763
- // rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
764
- });
765
- this.stopFn = typeof stop === "function" ? stop : null;
766
- this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
767
- if (typeof document !== "undefined") {
768
- this.visibilityListener = () => this.forceCheckoutIfIdle();
769
- document.addEventListener("visibilitychange", this.visibilityListener);
1894
+ /**
1895
+ * 수집기보다 먼저 만들어진 소켓을 첫 송신 때 붙잡는다.
1896
+ *
1897
+ * 생성자는 이미 지나갔지만 `send` 는 프로토타입을 거치므로 옛 인스턴스도 여기로 온다.
1898
+ * 이때 리스너를 붙이면 그 뒤로는 새로 연 연결과 똑같이 송신·수신을 모두 기록한다. 수집기가
1899
+ * 늦게 붙는 경우(설치·업데이트 전부터 열린 탭)에 연결이 통째로 빠지던 것을 막는다.
1900
+ * socket.io 처럼 주기적으로 핑을 보내는 연결은 한 주기 안에 붙잡힌다. 한 번도 보내지 않는
1901
+ * 수신 전용 연결은 여기서도 못 잡는다.
1902
+ */
1903
+ adopt(socket) {
1904
+ if (!this.running) return null;
1905
+ try {
1906
+ const connection = this.track(socket, socket.url, socket.protocol || void 0);
1907
+ if (connection) connection.adopted = true;
1908
+ return connection;
1909
+ } catch {
1910
+ return null;
770
1911
  }
771
1912
  }
772
- stop() {
773
- var _a;
774
- (_a = this.stopFn) == null ? void 0 : _a.call(this);
775
- this.stopFn = null;
776
- if (this.idleCheckoutTimer !== null) {
777
- clearInterval(this.idleCheckoutTimer);
778
- this.idleCheckoutTimer = null;
779
- }
780
- if (this.visibilityListener !== null) {
781
- document.removeEventListener("visibilitychange", this.visibilityListener);
782
- this.visibilityListener = null;
1913
+ /**
1914
+ * 계측이 스스로 꺼지는 차단기. NetworkCollector.recordInstrumentationFailure 와 같은 규칙이다.
1915
+ *
1916
+ * 계측에서 난 예외가 반복된다는 건 우리가 이해하지 못하는 환경에 들어와 있다는 뜻이다.
1917
+ * 고객 페이지에 계속 손대는 것보다 패치를 걷어내고 로그를 포기하는 쪽이 안전하다.
1918
+ */
1919
+ recordInstrumentationFailure() {
1920
+ this.instrumentationFailures += 1;
1921
+ if (this.instrumentationFailures >= INSTRUMENTATION_FAILURE_LIMIT$1) {
1922
+ this.stop();
1923
+ }
1924
+ }
1925
+ addFrame(connection, dir, data) {
1926
+ var _a;
1927
+ if (!this.running) return;
1928
+ try {
1929
+ const captured = toFrameData(data, dir);
1930
+ const truncated = captured.size > SOCKET_FRAME_MAX_BYTES;
1931
+ const frameData = this.buildFrameData(captured, truncated);
1932
+ connection.frames.push({
1933
+ t: Date.now(),
1934
+ dir,
1935
+ opcode: captured.opcode,
1936
+ size: captured.size,
1937
+ data: frameData,
1938
+ ...truncated ? { truncated: true } : {}
1939
+ });
1940
+ this.budgetUsed += frameData.length + SOCKET_FRAME_OVERHEAD_BYTES;
1941
+ this.prune();
1942
+ (_a = this.onChange) == null ? void 0 : _a.call(this);
1943
+ } catch {
1944
+ this.recordInstrumentationFailure();
783
1945
  }
784
1946
  }
785
1947
  /**
786
- * 이슈 생성 단계의 시작을 표시한다. 이 시각 이후는 스냅샷에서 빠진다.
1948
+ * 프레임 본문을 마스킹·절단해 저장용 문자열로 만든다.
787
1949
  *
788
- * 녹화를 멈추지 않고 스냅샷에서 잘라내는 이유는, 멈췄다 다시 켜면 그 사이 mutation 이
789
- * 빠져 이후 이벤트가 어긋난 DOM 에 적용되기 때문이다. 폼을 닫고 계속 쓰는 경우가 있으므로
790
- * 되돌릴 수 있어야 한다.
1950
+ * **상한 아래면 지금까지와 완전히 같다** — 아래 두 분기 모두 `truncated` 가 false 면
1951
+ * 원본 그대로 마스킹만 거치고 절단 코드를 타지 않는다.
1952
+ *
1953
+ * 상한을 넘을 때만 순서가 바뀐다: 절단은 항상 크기(`byteLength`/`Blob.size`/문자열
1954
+ * 길이)를 이미 아는 값으로 먼저 가늠하고, **변환(base64)·마스킹은 그 뒤 필요한 만큼만**
1955
+ * 돌린다. 텍스트는 마스킹이 끝난 뒤 최종 상한으로 한 번 더 자른다 — 마스킹이 앞부분을
1956
+ * 줄이거나 늘릴 수 있어서다(`MASK_PRESLICE_MARGIN_BYTES` 주석 참조).
791
1957
  */
792
- markTailCutoff(timestamp = this.nowFn()) {
793
- this.tailCutoff = timestamp;
794
- }
795
- /** 이슈 폼이 제출 없이 닫혔을 때. 다시 전체 구간을 담는다. */
796
- clearTailCutoff() {
797
- this.tailCutoff = null;
798
- }
799
- isRecording() {
800
- return this.stopFn !== null;
801
- }
802
- /** 백업이 읽어갈 현재 버퍼. */
803
- getSegments() {
804
- return this.segments;
1958
+ buildFrameData(captured, truncated) {
1959
+ if (captured.opcode === "binary") {
1960
+ if (!truncated) return bytesToBase64$1(captured.bytes);
1961
+ const bytesNeeded = Math.min(
1962
+ captured.bytes.length,
1963
+ bytesNeededForBase64Length(SOCKET_FRAME_MAX_BYTES)
1964
+ );
1965
+ return bytesToBase64$1(captured.bytes.subarray(0, bytesNeeded)).slice(
1966
+ 0,
1967
+ SOCKET_FRAME_MAX_BYTES
1968
+ );
1969
+ }
1970
+ if (!truncated) return maskFrameText(captured.text);
1971
+ const preSliced = captured.text.slice(0, MASK_PRESLICE_MARGIN_BYTES);
1972
+ return maskFrameText(preSliced).slice(0, SOCKET_FRAME_MAX_BYTES);
805
1973
  }
806
1974
  /**
807
- * 이전 문서의 segment 를 버퍼 앞에 되돌린다.
808
- *
809
- * **`start()` 뒤에 불러야 한다** — `start()` 는 버퍼를 새로 만들기 때문에 순서가 바뀌면
810
- * 되돌린 것이 그대로 지워진다.
1975
+ * 예산을 넘으면 오래된 프레임부터 버려 예산의 90% 까지 낮춘다. 예산은 전역이라 연결이 여럿이면
1976
+ * 가장 오래된 프레임을 가진 연결부터 줄인다.
811
1977
  *
812
- * 되돌린 뒤에도 윈도 규칙은 그대로 적용된다. "최근 120초" 라는 정의는 바뀌지 않고,
813
- * 문서 경계에서 시계가 0 으로 되돌아가지 않게 되는 것뿐이다. 이전 문서에 오래 머물렀다면
814
- * 그 앞부분은 여기서 잘려 나가는 게 맞다.
1978
+ * 넘을 때마다 한 건씩 버리지 않는다. 작은 프레임이 수만 건 쌓인 연결에서는 한 건을 버릴 때마다
1979
+ * 배열 전체가 당겨져, 새 프레임 하나에 그만큼의 일이 고객 페이지의 전송 경로에서 돈다. 여유를
1980
+ * 두고 한 번에 버리면 그 일이 수천 건에 한 번으로 준다.
815
1981
  */
816
- restore(segments) {
817
- var _a, _b;
818
- if (segments.length === 0) return;
819
- this.segments = trimSegments(
820
- [...segments, ...this.segments],
821
- this.windowMs,
822
- this.nowFn(),
823
- this.checkoutEveryMs
824
- );
825
- (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a);
1982
+ prune() {
1983
+ var _a;
1984
+ if (this.budgetUsed <= SOCKET_FRAME_BUDGET_BYTES) return;
1985
+ const target = SOCKET_FRAME_BUDGET_BYTES * PRUNE_TARGET_RATIO;
1986
+ while (this.budgetUsed > target) {
1987
+ let oldest = null;
1988
+ let nextOldestT = Number.POSITIVE_INFINITY;
1989
+ for (const connection of this.connections) {
1990
+ const first = connection.frames[0];
1991
+ if (!first) continue;
1992
+ if (!oldest || first.t < oldest.frames[0].t) {
1993
+ if (oldest) nextOldestT = Math.min(nextOldestT, oldest.frames[0].t);
1994
+ oldest = connection;
1995
+ } else {
1996
+ nextOldestT = Math.min(nextOldestT, first.t);
1997
+ }
1998
+ }
1999
+ if (!oldest) break;
2000
+ let count2 = 0;
2001
+ let freed = 0;
2002
+ for (const frame of oldest.frames) {
2003
+ if (this.budgetUsed - freed <= target) break;
2004
+ if (count2 > 0 && frame.t > nextOldestT) break;
2005
+ freed += (((_a = frame.data) == null ? void 0 : _a.length) ?? 0) + SOCKET_FRAME_OVERHEAD_BYTES;
2006
+ count2 += 1;
2007
+ }
2008
+ this.dropOldest(oldest, count2);
2009
+ }
2010
+ }
2011
+ /** 연결의 앞쪽 프레임 `count` 건을 버리고 버린 수·바이트를 센다. */
2012
+ dropOldest(connection, count2) {
2013
+ const removed = connection.frames.splice(0, count2);
2014
+ connection.droppedFrames += removed.length;
2015
+ connection.droppedBytes += storedFrameBytes(removed);
2016
+ this.budgetUsed -= frameBudgetCost(removed);
2017
+ }
2018
+ patch() {
2019
+ this.originalWebSocket = globalThis.WebSocket;
2020
+ const OriginalWebSocket = this.originalWebSocket;
2021
+ const track = this.track.bind(this);
2022
+ const addFrame = this.addFrame.bind(this);
2023
+ const adopt = this.adopt.bind(this);
2024
+ const byInstance = /* @__PURE__ */ new WeakMap();
2025
+ class PatchedWebSocket extends OriginalWebSocket {
2026
+ constructor(url, protocols) {
2027
+ super(url, protocols);
2028
+ byInstance.set(this, track(this, typeof url === "string" ? url : url.href, protocols));
2029
+ }
2030
+ }
2031
+ this.originalSend = OriginalWebSocket.prototype.send;
2032
+ const originalSend = this.originalSend;
2033
+ const patchedSend = function(data) {
2034
+ if (typeof this === "object" && this !== null && !byInstance.has(this)) {
2035
+ byInstance.set(this, adopt(this));
2036
+ }
2037
+ const connection = byInstance.get(this);
2038
+ if (connection) addFrame(connection, "send", data);
2039
+ return originalSend.call(this, data);
2040
+ };
2041
+ OriginalWebSocket.prototype.send = patchedSend;
2042
+ this.patchedSend = patchedSend;
2043
+ globalThis.WebSocket = PatchedWebSocket;
2044
+ this.patchedWebSocket = PatchedWebSocket;
826
2045
  }
827
2046
  /**
828
- * 현재 버퍼를 업로드 가능한 스냅샷으로 직렬화한다.
2047
+ * 우리 래퍼가 아직 현재 값일 때만 되돌린다.
829
2048
  *
830
- * 여기서도 한 번 더 트림한다. emit 시점의 트림만으로는 마지막 이벤트 이후 흐른 시간이
831
- * 무제한으로 실린다. 기준 시각은 꼬리 컷오프가 있으면 그 시점이다 — 폼을 채우는 동안
832
- * 흐른 시간 때문에 버그 직전 구간이 밀려나면 안 된다.
2049
+ * `NetworkCollector.restoreFetch`/`restoreXHR` 과 같은 규칙 — 무조건 되돌리면 우리
2050
+ * 뒤에 붙은 남의 계측(다른 APM 등)의 래퍼가 통째로 사라진다. 남의 것이 위에 있으면
2051
+ * 우리 래퍼는 체인에 남겨 두고 수집만 멈춘다.
833
2052
  */
834
- async snapshot() {
835
- const cfg = resolvePrivacyConfig(this.options.privacyConfig);
836
- const until = this.tailCutoff ?? this.nowFn();
837
- const trimmed = trimSegments(this.segments, this.windowMs, until, this.checkoutEveryMs);
838
- const segments = this.tailCutoff === null ? trimmed : dropEventsAfter(trimmed, this.tailCutoff);
839
- return serializeSnapshot({
840
- segments,
841
- redactEvents: redactReplayEvents,
842
- windowSeconds: spanSeconds(segments),
843
- rrwebVersion: RRWEB_VERSION,
844
- privacyConfigVersion: cfg.version
845
- });
2053
+ unpatch() {
2054
+ if (this.originalWebSocket && globalThis.WebSocket === this.patchedWebSocket) {
2055
+ globalThis.WebSocket = this.originalWebSocket;
2056
+ }
2057
+ if (this.originalWebSocket && this.originalSend && this.originalWebSocket.prototype.send === this.patchedSend) {
2058
+ this.originalWebSocket.prototype.send = this.originalSend;
2059
+ }
2060
+ this.originalWebSocket = null;
2061
+ this.patchedWebSocket = null;
2062
+ this.originalSend = null;
2063
+ this.patchedSend = null;
846
2064
  }
847
2065
  }
848
2066
  const LOG_BACKUP_SESSION_KEY = "__issueStickerLogSession";
@@ -851,13 +2069,22 @@ const DB_VERSION$1 = 1;
851
2069
  const STORE_NAME$1 = "sessions";
852
2070
  const LOG_BACKUP_TTL_MS = 24 * 60 * 60 * 1e3;
853
2071
  const LOG_BACKUP_BUDGET_BYTES = 8 * 1024 * 1024;
854
- function payloadSizeBytes(payload) {
2072
+ function jsonLength(value) {
855
2073
  try {
856
- return JSON.stringify(payload).length;
2074
+ return JSON.stringify(value).length;
857
2075
  } catch {
858
2076
  return Number.POSITIVE_INFINITY;
859
2077
  }
860
2078
  }
2079
+ function dropOldestToFit(items, overBytes) {
2080
+ let dropped = 0;
2081
+ let freed = 0;
2082
+ while (freed < overBytes && dropped < items.length) {
2083
+ freed += jsonLength(items[dropped]) + 1;
2084
+ dropped += 1;
2085
+ }
2086
+ return { kept: items.slice(dropped), freed };
2087
+ }
861
2088
  function withoutBody(log) {
862
2089
  if (log.responseBody === void 0 && log.requestBody === void 0) return log;
863
2090
  const stripped = { ...log };
@@ -872,19 +2099,47 @@ function withoutBody(log) {
872
2099
  }
873
2100
  return stripped;
874
2101
  }
2102
+ function withoutFrameData(connection) {
2103
+ if (connection.frames.length === 0) return connection;
2104
+ return {
2105
+ ...connection,
2106
+ frames: connection.frames.map((frame) => {
2107
+ if (frame.data === void 0) return frame;
2108
+ const stripped = { ...frame };
2109
+ delete stripped.data;
2110
+ return stripped;
2111
+ })
2112
+ };
2113
+ }
2114
+ function withClippedFrames(connection) {
2115
+ const excess = connection.frames.length - SOCKET_FRAME_BACKUP_MAX_FRAMES;
2116
+ const dropped = excess > 0 ? connection.frames.slice(0, excess) : [];
2117
+ const kept = excess > 0 ? connection.frames.slice(excess) : connection.frames;
2118
+ return {
2119
+ ...connection,
2120
+ droppedFrames: connection.droppedFrames + dropped.length,
2121
+ droppedBytes: connection.droppedBytes + storedFrameBytes(dropped),
2122
+ frames: kept.map(
2123
+ (frame) => {
2124
+ var _a;
2125
+ return (((_a = frame.data) == null ? void 0 : _a.length) ?? 0) > SOCKET_FRAME_BACKUP_MAX_BYTES ? { ...frame, data: frame.data.slice(0, SOCKET_FRAME_BACKUP_MAX_BYTES), truncated: true } : frame;
2126
+ }
2127
+ )
2128
+ };
2129
+ }
875
2130
  function trimLogBackupPayload(payload, budgetBytes = LOG_BACKUP_BUDGET_BYTES) {
876
- if (payloadSizeBytes(payload) <= budgetBytes) return payload;
877
- let trimmed = {
878
- network: payload.network.map(withoutBody),
879
- console: payload.console
2131
+ var _a;
2132
+ const clipped = payload.socket ? { ...payload, socket: payload.socket.map(withClippedFrames) } : payload;
2133
+ if (jsonLength(clipped) <= budgetBytes) return clipped;
2134
+ const stripped = {
2135
+ network: clipped.network.map(withoutBody),
2136
+ console: clipped.console,
2137
+ socket: (_a = clipped.socket) == null ? void 0 : _a.map(withoutFrameData)
880
2138
  };
881
- while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.network.length > 0) {
882
- trimmed = { network: trimmed.network.slice(1), console: trimmed.console };
883
- }
884
- while (payloadSizeBytes(trimmed) > budgetBytes && trimmed.console.length > 0) {
885
- trimmed = { network: trimmed.network, console: trimmed.console.slice(1) };
886
- }
887
- return trimmed;
2139
+ const over = jsonLength(stripped) - budgetBytes;
2140
+ const network = dropOldestToFit(stripped.network, over);
2141
+ const consoleLogs = dropOldestToFit(stripped.console, over - network.freed);
2142
+ return { ...stripped, network: network.kept, console: consoleLogs.kept };
888
2143
  }
889
2144
  function requestToPromise$1(request) {
890
2145
  return new Promise((resolve, reject) => {
@@ -1031,13 +2286,29 @@ const DB_NAME = "issue-sticker-replay";
1031
2286
  const DB_VERSION = 1;
1032
2287
  const STORE_NAME = "segments";
1033
2288
  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);
2289
+ const REPLAY_BACKUP_SIZE_BUDGET = 32 * 1024 * 1024;
2290
+ const eventSizes = /* @__PURE__ */ new WeakMap();
2291
+ function sizeOfEvent(event) {
2292
+ const cached = eventSizes.get(event);
2293
+ if (cached !== void 0) return cached;
2294
+ let size;
2295
+ try {
2296
+ size = JSON.stringify(event).length;
2297
+ } catch {
2298
+ size = Number.MAX_SAFE_INTEGER;
2299
+ }
2300
+ eventSizes.set(event, size);
2301
+ return size;
2302
+ }
2303
+ function sizeOfSegments(segments) {
2304
+ return segments.reduce(
2305
+ (total, segment) => total + segment.events.reduce((sum, event) => sum + sizeOfEvent(event), 0),
2306
+ 0
2307
+ );
1037
2308
  }
1038
- function trimReplayBackup(segments, eventBudget = REPLAY_BACKUP_EVENT_BUDGET) {
2309
+ function trimReplayBackup(segments, sizeBudget = REPLAY_BACKUP_SIZE_BUDGET) {
1039
2310
  let trimmed = segments;
1040
- while (trimmed.length > 1 && countEvents(trimmed) > eventBudget) {
2311
+ while (trimmed.length > 1 && sizeOfSegments(trimmed) > sizeBudget) {
1041
2312
  trimmed = trimmed.slice(1);
1042
2313
  }
1043
2314
  return trimmed;
@@ -1051,14 +2322,13 @@ function keepPlayableSegments(segments) {
1051
2322
  return events.every(isReplayEvent) && hasFullSnapshot(events);
1052
2323
  });
1053
2324
  }
1054
- const FULL_SNAPSHOT_TYPE = 2;
1055
2325
  function isReplayEvent(value) {
1056
2326
  if (!value || typeof value !== "object") return false;
1057
2327
  const event = value;
1058
2328
  return typeof event.type === "number" && typeof event.timestamp === "number";
1059
2329
  }
1060
2330
  function hasFullSnapshot(events) {
1061
- return events.some((event) => event.type === FULL_SNAPSHOT_TYPE);
2331
+ return events.some((event) => event.type === REPLAY_EVENT_TYPE.FullSnapshot);
1062
2332
  }
1063
2333
  function requestToPromise(request) {
1064
2334
  return new Promise((resolve, reject) => {
@@ -1238,6 +2508,9 @@ function createReplaySession(options = {}) {
1238
2508
  backup.schedule();
1239
2509
  };
1240
2510
  const recorder = options.recorder ?? new RingBufferRecorder({
2511
+ ...options.trackCanvasInteractions === false ? { trackCanvasInteractions: false } : {},
2512
+ ...options.windowMs === void 0 ? {} : { windowMs: options.windowMs },
2513
+ onFullSnapshot: options.onFullSnapshot,
1241
2514
  privacyConfig: options.privacyConfig,
1242
2515
  onChange: scheduleBackup
1243
2516
  });
@@ -1267,6 +2540,9 @@ function createReplaySession(options = {}) {
1267
2540
  },
1268
2541
  clearReportStart() {
1269
2542
  recorder.clearTailCutoff();
2543
+ },
2544
+ setWindowMs(windowMs) {
2545
+ recorder.setWindowMs(windowMs);
1270
2546
  }
1271
2547
  };
1272
2548
  }
@@ -1298,7 +2574,22 @@ class SessionReplayCaptureController {
1298
2574
  listener(this.snapshot);
1299
2575
  return () => this.listeners.delete(listener);
1300
2576
  }
2577
+ /**
2578
+ * 녹화를 시작하고 첨부 가능 여부를 묻는다.
2579
+ *
2580
+ * 녹화는 응답을 기다리지 않는다. 한도는 첨부할 수 있는지만 정하고, 실제 판정은 이슈를 저장할
2581
+ * 때 서버가 한다. 응답을 기다렸다 시작하면 그 사이 화면이 리플레이에서 빠진다.
2582
+ *
2583
+ * 마스킹 설정은 기본값을 사용한다. rrweb 은 기록하는 순간 가리므로, 테넌트별 마스킹을
2584
+ * 서버에서 받게 되면 녹화 시작을 그 응답 뒤로 옮기거나 받은 뒤 다시 시작해야 한다.
2585
+ */
1301
2586
  initialize() {
2587
+ if (!this.recorder && !this.destroyed) {
2588
+ const recorder = this.options.createRecorder ? this.options.createRecorder() : createReplaySession();
2589
+ recorder.start();
2590
+ this.recorder = recorder;
2591
+ this.setSnapshot({ available: true });
2592
+ }
1302
2593
  if (this.initializePromise) {
1303
2594
  return this.initializePromise;
1304
2595
  }
@@ -1307,31 +2598,25 @@ class SessionReplayCaptureController {
1307
2598
  });
1308
2599
  return this.initializePromise;
1309
2600
  }
2601
+ /**
2602
+ * 첨부 가능 여부와 기록 구간 길이를 다시 묻는다. 로그인·로그아웃처럼 인증이 바뀔 때 부른다.
2603
+ * 녹화는 건드리지 않는다 — 다시 시작하면 그때까지 모은 구간이 사라진다.
2604
+ */
1310
2605
  async refresh() {
1311
2606
  var _a, _b;
1312
2607
  try {
1313
2608
  const data = await this.options.loadCapabilities();
1314
2609
  if (this.destroyed) return;
1315
2610
  const sessionReplay = data == null ? void 0 : data.sessionReplay;
1316
- const canPrepare = Boolean((_a = sessionReplay == null ? void 0 : sessionReplay.canPrepareSessionReplay) == null ? void 0 : _a.enabled);
1317
- const canAttach = Boolean((_b = sessionReplay == null ? void 0 : sessionReplay.canAttachSessionReplay) == null ? void 0 : _b.enabled);
1318
- this.stopRecorder();
1319
- if (!canPrepare) {
1320
- this.setSnapshot({ available: false, canAttach: false, error: null });
1321
- return;
1322
- }
1323
- const recorder = this.createRecorder((sessionReplay == null ? void 0 : sessionReplay.privacyConfig) ?? void 0);
1324
- recorder.start();
1325
- this.recorder = recorder;
1326
- this.setSnapshot({ available: true, canAttach, error: null });
1327
- } catch (error) {
1328
- if (this.destroyed) return;
1329
- this.stopRecorder();
2611
+ const windowSeconds = sessionReplay == null ? void 0 : sessionReplay.windowSeconds;
2612
+ if (typeof windowSeconds === "number") (_a = this.recorder) == null ? void 0 : _a.setWindowMs(windowSeconds * 1e3);
1330
2613
  this.setSnapshot({
1331
- available: false,
1332
- canAttach: false,
1333
- error: readErrorMessage(error)
2614
+ canAttach: Boolean((_b = sessionReplay == null ? void 0 : sessionReplay.canAttachSessionReplay) == null ? void 0 : _b.enabled),
2615
+ error: null
1334
2616
  });
2617
+ } catch (error) {
2618
+ if (this.destroyed) return;
2619
+ this.setSnapshot({ canAttach: false, error: readErrorMessage(error) });
1335
2620
  }
1336
2621
  }
1337
2622
  setEnabled(enabled) {
@@ -1352,198 +2637,19 @@ class SessionReplayCaptureController {
1352
2637
  return this.recorder.snapshot();
1353
2638
  }
1354
2639
  destroy() {
1355
- this.destroyed = true;
1356
- this.stopRecorder();
1357
- this.listeners.clear();
1358
- }
1359
- createRecorder(privacyConfig) {
1360
- return this.options.createRecorder ? this.options.createRecorder(privacyConfig) : createReplaySession({ privacyConfig });
1361
- }
1362
- stopRecorder() {
1363
2640
  var _a;
1364
- (_a = this.recorder) == null ? void 0 : _a.stop();
1365
- this.recorder = null;
1366
- }
1367
- setSnapshot(next) {
1368
- this.snapshot = {
1369
- ...this.snapshot,
1370
- ...next
1371
- };
1372
- this.listeners.forEach((listener) => listener(this.snapshot));
1373
- }
1374
- }
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");
2641
+ this.destroyed = true;
2642
+ (_a = this.recorder) == null ? void 0 : _a.stop();
2643
+ this.recorder = null;
2644
+ this.listeners.clear();
1509
2645
  }
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);
2646
+ setSnapshot(next) {
2647
+ this.snapshot = {
2648
+ ...this.snapshot,
2649
+ ...next
2650
+ };
2651
+ this.listeners.forEach((listener) => listener(this.snapshot));
1540
2652
  }
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
2653
  }
1548
2654
  const SCREENSHOT_DATA_URL_QUALITY = 0.95;
1549
2655
  let nextCaptureRunId = 0;
@@ -1763,10 +2869,18 @@ function blobToDataUrl(blob) {
1763
2869
  reader.readAsDataURL(blob);
1764
2870
  });
1765
2871
  }
2872
+ function captureOrigin(element) {
2873
+ if (element === document.documentElement || element === document.body) {
2874
+ return { x: 0, y: 0 };
2875
+ }
2876
+ const rect = element.getBoundingClientRect();
2877
+ return { x: rect.left, y: rect.top };
2878
+ }
1766
2879
  async function captureWithModernScreenshot(options) {
1767
2880
  const { domToBlob } = await loadModernScreenshot();
1768
2881
  const { element, excludeSelectors = [], width, height, scale = getCaptureScale() } = options;
1769
2882
  await waitForDocumentFonts();
2883
+ const canvasFrames = await grabCanvasFrames(element, excludeSelectors);
1770
2884
  const attributeNames = createCaptureAttributeNames();
1771
2885
  const stickySnapshot = createModernStickySnapshot(element, attributeNames);
1772
2886
  const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors, attributeNames);
@@ -1793,8 +2907,21 @@ async function captureWithModernScreenshot(options) {
1793
2907
  if (height) {
1794
2908
  captureOptions.height = height;
1795
2909
  }
1796
- return await blobToDataUrl(await domToBlob(element, captureOptions));
2910
+ const captured = await domToBlob(element, captureOptions);
2911
+ if (canvasFrames.length === 0) {
2912
+ return await blobToDataUrl(captured);
2913
+ }
2914
+ return await blobToDataUrl(
2915
+ await overlayCanvasFrames(
2916
+ captured,
2917
+ canvasFrames,
2918
+ captureOrigin(element),
2919
+ width ?? element.getBoundingClientRect().width,
2920
+ SCREENSHOT_DATA_URL_QUALITY
2921
+ )
2922
+ );
1797
2923
  } finally {
2924
+ canvasFrames.forEach((frame) => frame.bitmap.close());
1798
2925
  excludeSnapshot.cleanup();
1799
2926
  stickySnapshot.cleanup();
1800
2927
  }
@@ -1879,7 +3006,7 @@ async function calculateSha256Hex(file) {
1879
3006
  }
1880
3007
  function normalizeUploadMimeType(mimeType, mediaType) {
1881
3008
  var _a;
1882
- const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" ? "application/gzip" : "video/webm";
3009
+ const fallback = mediaType === "screenshot" ? "image/webp" : mediaType === "replay" || mediaType === "socket" ? "application/gzip" : "video/webm";
1883
3010
  const normalized = (_a = (mimeType || fallback).split(";", 1)[0]) == null ? void 0 : _a.trim().toLowerCase();
1884
3011
  return normalized && normalized.length > 0 ? normalized : fallback;
1885
3012
  }
@@ -3772,22 +4899,28 @@ function isAppRequest(log) {
3772
4899
  if (log.detailLevel === "timing-only") return false;
3773
4900
  return !RESOURCE_REQUEST_TYPES.has((log.type ?? "").toLowerCase());
3774
4901
  }
4902
+ function takeLast(items, count2) {
4903
+ return count2 > 0 ? items.slice(-count2) : [];
4904
+ }
3775
4905
  function selectNetworkLogsForSubmit(logs) {
3776
4906
  const candidates = logs.filter((log) => !isExcludedRequestType(log.type));
3777
- const detailed = candidates.filter(isAppRequest);
4907
+ const sockets = candidates.filter(isSocketLog);
4908
+ const detailed = candidates.filter((log) => isAppRequest(log) && !isSocketLog(log));
3778
4909
  const timing = candidates.filter((log) => !isAppRequest(log));
3779
- const keptDetailed = detailed.slice(-NETWORK_LOG_SUBMIT_LIMIT);
4910
+ const keptSockets = takeLast(sockets, NETWORK_LOG_SUBMIT_LIMIT);
4911
+ const keptDetailed = takeLast(detailed, NETWORK_LOG_SUBMIT_LIMIT - keptSockets.length);
3780
4912
  const timingQuota = Math.min(
3781
4913
  NETWORK_LOG_TIMING_SUBMIT_CAP,
3782
- NETWORK_LOG_SUBMIT_LIMIT - keptDetailed.length
4914
+ NETWORK_LOG_SUBMIT_LIMIT - keptSockets.length - keptDetailed.length
3783
4915
  );
3784
4916
  const failedTiming = timing.filter((log) => log.statusCode >= 400);
3785
4917
  const restTiming = timing.filter((log) => log.statusCode < 400);
3786
- const keptTiming = [
3787
- ...failedTiming.slice(-timingQuota),
3788
- ...restTiming.slice(-Math.max(0, timingQuota - failedTiming.length))
3789
- ];
3790
- return [...keptDetailed, ...keptTiming].sort((a2, b) => a2.timestamp - b.timestamp);
4918
+ const keptFailed = takeLast(failedTiming, timingQuota);
4919
+ const keptTiming = [...keptFailed, ...takeLast(restTiming, timingQuota - keptFailed.length)];
4920
+ return [...keptSockets, ...keptDetailed, ...keptTiming].sort((a2, b) => a2.timestamp - b.timestamp);
4921
+ }
4922
+ function isSocketLog(log) {
4923
+ return log.type === "websocket";
3791
4924
  }
3792
4925
  const CONSOLE_METHODS = ["log", "warn", "error", "info", "debug"];
3793
4926
  function methodToLogType(method) {
@@ -3991,190 +5124,6 @@ class ConsoleCollector {
3991
5124
  (_a = this.onChange) == null ? void 0 : _a.call(this);
3992
5125
  }
3993
5126
  }
3994
- const NETWORK_LOG_IMAGE_BODY_LIMIT_BYTES = 64 * 1024;
3995
- const BODY_DENIED_MIME_PREFIXES = ["font/", "video/", "audio/", "model/", "multipart/"];
3996
- const STREAMING_MIME_TYPES = /* @__PURE__ */ new Set(["text/event-stream", "application/x-ndjson"]);
3997
- const BODY_DENIED_MIME_TYPES = /* @__PURE__ */ new Set([
3998
- "application/wasm",
3999
- "application/font-woff",
4000
- "application/font-woff2",
4001
- "application/x-font-ttf",
4002
- "application/x-font-otf",
4003
- "application/vnd.ms-fontobject",
4004
- "application/pdf",
4005
- "application/zip",
4006
- "application/gzip",
4007
- "application/x-gzip",
4008
- "application/x-tar"
4009
- ]);
4010
- const OCTET_STREAM_MIME = "application/octet-stream";
4011
- const OCTET_STREAM_BODY_LIMIT_BYTES = 32 * 1024;
4012
- function isImageMimeType(mimeType) {
4013
- return Boolean(mimeType == null ? void 0 : mimeType.startsWith("image/"));
4014
- }
4015
- function isBodyDeniedMimeType(mimeType) {
4016
- if (!mimeType) return false;
4017
- return BODY_DENIED_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix)) || BODY_DENIED_MIME_TYPES.has(mimeType) || STREAMING_MIME_TYPES.has(mimeType);
4018
- }
4019
- function getResponseBodyLimitBytes(mimeType) {
4020
- if (isBodyDeniedMimeType(mimeType)) return 0;
4021
- if (isImageMimeType(mimeType)) return NETWORK_LOG_IMAGE_BODY_LIMIT_BYTES;
4022
- if (mimeType === OCTET_STREAM_MIME) return OCTET_STREAM_BODY_LIMIT_BYTES;
4023
- return NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES;
4024
- }
4025
- const NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES = 256 * 1024;
4026
- const NETWORK_LOG_TRANSPORT_BODY_BUDGET_BYTES = 2 * 1024 * 1024;
4027
- function getUtf8ByteLength(value) {
4028
- let bytes = 0;
4029
- for (let index = 0; index < value.length; index += 1) {
4030
- const codePoint = value.codePointAt(index) ?? 0;
4031
- if (codePoint > 65535) {
4032
- index += 1;
4033
- }
4034
- if (codePoint <= 127) {
4035
- bytes += 1;
4036
- } else if (codePoint <= 2047) {
4037
- bytes += 2;
4038
- } else if (codePoint <= 65535) {
4039
- bytes += 3;
4040
- } else {
4041
- bytes += 4;
4042
- }
4043
- }
4044
- return bytes;
4045
- }
4046
- function estimateBase64DecodedBytes(value) {
4047
- const compact = value.replace(/^data:[^,]*,/i, "").replace(/\s/g, "");
4048
- if (!compact) return 0;
4049
- const padding = compact.endsWith("==") ? 2 : compact.endsWith("=") ? 1 : 0;
4050
- return Math.max(0, Math.floor(compact.length * 3 / 4) - padding);
4051
- }
4052
- function getCapturedBodySizeBytes(body, encoding) {
4053
- return encoding === "base64" ? estimateBase64DecodedBytes(body) : getUtf8ByteLength(body);
4054
- }
4055
- function isBodyWithinCaptureLimit(sizeBytes) {
4056
- return sizeBytes <= NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES;
4057
- }
4058
- function setOmittedBodyMetadata(log, field, reason, sizeBytes) {
4059
- if (field === "requestBody") {
4060
- delete log.requestBody;
4061
- log.requestBodyOmittedReason = reason;
4062
- log.requestBodySizeBytes = sizeBytes;
4063
- return;
4064
- }
4065
- delete log.responseBody;
4066
- delete log.responseBodyEncoding;
4067
- log.responseBodyOmittedReason = reason;
4068
- log.responseBodySizeBytes = sizeBytes;
4069
- }
4070
- function omitOversizedIndividualBodies(logs) {
4071
- return logs.map((log) => {
4072
- const nextLog = { ...log };
4073
- if (nextLog.requestBody != null) {
4074
- const sizeBytes = getUtf8ByteLength(nextLog.requestBody);
4075
- if (sizeBytes > NETWORK_LOG_BODY_CAPTURE_LIMIT_BYTES) {
4076
- setOmittedBodyMetadata(nextLog, "requestBody", "too-large", sizeBytes);
4077
- }
4078
- }
4079
- if (nextLog.responseBody != null) {
4080
- const sizeBytes = getCapturedBodySizeBytes(
4081
- nextLog.responseBody,
4082
- nextLog.responseBodyEncoding
4083
- );
4084
- const limitBytes = getResponseBodyLimitBytes(nextLog.responseMimeType);
4085
- if (limitBytes === 0) {
4086
- setOmittedBodyMetadata(nextLog, "responseBody", "mime-type", sizeBytes);
4087
- } else if (sizeBytes > limitBytes) {
4088
- setOmittedBodyMetadata(nextLog, "responseBody", "too-large", sizeBytes);
4089
- }
4090
- }
4091
- return nextLog;
4092
- });
4093
- }
4094
- function omitNetworkLogBody(log, field, reason, sizeBytes) {
4095
- const nextLog = { ...log };
4096
- setOmittedBodyMetadata(nextLog, field, reason, sizeBytes);
4097
- return nextLog;
4098
- }
4099
- function compactNetworkLogsForTransport(logs, budgetBytes = NETWORK_LOG_TRANSPORT_BODY_BUDGET_BYTES) {
4100
- if (!logs) return void 0;
4101
- const compacted = omitOversizedIndividualBodies(logs);
4102
- const candidates = [];
4103
- compacted.forEach((log, index) => {
4104
- if (log.requestBody != null) {
4105
- candidates.push({
4106
- index,
4107
- field: "requestBody",
4108
- timestamp: log.timestamp,
4109
- sizeBytes: getUtf8ByteLength(log.requestBody)
4110
- });
4111
- }
4112
- if (log.responseBody != null) {
4113
- candidates.push({
4114
- index,
4115
- field: "responseBody",
4116
- timestamp: log.timestamp,
4117
- sizeBytes: getUtf8ByteLength(log.responseBody)
4118
- });
4119
- }
4120
- });
4121
- candidates.sort((a2, b) => {
4122
- const timestampDiff = b.timestamp - a2.timestamp;
4123
- if (timestampDiff !== 0) return timestampDiff;
4124
- const indexDiff = b.index - a2.index;
4125
- if (indexDiff !== 0) return indexDiff;
4126
- return a2.field === "responseBody" ? -1 : 1;
4127
- });
4128
- let usedBytes = 0;
4129
- for (const candidate of candidates) {
4130
- if (usedBytes + candidate.sizeBytes > budgetBytes) {
4131
- const log = compacted[candidate.index];
4132
- if (!log) continue;
4133
- setOmittedBodyMetadata(log, candidate.field, "transport-budget", candidate.sizeBytes);
4134
- continue;
4135
- }
4136
- usedBytes += candidate.sizeBytes;
4137
- }
4138
- return compacted;
4139
- }
4140
- const SELF_TRAFFIC_CLIENT_HEADER = "x-client-info";
4141
- const SELF_TRAFFIC_CLIENT_VALUES = ["issue-sticker-sdk", "issue-sticker-extension"];
4142
- const SELF_TRAFFIC_CLIENT_VALUE_SET = new Set(SELF_TRAFFIC_CLIENT_VALUES);
4143
- function isSharedLocalHost(hostname) {
4144
- return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0" || hostname.endsWith(".local") || hostname.endsWith(".localhost");
4145
- }
4146
- function readHeader(headers, headerName) {
4147
- if (!headers) return void 0;
4148
- const target = headerName.toLowerCase();
4149
- for (const [key, value] of Object.entries(headers)) {
4150
- if (key.toLowerCase() === target) return value;
4151
- }
4152
- return void 0;
4153
- }
4154
- function hasSelfTrafficHeader(headers) {
4155
- const clientInfo = readHeader(headers, SELF_TRAFFIC_CLIENT_HEADER);
4156
- return Boolean(clientInfo && SELF_TRAFFIC_CLIENT_VALUE_SET.has(clientInfo));
4157
- }
4158
- function isSelfTrafficOrigin(url, apiOrigins) {
4159
- if (apiOrigins.length === 0) return false;
4160
- let origin;
4161
- let hostname;
4162
- try {
4163
- const parsed = new URL(url);
4164
- origin = parsed.origin;
4165
- hostname = parsed.hostname;
4166
- } catch {
4167
- return false;
4168
- }
4169
- if (isSharedLocalHost(hostname)) return false;
4170
- return apiOrigins.some((candidate) => {
4171
- try {
4172
- return new URL(candidate).origin === origin;
4173
- } catch {
4174
- return false;
4175
- }
4176
- });
4177
- }
4178
5127
  let registeredPristineFetch = null;
4179
5128
  function registerPristineFetch(fn) {
4180
5129
  if (!registeredPristineFetch) {
@@ -4380,6 +5329,7 @@ async function readXHRResponseBody(xhr, responseMimeType, contentLength) {
4380
5329
  class NetworkCollector {
4381
5330
  constructor(apiBaseUrl, options) {
4382
5331
  this.logs = [];
5332
+ this.socketLogs = null;
4383
5333
  this.originalFetch = null;
4384
5334
  this.patchedFetch = null;
4385
5335
  this.originalXHROpen = null;
@@ -4419,9 +5369,24 @@ class NetworkCollector {
4419
5369
  this.repatchCount += 1;
4420
5370
  this.interceptFetch();
4421
5371
  }
5372
+ /** 보관 로그 전체. 웹소켓 줄이 뒤에 붙는다. 로그를 읽는 쪽은 모두 이것이나 `getFilteredLogs` 를 부른다. */
4422
5373
  getLogs() {
5374
+ var _a;
5375
+ return [...this.logs, ...((_a = this.socketLogs) == null ? void 0 : _a.call(this)) ?? []];
5376
+ }
5377
+ /** 버퍼에 담은 로그만. 백업이 저장하는 것이다. 웹소켓 줄은 소켓 연결과 함께 따로 백업된다. */
5378
+ getBufferedLogs() {
4423
5379
  return [...this.logs];
4424
5380
  }
5381
+ /**
5382
+ * 웹소켓 줄을 내줄 곳을 건다. 줄은 버퍼에 넣지 않고 읽을 때마다 받아 붙인다.
5383
+ *
5384
+ * 버퍼에 넣으면 연결이 열린 뒤 요청이 상한만큼 쌓였을 때 줄이 밀려난다. 오래 열어 두는
5385
+ * 연결일수록 먼저 사라진다.
5386
+ */
5387
+ bindSocketLogs(source) {
5388
+ this.socketLogs = source;
5389
+ }
4425
5390
  /**
4426
5391
  * 패치가 볼 수 없는 요청(문서·리소스 로딩)을 밖에서 넣는다.
4427
5392
  *
@@ -4437,17 +5402,18 @@ class NetworkCollector {
4437
5402
  * 가 통째로 없어진다.
4438
5403
  */
4439
5404
  restore(logs) {
4440
- if (logs.length === 0) return;
4441
- this.logs = [...logs, ...this.logs].slice(-NETWORK_LOG_CLIENT_BUFFER_SIZE);
5405
+ const buffered = logs.filter((log) => log.type !== "websocket");
5406
+ if (buffered.length === 0) return;
5407
+ this.logs = [...buffered, ...this.logs].slice(-NETWORK_LOG_CLIENT_BUFFER_SIZE);
4442
5408
  }
4443
5409
  /**
4444
- * 자기 트래픽을 걷어낸 보관 로그 전체.
5410
+ * 자기 트래픽을 걷어낸 보관 로그 전체. 웹소켓 줄이 뒤에 붙는다.
4445
5411
  *
4446
5412
  * 여기서 제출 상한(50)까지 잘라내면 안 된다. 소비자가 헤더 기반 판정을 한 번 더 돌린 뒤
4447
5413
  * 마지막에 잘라야, 노이즈가 채운 자리 때문에 진짜 로그가 사라지지 않는다.
4448
5414
  */
4449
5415
  getFilteredLogs() {
4450
- return this.logs.filter((log) => !this.isServiceRequest(log.url));
5416
+ return this.getLogs().filter((log) => !this.isServiceRequest(log.url));
4451
5417
  }
4452
5418
  clear() {
4453
5419
  this.logs = [];
@@ -4683,6 +5649,64 @@ class NetworkCollector {
4683
5649
  this.patchedXHR = null;
4684
5650
  }
4685
5651
  }
5652
+ async function sha256Hex(bytes) {
5653
+ const digest = await crypto.subtle.digest("SHA-256", bytes.buffer);
5654
+ return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
5655
+ }
5656
+ function dropOldestFrames(connections, bytesToDrop) {
5657
+ const order = connections.flatMap(
5658
+ (connection, index) => connection.frames.map((frame) => {
5659
+ var _a;
5660
+ return { index, t: frame.t, bytes: ((_a = frame.data) == null ? void 0 : _a.length) ?? 0 };
5661
+ })
5662
+ ).sort((a2, b) => a2.t - b.t);
5663
+ const dropCount = connections.map(() => 0);
5664
+ const dropBytes = connections.map(() => 0);
5665
+ let removed = 0;
5666
+ for (const entry of order) {
5667
+ if (removed >= bytesToDrop) break;
5668
+ dropCount[entry.index] += 1;
5669
+ dropBytes[entry.index] += entry.bytes;
5670
+ removed += entry.bytes;
5671
+ }
5672
+ return connections.map((connection, index) => {
5673
+ const count2 = dropCount[index];
5674
+ if (count2 === 0) return connection;
5675
+ return {
5676
+ ...connection,
5677
+ frames: connection.frames.slice(count2),
5678
+ droppedFrames: connection.droppedFrames + count2,
5679
+ droppedBytes: connection.droppedBytes + dropBytes[index]
5680
+ };
5681
+ });
5682
+ }
5683
+ const FIT_ATTEMPTS = 6;
5684
+ async function serializeSocketTranscript(connections, maxBytes = SOCKET_TRANSCRIPT_MAX_BYTES) {
5685
+ const frameCount = connections.reduce(
5686
+ (sum, connection) => sum + connection.frames.length + connection.droppedFrames,
5687
+ 0
5688
+ );
5689
+ if (connections.length === 0 || frameCount === 0) return null;
5690
+ let current = connections;
5691
+ let payload = gzip_1(JSON.stringify({ connections: current }));
5692
+ for (let attempt = 0; attempt < FIT_ATTEMPTS && payload.byteLength > maxBytes; attempt += 1) {
5693
+ const storedBytes = current.reduce(
5694
+ (sum, connection) => sum + storedFrameBytes(connection.frames),
5695
+ 0
5696
+ );
5697
+ if (storedBytes === 0) break;
5698
+ const overRatio = 1 - maxBytes / payload.byteLength;
5699
+ current = dropOldestFrames(current, storedBytes * Math.min(1, overRatio + 0.1));
5700
+ payload = gzip_1(JSON.stringify({ connections: current }));
5701
+ }
5702
+ return {
5703
+ payload,
5704
+ checksumSha256: await sha256Hex(payload),
5705
+ sizeBytes: payload.byteLength,
5706
+ connectionCount: connections.length,
5707
+ frameCount
5708
+ };
5709
+ }
4686
5710
  var NetworkLogType = /* @__PURE__ */ ((NetworkLogType2) => {
4687
5711
  NetworkLogType2["MAIN_FRAME"] = "main_frame";
4688
5712
  NetworkLogType2["SUB_FRAME"] = "sub_frame";
@@ -14230,6 +15254,20 @@ function filterExcludeServiceRelatedConsoleLogs(consoleLogs) {
14230
15254
  return !isIssueStickerInternalConsoleLog(log) && !isIssueStickerServiceConsoleLog(log);
14231
15255
  });
14232
15256
  }
15257
+ const TRANSCRIPT_CONTENT_TYPE = "application/gzip";
15258
+ async function uploadSocketTranscript(api, snapshot, options = {}) {
15259
+ if (snapshot.sizeBytes > SOCKET_TRANSCRIPT_MAX_BYTES) return null;
15260
+ try {
15261
+ const file = new File([snapshot.payload], "socket-transcript.json.gz", {
15262
+ type: TRANSCRIPT_CONTENT_TYPE
15263
+ });
15264
+ const uploaded = await uploadSignedMediaWithApi(api, file, "socket", options);
15265
+ return { uploadId: uploaded.id };
15266
+ } catch (error) {
15267
+ devLogger.error("socketTranscriptUpload", "소켓 전사 업로드 실패:", error);
15268
+ return null;
15269
+ }
15270
+ }
14233
15271
  function buildExternalUserPayload(user) {
14234
15272
  if (!user) return {};
14235
15273
  return {
@@ -14238,60 +15276,63 @@ function buildExternalUserPayload(user) {
14238
15276
  };
14239
15277
  }
14240
15278
  export {
14241
- getNotionPageMetadata as $,
14242
- getLogBackupSessionId as A,
14243
- createIndexedDbLogBackup as B,
15279
+ Check as $,
15280
+ trimLogBackupPayload as A,
15281
+ createSafeLogBackup as B,
14244
15282
  ConsoleCollector as C,
14245
- PopoverTrigger as D,
14246
- Input as E,
14247
- PopoverContent as F,
14248
- Select$1 as G,
14249
- SelectTrigger as H,
15283
+ getLogBackupSessionId as D,
15284
+ createIndexedDbLogBackup as E,
15285
+ PopoverTrigger as F,
15286
+ Input as G,
15287
+ PopoverContent as H,
14250
15288
  ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
14251
- SelectValue as J,
14252
- SelectContent as K,
15289
+ Select$1 as J,
15290
+ SelectTrigger as K,
14253
15291
  Label as L,
14254
- SelectItem as M,
15292
+ SelectValue as M,
14255
15293
  NetworkCollector as N,
14256
- getJiraMetadata as O,
15294
+ SelectContent as O,
14257
15295
  Popover as P,
14258
- buildJiraIssueFields as Q,
14259
- Button$1 as R,
15296
+ SelectItem as Q,
15297
+ getJiraMetadata as R,
14260
15298
  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 _,
15299
+ buildJiraIssueFields as T,
15300
+ Button$1 as U,
15301
+ Root2 as V,
15302
+ WebSocketCollector as W,
15303
+ Trigger as X,
15304
+ ChevronDown as Y,
15305
+ Portal as Z,
15306
+ Content2 as _,
14269
15307
  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,
15308
+ X as a0,
15309
+ DatePicker as a1,
15310
+ getNotionPageMetadata as a2,
15311
+ buildNotionFieldViews as a3,
15312
+ DirectUploadError as a4,
15313
+ toast as a5,
15314
+ getSnapshot as a6,
15315
+ subscribe as a7,
15316
+ getRegisteredUploadId as a8,
15317
+ ChevronRight as a9,
15318
+ ReactRemoveScroll as aa,
15319
+ hideOthers as ab,
15320
+ useFocusGuards as ac,
15321
+ FocusScope as ad,
15322
+ getExposedJiraFields as ae,
15323
+ isJiraFieldExposed as af,
15324
+ normalizeExposedJiraFields as ag,
15325
+ getPrimaryJiraMapping as ah,
15326
+ createCollection as ai,
15327
+ useDirection as aj,
15328
+ getPrimaryNotionMapping as ak,
15329
+ buttonVariants as al,
15330
+ findElementBySelector as am,
15331
+ findElementByXPath as an,
15332
+ OnboardingProvider as ao,
15333
+ useOnboardingTarget as ap,
15334
+ useOnboardingFlow as aq,
15335
+ PopoverAnchor as ar,
14295
15336
  buildJiraCreateFields as b,
14296
15337
  captureWithModernScreenshot as c,
14297
15338
  directUploadToSignedUrl as d,
@@ -14312,9 +15353,9 @@ export {
14312
15353
  selectNetworkLogsForSubmit as s,
14313
15354
  sanitizeString as t,
14314
15355
  uploadScreenshotDataUrls as u,
14315
- getCurrentPageUrl as v,
14316
- SessionReplayCaptureController as w,
14317
- preloadModernScreenshot as x,
14318
- trimLogBackupPayload as y,
14319
- createSafeLogBackup as z
15356
+ serializeSocketTranscript as v,
15357
+ uploadSocketTranscript as w,
15358
+ getCurrentPageUrl as x,
15359
+ SessionReplayCaptureController as y,
15360
+ preloadModernScreenshot as z
14320
15361
  };