issue-sticker 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { d as OVERLAY_LAYER_INDEX, q as gzip_1, r as record, a as cn, l as useCallbackRef$1, j as useComposedRefs, P as Primitive, h as Presence, n as Portal$1, g as composeEventHandlers, i as createContextScope, o as createSlot, D as DismissableLayer, C as Content, t as createPopperScope, e as useControllableState, v as Root2$1, f as useId, A as Anchor, w as Arrow, b as usePortalContainer, O as OVERLAY_LAYER, s as stopEscapePropagation, k as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, x as clsx, S as Slot$2 } from "./createLucideIcon-C0Ldhp96.js";
1
+ import { d as OVERLAY_LAYER_INDEX, 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, O as OVERLAY_LAYER, s as stopEscapePropagation, n as useLayoutEffect2, V as VISUALLY_HIDDEN_STYLES, c as createLucideIcon, x as clsx, S as Slot$2 } from "./createLucideIcon-BzRMP02h.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";
@@ -282,14 +282,63 @@ class ScreenRecordingController {
282
282
  }
283
283
  const SESSION_REPLAY_BLOCK_CLASS = "rr-block";
284
284
  const SESSION_REPLAY_MASK_CLASS = "rr-mask";
285
+ const SESSION_REPLAY_UNMASK_CLASS = "rr-unmask";
285
286
  const FOREIGN_BLOCK_SELECTORS = [
287
+ // FullStory
286
288
  ".fs-exclude",
289
+ ".fs-block",
290
+ // Sentry — ignore 는 의미가 "입력 이벤트를 기록하지 않는다"라 차단과 어긋나지만,
291
+ // 빼면 이미 그것에 기대고 있는 고객에게 회귀다. 과잉 마스킹은 안전한 방향이라 둔다.
287
292
  ".sentry-block",
288
293
  ".sentry-ignore",
294
+ "[data-sentry-block]",
295
+ // PostHog
289
296
  ".ph-no-capture",
290
- "[data-hj-suppress]"
297
+ // Hotjar
298
+ "[data-hj-suppress]",
299
+ // OpenReplay
300
+ "[data-openreplay-hidden]",
301
+ // Highlight.io
302
+ ".highlight-block",
303
+ ".highlight-ignore",
304
+ // Heap
305
+ "[data-heap-ignore]",
306
+ ".heap-ignore"
307
+ ];
308
+ const FOREIGN_MASK_SELECTORS = [
309
+ // FullStory
310
+ ".fs-mask",
311
+ // Sentry
312
+ ".sentry-mask",
313
+ "[data-sentry-mask]",
314
+ // LogRocket
315
+ "[data-private]",
316
+ // Microsoft Clarity
317
+ "[data-clarity-mask]",
318
+ // Hotjar
319
+ "[data-hj-masked]",
320
+ // OpenReplay
321
+ "[data-openreplay-obscured]",
322
+ // Highlight.io
323
+ ".highlight-mask",
324
+ // ContentSquare
325
+ "[data-cs-mask]",
326
+ "[data-cs-encrypt]",
327
+ // Heap
328
+ "[data-heap-redact-text]",
329
+ "[data-heap-redact-attributes]",
330
+ // Amplitude
331
+ "[data-amp-mask]",
332
+ // Matomo
333
+ "[data-matomo-mask]"
334
+ ];
335
+ const FOREIGN_UNMASK_SELECTORS = [
336
+ ".fs-unmask",
337
+ ".sentry-unmask",
338
+ "[data-clarity-unmask]",
339
+ "[data-amp-unmask]",
340
+ "[data-cs-capture]"
291
341
  ];
292
- const FOREIGN_MASK_SELECTORS = [".fs-mask", ".sentry-mask"];
293
342
  const SESSION_REPLAY_FULL_MASK_SELECTOR = [
294
343
  `.${SESSION_REPLAY_MASK_CLASS}`,
295
344
  ...FOREIGN_MASK_SELECTORS
@@ -298,11 +347,25 @@ const SESSION_REPLAY_BLOCK_SELECTOR = [
298
347
  `.${SESSION_REPLAY_BLOCK_CLASS}`,
299
348
  ...FOREIGN_BLOCK_SELECTORS
300
349
  ].join(",");
350
+ const SESSION_REPLAY_UNMASK_SELECTOR = [
351
+ `.${SESSION_REPLAY_UNMASK_CLASS}`,
352
+ ...FOREIGN_UNMASK_SELECTORS
353
+ ].join(",");
354
+ function isElementFullyMasked(element, maskSelector = SESSION_REPLAY_FULL_MASK_SELECTOR, unmaskSelector = SESSION_REPLAY_UNMASK_SELECTOR) {
355
+ if (!element) return false;
356
+ try {
357
+ const nearest = element.closest(`${maskSelector},${unmaskSelector}`);
358
+ return nearest !== null && nearest.matches(maskSelector);
359
+ } catch {
360
+ return true;
361
+ }
362
+ }
301
363
  const DEFAULT_PRIVACY_CONFIG_V2 = {
302
364
  version: "v2",
303
365
  maskAllInputs: true,
304
366
  maskTextSelector: "*",
305
367
  fullMaskSelector: SESSION_REPLAY_FULL_MASK_SELECTOR,
368
+ unmaskSelector: SESSION_REPLAY_UNMASK_SELECTOR,
306
369
  blockSelector: SESSION_REPLAY_BLOCK_SELECTOR,
307
370
  allowCanvasRecording: false
308
371
  };
@@ -324,7 +387,8 @@ function appendEvent(segments, event, isCheckout) {
324
387
  }
325
388
  return copy;
326
389
  }
327
- function trimSegments(segments, windowMs, now) {
390
+ function trimSegments(segments, windowMs, now, checkoutMs) {
391
+ var _a, _b;
328
392
  const cutoff = now - windowMs;
329
393
  let startIdx = 0;
330
394
  for (let i2 = 0; i2 < segments.length; i2 += 1) {
@@ -334,6 +398,10 @@ function trimSegments(segments, windowMs, now) {
334
398
  startIdx = i2;
335
399
  }
336
400
  }
401
+ const headStart = (_b = (_a = segments[startIdx]) == null ? void 0 : _a.events[0]) == null ? void 0 : _b.timestamp;
402
+ if (checkoutMs !== void 0 && headStart !== void 0 && headStart < cutoff - checkoutMs && startIdx < segments.length - 1) {
403
+ startIdx += 1;
404
+ }
337
405
  return segments.slice(startIdx);
338
406
  }
339
407
  function flattenEvents(segments) {
@@ -518,6 +586,12 @@ const RRN_CANDIDATE_PATTERN = /\b(\d{2})(\d{2})(\d{2})-?([1-8])(\d{6})\b/g;
518
586
  const BRN_CANDIDATE_PATTERN = /\b(\d{3})-?(\d{2})-?(\d{5})\b/g;
519
587
  const BRN_WEIGHTS = [1, 3, 7, 1, 3, 7, 1, 3, 5];
520
588
  const KR_MOBILE_PATTERN = /\b01[016789]-?\d{3,4}-?\d{4}\b/g;
589
+ const EMAIL_PATTERN = /[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+/g;
590
+ function redactEmails(text) {
591
+ if (!text) return text;
592
+ const out = text.replace(EMAIL_PATTERN, REDACTED_BODY_VALUE);
593
+ return out === text ? text : out;
594
+ }
521
595
  function passesBrnChecksum(digits) {
522
596
  let sum = 0;
523
597
  for (let i2 = 0; i2 < 9; i2 += 1) {
@@ -664,17 +738,17 @@ function redactReplayEvents(events) {
664
738
  }
665
739
  return events;
666
740
  }
667
- const RRWEB_VERSION = "2.0.0";
741
+ const RRWEB_VERSION = "2.1.0";
668
742
  const SESSION_REPLAY_WINDOW_MS = 12e4;
669
743
  const SESSION_REPLAY_CHECKOUT_MS = 6e4;
670
744
  const REPLAY_MASK_VALUE = "[redacted]";
671
745
  function maskReplayInput(text) {
672
746
  return text ? REPLAY_MASK_VALUE : text;
673
747
  }
674
- function createMaskTextFn(fullMaskSelector) {
748
+ function createMaskTextFn(fullMaskSelector, unmaskSelector) {
675
749
  return (text, element) => {
676
750
  try {
677
- if (fullMaskSelector && (element == null ? void 0 : element.closest(fullMaskSelector))) {
751
+ if (fullMaskSelector && isElementFullyMasked(element, fullMaskSelector, unmaskSelector)) {
678
752
  return REPLAY_MASK_VALUE;
679
753
  }
680
754
  return redactSensitiveValues(text);
@@ -684,12 +758,21 @@ function createMaskTextFn(fullMaskSelector) {
684
758
  };
685
759
  }
686
760
  const IDLE_CHECKOUT_POLL_MS = 5e3;
761
+ function spanSeconds(segments) {
762
+ var _a, _b;
763
+ const events = flattenEvents(segments);
764
+ const first = (_a = events[0]) == null ? void 0 : _a.timestamp;
765
+ const last = (_b = events[events.length - 1]) == null ? void 0 : _b.timestamp;
766
+ if (first === void 0 || last === void 0) return 0;
767
+ return Math.round((last - first) / 1e3);
768
+ }
687
769
  class RingBufferRecorder {
688
770
  constructor(options = {}) {
689
771
  this.options = options;
690
772
  this.segments = createSegments();
691
773
  this.stopFn = null;
692
774
  this.idleCheckoutTimer = null;
775
+ this.visibilityListener = null;
693
776
  this.tailCutoff = null;
694
777
  }
695
778
  get windowMs() {
@@ -710,8 +793,13 @@ class RingBufferRecorder {
710
793
  /**
711
794
  * 유휴가 길어 경계가 없으면 FullSnapshot 을 강제로 찍는다.
712
795
  *
713
- * 숨은 탭에서는 건너뛴다 — 보이지도 않는 페이지의 DOM 을 주기적으로 직렬화할 이유가 없고,
714
- * 다시 보이게 되면 그때 다음 검사에서 찍힌다.
796
+ * 숨은 탭에서는 주기 검사를 건너뛴다 — 보이지도 않는 페이지의 DOM 을 주기적으로 직렬화할
797
+ * 이유가 없다. 대신 다시 보이는 순간 `visibilitychange` 로 여기를 한 번 더 부른다.
798
+ * 다음 폴링을 기다리면 그 사이에 스냅샷을 뜰 때 과길이 버퍼가 그대로 올라간다.
799
+ *
800
+ * rrweb 은 `recording` 이 false 면 던진다. 그 값은 모듈 전역이라 옛 recorder 의 stop 이
801
+ * 늦게 돌면 살아 있는 recorder 의 인터벌이 5초마다 던지게 된다. 고객 페이지에서 도는
802
+ * 코드라 밖으로 새면 안 된다.
715
803
  */
716
804
  forceCheckoutIfIdle() {
717
805
  var _a;
@@ -720,7 +808,10 @@ class RingBufferRecorder {
720
808
  if (start === null) return;
721
809
  if (this.nowFn() - start < this.checkoutEveryMs) return;
722
810
  const recordFn = this.options.recordFn ?? record;
723
- (_a = recordFn.takeFullSnapshot) == null ? void 0 : _a.call(recordFn, true);
811
+ try {
812
+ (_a = recordFn.takeFullSnapshot) == null ? void 0 : _a.call(recordFn, true);
813
+ } catch {
814
+ }
724
815
  }
725
816
  start() {
726
817
  if (this.stopFn) return;
@@ -732,20 +823,24 @@ class RingBufferRecorder {
732
823
  emit: (event, isCheckout) => {
733
824
  var _a, _b;
734
825
  this.segments = appendEvent(this.segments, event, Boolean(isCheckout));
735
- this.segments = trimSegments(this.segments, this.windowMs, now());
826
+ this.segments = trimSegments(this.segments, this.windowMs, now(), this.checkoutEveryMs);
736
827
  (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a);
737
828
  },
738
829
  checkoutEveryNms: this.options.checkoutEveryNms ?? SESSION_REPLAY_CHECKOUT_MS,
739
830
  maskAllInputs: cfg.maskAllInputs,
740
831
  blockSelector: cfg.blockSelector,
741
832
  maskTextSelector: cfg.maskTextSelector,
742
- maskTextFn: createMaskTextFn(cfg.fullMaskSelector),
833
+ maskTextFn: createMaskTextFn(cfg.fullMaskSelector, cfg.unmaskSelector),
743
834
  maskInputFn: maskReplayInput,
744
835
  recordCanvas: cfg.allowCanvasRecording
745
836
  // rrweb 옵션 타입이 버전별로 달라 캐스팅한다.
746
837
  });
747
838
  this.stopFn = typeof stop === "function" ? stop : null;
748
839
  this.idleCheckoutTimer = setInterval(() => this.forceCheckoutIfIdle(), IDLE_CHECKOUT_POLL_MS);
840
+ if (typeof document !== "undefined") {
841
+ this.visibilityListener = () => this.forceCheckoutIfIdle();
842
+ document.addEventListener("visibilitychange", this.visibilityListener);
843
+ }
749
844
  }
750
845
  stop() {
751
846
  var _a;
@@ -755,6 +850,10 @@ class RingBufferRecorder {
755
850
  clearInterval(this.idleCheckoutTimer);
756
851
  this.idleCheckoutTimer = null;
757
852
  }
853
+ if (this.visibilityListener !== null) {
854
+ document.removeEventListener("visibilitychange", this.visibilityListener);
855
+ this.visibilityListener = null;
856
+ }
758
857
  }
759
858
  /**
760
859
  * 이슈 생성 단계의 시작을 표시한다. 이 시각 이후는 스냅샷에서 빠진다.
@@ -790,17 +889,30 @@ class RingBufferRecorder {
790
889
  restore(segments) {
791
890
  var _a, _b;
792
891
  if (segments.length === 0) return;
793
- this.segments = trimSegments([...segments, ...this.segments], this.windowMs, this.nowFn());
892
+ this.segments = trimSegments(
893
+ [...segments, ...this.segments],
894
+ this.windowMs,
895
+ this.nowFn(),
896
+ this.checkoutEveryMs
897
+ );
794
898
  (_b = (_a = this.options).onChange) == null ? void 0 : _b.call(_a);
795
899
  }
796
- /** 현재 버퍼를 업로드 가능한 스냅샷으로 직렬화한다. */
900
+ /**
901
+ * 현재 버퍼를 업로드 가능한 스냅샷으로 직렬화한다.
902
+ *
903
+ * 여기서도 한 번 더 트림한다. emit 시점의 트림만으로는 마지막 이벤트 이후 흐른 시간이
904
+ * 무제한으로 실린다. 기준 시각은 꼬리 컷오프가 있으면 그 시점이다 — 폼을 채우는 동안
905
+ * 흐른 시간 때문에 버그 직전 구간이 밀려나면 안 된다.
906
+ */
797
907
  async snapshot() {
798
908
  const cfg = resolvePrivacyConfig(this.options.privacyConfig);
799
- const segments = this.tailCutoff === null ? this.segments : dropEventsAfter(this.segments, this.tailCutoff);
909
+ const until = this.tailCutoff ?? this.nowFn();
910
+ const trimmed = trimSegments(this.segments, this.windowMs, until, this.checkoutEveryMs);
911
+ const segments = this.tailCutoff === null ? trimmed : dropEventsAfter(trimmed, this.tailCutoff);
800
912
  return serializeSnapshot({
801
913
  segments,
802
914
  redactEvents: redactReplayEvents,
803
- windowSeconds: Math.round(this.windowMs / 1e3),
915
+ windowSeconds: spanSeconds(segments),
804
916
  rrwebVersion: RRWEB_VERSION,
805
917
  privacyConfigVersion: cfg.version
806
918
  });
@@ -1231,27 +1343,11 @@ function createReplaySession(options = {}) {
1231
1343
  }
1232
1344
  };
1233
1345
  }
1234
- const STORAGE_KEY = "issue-sticker-session-replay-attach:v1";
1235
- function getReplayAttachPreference() {
1236
- try {
1237
- const raw = window.localStorage.getItem(STORAGE_KEY);
1238
- if (raw === null) return true;
1239
- return raw !== "off";
1240
- } catch {
1241
- return true;
1242
- }
1243
- }
1244
- function setReplayAttachPreference(enabled) {
1245
- try {
1246
- window.localStorage.setItem(STORAGE_KEY, enabled ? "on" : "off");
1247
- } catch {
1248
- }
1249
- }
1250
1346
  function createInitialSnapshot() {
1251
1347
  return {
1252
1348
  available: false,
1253
1349
  canAttach: false,
1254
- enabled: getReplayAttachPreference(),
1350
+ enabled: true,
1255
1351
  error: null
1256
1352
  };
1257
1353
  }
@@ -1312,7 +1408,6 @@ class SessionReplayCaptureController {
1312
1408
  }
1313
1409
  }
1314
1410
  setEnabled(enabled) {
1315
- setReplayAttachPreference(enabled);
1316
1411
  this.setSnapshot({ enabled });
1317
1412
  }
1318
1413
  /** 이슈 폼이 열렸다. 이후 구간은 리플레이에서 빠진다. */
@@ -1350,10 +1445,189 @@ class SessionReplayCaptureController {
1350
1445
  this.listeners.forEach((listener) => listener(this.snapshot));
1351
1446
  }
1352
1447
  }
1448
+ function forEachElementDeep(root, callback) {
1449
+ if (root instanceof Element) {
1450
+ callback(root);
1451
+ if (root.shadowRoot) {
1452
+ forEachElementDeep(root.shadowRoot, callback);
1453
+ }
1454
+ }
1455
+ Array.from(root.children).forEach((child) => {
1456
+ forEachElementDeep(child, callback);
1457
+ });
1458
+ }
1459
+ const SCREENSHOT_FULL_MASK_SELECTOR = SESSION_REPLAY_FULL_MASK_SELECTOR;
1460
+ const SCREENSHOT_BLOCK_SELECTOR = SESSION_REPLAY_BLOCK_SELECTOR;
1461
+ const SCREENSHOT_MASK_BACKGROUND = "rgba(148, 148, 148, 0.28)";
1462
+ const SCREENSHOT_BLOCK_BACKGROUND = "rgba(148, 148, 148, 0.85)";
1463
+ const MASK_LABEL_WIDTH = 44;
1464
+ const MASK_LABEL_HEIGHT = 16;
1465
+ const MASK_LABEL_IMAGE = `url("data:image/svg+xml;charset=utf-8,${encodeURIComponent(
1466
+ `<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>`
1467
+ )}")`;
1468
+ const MASK_LABEL_MIN_WIDTH = MASK_LABEL_WIDTH + 8;
1469
+ const MASK_LABEL_MIN_HEIGHT = MASK_LABEL_HEIGHT + 8;
1470
+ const TEXT_HIDING_PROPERTIES = [
1471
+ ["color", "transparent"],
1472
+ ["-webkit-text-fill-color", "transparent"],
1473
+ ["text-shadow", "none"],
1474
+ ["fill", "transparent"]
1475
+ ];
1476
+ const REPLACED_MEDIA_SELECTOR = "img, svg, video, canvas, picture, iframe, object, embed";
1477
+ const VALUE_ELEMENT_SELECTOR = "input, textarea, select";
1478
+ const SENSITIVE_AUTOCOMPLETE_TOKENS = /* @__PURE__ */ new Set([
1479
+ // 자격증명
1480
+ "current-password",
1481
+ "new-password",
1482
+ "one-time-code",
1483
+ // 결제
1484
+ "cc-number",
1485
+ "cc-exp",
1486
+ "cc-exp-month",
1487
+ "cc-exp-year",
1488
+ "cc-csc",
1489
+ "cc-name",
1490
+ "cc-type",
1491
+ "cc-given-name",
1492
+ "cc-additional-name",
1493
+ "cc-family-name",
1494
+ // 신원
1495
+ "name",
1496
+ "given-name",
1497
+ "additional-name",
1498
+ "family-name",
1499
+ "honorific-prefix",
1500
+ "honorific-suffix",
1501
+ "nickname",
1502
+ "bday",
1503
+ "bday-day",
1504
+ "bday-month",
1505
+ "bday-year",
1506
+ "sex",
1507
+ // 주소
1508
+ "street-address",
1509
+ "address-line1",
1510
+ "address-line2",
1511
+ "address-line3",
1512
+ "postal-code",
1513
+ // 연락처
1514
+ "tel",
1515
+ "tel-national",
1516
+ "tel-local",
1517
+ "tel-area-code",
1518
+ "email",
1519
+ "impp"
1520
+ ]);
1521
+ function redactScreenshotText(text) {
1522
+ return redactEmails(redactSensitiveValues(text));
1523
+ }
1524
+ function matchesSafely(element, selector) {
1525
+ try {
1526
+ return element.matches(selector);
1527
+ } catch {
1528
+ return false;
1529
+ }
1530
+ }
1531
+ function closestSafely(element, selector) {
1532
+ if (!element) return null;
1533
+ try {
1534
+ return element.closest(selector);
1535
+ } catch {
1536
+ return null;
1537
+ }
1538
+ }
1539
+ function hasSensitiveValueHint(element) {
1540
+ var _a;
1541
+ if (((_a = element.getAttribute("type")) == null ? void 0 : _a.toLowerCase()) === "password") {
1542
+ return true;
1543
+ }
1544
+ const autocomplete = element.getAttribute("autocomplete");
1545
+ if (!autocomplete) return false;
1546
+ return autocomplete.toLowerCase().split(/\s+/).some((token) => SENSITIVE_AUTOCOMPLETE_TOKENS.has(token));
1547
+ }
1548
+ const overwriteProperty = (element, name, value) => {
1549
+ const style = element.style;
1550
+ style == null ? void 0 : style.setProperty(name, value, "important");
1551
+ };
1552
+ function coverMaskedRegion(region, setProperty) {
1553
+ setProperty(region, "background-color", SCREENSHOT_MASK_BACKGROUND);
1554
+ forEachElementDeep(region, (node) => {
1555
+ if (!isElementFullyMasked(node)) return;
1556
+ TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(node, name, value));
1557
+ if (matchesSafely(node, REPLACED_MEDIA_SELECTOR)) {
1558
+ setProperty(node, "visibility", "hidden");
1559
+ }
1560
+ });
1561
+ }
1562
+ function parsePixels(value) {
1563
+ const parsed = Number.parseFloat(value ?? "");
1564
+ return Number.isFinite(parsed) ? parsed : 0;
1565
+ }
1566
+ function fitsMaskLabel(region) {
1567
+ const rect = region.getBoundingClientRect();
1568
+ const style = region.style;
1569
+ const width = rect.width || parsePixels(style == null ? void 0 : style.width);
1570
+ const height = rect.height || parsePixels(style == null ? void 0 : style.height);
1571
+ return width >= MASK_LABEL_MIN_WIDTH && height >= MASK_LABEL_MIN_HEIGHT;
1572
+ }
1573
+ function coverBlockedRegion(region, setProperty) {
1574
+ const labeled = fitsMaskLabel(region);
1575
+ setProperty(region, "visibility", "visible");
1576
+ setProperty(region, "background-color", SCREENSHOT_BLOCK_BACKGROUND);
1577
+ setProperty(region, "background-image", labeled ? MASK_LABEL_IMAGE : "none");
1578
+ if (labeled) {
1579
+ setProperty(region, "background-repeat", "no-repeat");
1580
+ setProperty(region, "background-position", "center");
1581
+ setProperty(region, "background-size", "auto");
1582
+ }
1583
+ TEXT_HIDING_PROPERTIES.forEach(([name, value]) => setProperty(region, name, value));
1584
+ setProperty(region, "pointer-events", "none");
1585
+ forEachElementDeep(region, (descendant) => {
1586
+ if (descendant !== region) {
1587
+ setProperty(descendant, "visibility", "hidden");
1588
+ }
1589
+ });
1590
+ }
1591
+ function coverPrivateRegions(root, setProperty) {
1592
+ const blocked = [];
1593
+ const masked = [];
1594
+ forEachElementDeep(root, (element) => {
1595
+ if (matchesSafely(element, SCREENSHOT_BLOCK_SELECTOR)) {
1596
+ blocked.push(element);
1597
+ return;
1598
+ }
1599
+ if (matchesSafely(element, SCREENSHOT_FULL_MASK_SELECTOR) && isElementFullyMasked(element)) {
1600
+ masked.push(element);
1601
+ }
1602
+ });
1603
+ masked.forEach((region) => coverMaskedRegion(region, setProperty));
1604
+ blocked.forEach((region) => coverBlockedRegion(region, setProperty));
1605
+ }
1606
+ function applyScreenshotPrivacyMask(clonedRoot) {
1607
+ coverPrivateRegions(clonedRoot, overwriteProperty);
1608
+ const walker = clonedRoot.ownerDocument.createTreeWalker(clonedRoot, NodeFilter.SHOW_TEXT);
1609
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1610
+ const text = node.nodeValue;
1611
+ if (!text) continue;
1612
+ node.nodeValue = redactScreenshotText(text);
1613
+ }
1614
+ clonedRoot.querySelectorAll(VALUE_ELEMENT_SELECTOR).forEach((element) => {
1615
+ const value = element.getAttribute("value");
1616
+ if (!value) return;
1617
+ const hidden = closestSafely(element, SCREENSHOT_BLOCK_SELECTOR) !== null || isElementFullyMasked(element) || hasSensitiveValueHint(element);
1618
+ element.setAttribute("value", hidden ? "" : redactScreenshotText(value));
1619
+ });
1620
+ }
1353
1621
  const SCREENSHOT_DATA_URL_QUALITY = 0.95;
1354
- const CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE = "data-issue-sticker-capture-exclude-node-id";
1355
- const MODERN_STICKY_NODE_ID_ATTRIBUTE = "data-issue-sticker-modern-sticky-node-id";
1356
- const MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE = "data-issue-sticker-modern-scroll-ancestor-id";
1622
+ let nextCaptureRunId = 0;
1623
+ function createCaptureAttributeNames() {
1624
+ const runId = ++nextCaptureRunId;
1625
+ return {
1626
+ excludeNodeId: `data-issue-sticker-capture-exclude-node-id-${runId}`,
1627
+ stickyNodeId: `data-issue-sticker-modern-sticky-node-id-${runId}`,
1628
+ scrollAncestorId: `data-issue-sticker-modern-scroll-ancestor-id-${runId}`
1629
+ };
1630
+ }
1357
1631
  let modernScreenshotModule = null;
1358
1632
  function loadModernScreenshot() {
1359
1633
  if (!modernScreenshotModule) {
@@ -1378,17 +1652,6 @@ async function waitForDocumentFonts() {
1378
1652
  }
1379
1653
  await fontFaceSet.ready;
1380
1654
  }
1381
- function forEachElementDeep(root, callback) {
1382
- if (root instanceof Element) {
1383
- callback(root);
1384
- if (root.shadowRoot) {
1385
- forEachElementDeep(root.shadowRoot, callback);
1386
- }
1387
- }
1388
- Array.from(root.children).forEach((child) => {
1389
- forEachElementDeep(child, callback);
1390
- });
1391
- }
1392
1655
  function isScrollableOnAxis(element, axis) {
1393
1656
  const style = getComputedStyle(element);
1394
1657
  const overflowValue = axis === "y" ? style.overflowY || style.overflow : style.overflowX || style.overflow;
@@ -1412,28 +1675,27 @@ function parseStickyInset(value) {
1412
1675
  const parsed = Number.parseFloat(value);
1413
1676
  return Number.isFinite(parsed) ? parsed : null;
1414
1677
  }
1415
- function createExcludeElementSnapshot(root, excludeSelectors) {
1678
+ function createExcludeElementSnapshot(root, excludeSelectors, attributeNames) {
1416
1679
  if (excludeSelectors.length === 0) {
1417
1680
  return {
1418
1681
  applyToClone: () => void 0,
1419
1682
  cleanup: () => void 0
1420
1683
  };
1421
1684
  }
1685
+ const { excludeNodeId: excludeAttribute } = attributeNames;
1422
1686
  const markedElements = [];
1423
1687
  let nextCaptureId = 0;
1424
1688
  forEachElementDeep(root, (element) => {
1425
1689
  if (!excludeSelectors.some((selector) => element.matches(selector))) {
1426
1690
  return;
1427
1691
  }
1428
- const captureId = String(++nextCaptureId);
1429
- const previousValue = element.getAttribute(CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE);
1430
- element.setAttribute(CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE, captureId);
1431
- markedElements.push({ element, previousValue });
1692
+ element.setAttribute(excludeAttribute, String(++nextCaptureId));
1693
+ markedElements.push(element);
1432
1694
  });
1433
1695
  return {
1434
1696
  applyToClone(clonedRoot) {
1435
1697
  forEachElementDeep(clonedRoot, (element) => {
1436
- if (!element.hasAttribute(CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE)) {
1698
+ if (!element.hasAttribute(excludeAttribute)) {
1437
1699
  return;
1438
1700
  }
1439
1701
  const styledElement = element;
@@ -1442,35 +1704,29 @@ function createExcludeElementSnapshot(root, excludeSelectors) {
1442
1704
  styledElement.style.setProperty("visibility", "hidden", "important");
1443
1705
  styledElement.style.setProperty("pointer-events", "none", "important");
1444
1706
  }
1445
- element.removeAttribute(CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE);
1707
+ element.removeAttribute(excludeAttribute);
1446
1708
  });
1447
1709
  },
1448
1710
  cleanup() {
1449
- markedElements.forEach(({ element, previousValue }) => {
1450
- if (previousValue === null) {
1451
- element.removeAttribute(CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE);
1452
- return;
1453
- }
1454
- element.setAttribute(CAPTURE_EXCLUDE_NODE_ID_ATTRIBUTE, previousValue);
1455
- });
1711
+ markedElements.forEach((element) => element.removeAttribute(excludeAttribute));
1456
1712
  }
1457
1713
  };
1458
1714
  }
1459
- function createModernStickySnapshot(root) {
1715
+ function createModernStickySnapshot(root, attributeNames) {
1716
+ const { stickyNodeId: stickyAttribute, scrollAncestorId: scrollAncestorAttribute } = attributeNames;
1460
1717
  const stickyOverlays = /* @__PURE__ */ new Map();
1461
1718
  const markedStickyElements = [];
1462
1719
  const markedScrollAncestors = [];
1463
1720
  let nextStickyId = 0;
1464
1721
  let nextScrollAncestorId = 0;
1465
1722
  function ensureScrollAncestorId(element) {
1466
- const existingId = element.getAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE);
1723
+ const existingId = element.getAttribute(scrollAncestorAttribute);
1467
1724
  if (existingId) {
1468
1725
  return existingId;
1469
1726
  }
1470
1727
  const ancestorId = `scroll-${++nextScrollAncestorId}`;
1471
- const previousValue = element.getAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE);
1472
- element.setAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE, ancestorId);
1473
- markedScrollAncestors.push({ element, previousValue });
1728
+ element.setAttribute(scrollAncestorAttribute, ancestorId);
1729
+ markedScrollAncestors.push(element);
1474
1730
  return ancestorId;
1475
1731
  }
1476
1732
  forEachElementDeep(root, (node) => {
@@ -1497,7 +1753,6 @@ function createModernStickySnapshot(root) {
1497
1753
  return;
1498
1754
  }
1499
1755
  const captureId = String(++nextStickyId);
1500
- const previousValue = node.getAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE);
1501
1756
  const ancestorId = ensureScrollAncestorId(scrollAncestor);
1502
1757
  stickyOverlays.set(captureId, {
1503
1758
  ancestorId,
@@ -1506,8 +1761,8 @@ function createModernStickySnapshot(root) {
1506
1761
  width: nodeRect.width,
1507
1762
  height: nodeRect.height
1508
1763
  });
1509
- node.setAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE, captureId);
1510
- markedStickyElements.push({ element: node, previousValue });
1764
+ node.setAttribute(stickyAttribute, captureId);
1765
+ markedStickyElements.push(node);
1511
1766
  });
1512
1767
  return {
1513
1768
  applyToClone(clonedRoot) {
@@ -1517,11 +1772,11 @@ function createModernStickySnapshot(root) {
1517
1772
  if (!(node instanceof HTMLElement)) {
1518
1773
  return;
1519
1774
  }
1520
- const captureId = node.getAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE);
1775
+ const captureId = node.getAttribute(stickyAttribute);
1521
1776
  if (captureId) {
1522
1777
  stickyById.set(captureId, node);
1523
1778
  }
1524
- const scrollAncestorId = node.getAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE);
1779
+ const scrollAncestorId = node.getAttribute(scrollAncestorAttribute);
1525
1780
  if (scrollAncestorId) {
1526
1781
  scrollAncestorById.set(scrollAncestorId, node);
1527
1782
  }
@@ -1536,8 +1791,8 @@ function createModernStickySnapshot(root) {
1536
1791
  if (!(overlay instanceof HTMLElement)) {
1537
1792
  return;
1538
1793
  }
1539
- overlay.removeAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE);
1540
- overlay.removeAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE);
1794
+ overlay.removeAttribute(stickyAttribute);
1795
+ overlay.removeAttribute(scrollAncestorAttribute);
1541
1796
  overlay.style.position = "absolute";
1542
1797
  overlay.style.top = `${top}px`;
1543
1798
  overlay.style.left = `${left}px`;
@@ -1563,36 +1818,34 @@ function createModernStickySnapshot(root) {
1563
1818
  if (!(node instanceof HTMLElement)) {
1564
1819
  return;
1565
1820
  }
1566
- node.removeAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE);
1567
- node.removeAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE);
1821
+ node.removeAttribute(stickyAttribute);
1822
+ node.removeAttribute(scrollAncestorAttribute);
1568
1823
  });
1569
1824
  },
1570
1825
  cleanup() {
1571
- markedStickyElements.forEach(({ element, previousValue }) => {
1572
- if (previousValue === null) {
1573
- element.removeAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE);
1574
- return;
1575
- }
1576
- element.setAttribute(MODERN_STICKY_NODE_ID_ATTRIBUTE, previousValue);
1577
- });
1578
- markedScrollAncestors.forEach(({ element, previousValue }) => {
1579
- if (previousValue === null) {
1580
- element.removeAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE);
1581
- return;
1582
- }
1583
- element.setAttribute(MODERN_SCROLL_ANCESTOR_ID_ATTRIBUTE, previousValue);
1584
- });
1826
+ markedStickyElements.forEach((element) => element.removeAttribute(stickyAttribute));
1827
+ markedScrollAncestors.forEach((element) => element.removeAttribute(scrollAncestorAttribute));
1585
1828
  }
1586
1829
  };
1587
1830
  }
1831
+ function blobToDataUrl(blob) {
1832
+ return new Promise((resolve, reject) => {
1833
+ const reader = new FileReader();
1834
+ reader.onload = () => resolve(String(reader.result));
1835
+ reader.onerror = () => reject(reader.error ?? new Error("스크린샷을 dataUrl 로 변환하지 못했습니다."));
1836
+ reader.readAsDataURL(blob);
1837
+ });
1838
+ }
1588
1839
  async function captureWithModernScreenshot(options) {
1589
- const { domToWebp } = await loadModernScreenshot();
1840
+ const { domToBlob } = await loadModernScreenshot();
1590
1841
  const { element, excludeSelectors = [], width, height, scale = getCaptureScale() } = options;
1591
1842
  await waitForDocumentFonts();
1592
- const stickySnapshot = createModernStickySnapshot(element);
1593
- const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors);
1843
+ const attributeNames = createCaptureAttributeNames();
1844
+ const stickySnapshot = createModernStickySnapshot(element, attributeNames);
1845
+ const excludeSnapshot = createExcludeElementSnapshot(element, excludeSelectors, attributeNames);
1594
1846
  try {
1595
1847
  const captureOptions = {
1848
+ type: "image/webp",
1596
1849
  scale,
1597
1850
  quality: SCREENSHOT_DATA_URL_QUALITY,
1598
1851
  features: {
@@ -1604,6 +1857,7 @@ async function captureWithModernScreenshot(options) {
1604
1857
  }
1605
1858
  stickySnapshot.applyToClone(cloned);
1606
1859
  excludeSnapshot.applyToClone(cloned);
1860
+ applyScreenshotPrivacyMask(cloned);
1607
1861
  }
1608
1862
  };
1609
1863
  if (width) {
@@ -1612,12 +1866,53 @@ async function captureWithModernScreenshot(options) {
1612
1866
  if (height) {
1613
1867
  captureOptions.height = height;
1614
1868
  }
1615
- return await domToWebp(element, captureOptions);
1869
+ return await blobToDataUrl(await domToBlob(element, captureOptions));
1616
1870
  } finally {
1617
1871
  excludeSnapshot.cleanup();
1618
1872
  stickySnapshot.cleanup();
1619
1873
  }
1620
1874
  }
1875
+ const ISSUE_STICKER_INTERNAL_LOG_PREFIX = "__ISSUE_STICKER_INTERNAL_DEBUG__";
1876
+ function isInternalDebugLog$1(message2) {
1877
+ return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
1878
+ }
1879
+ function isIssueStickerInternalConsoleLog$1(log) {
1880
+ return isInternalDebugLog$1(log.message);
1881
+ }
1882
+ const __vite_import_meta_env__ = { "DEV": false };
1883
+ function isDevLoggingEnabled() {
1884
+ const env = __vite_import_meta_env__;
1885
+ {
1886
+ return env.DEV;
1887
+ }
1888
+ }
1889
+ function isInternalDebugLog(message2) {
1890
+ return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
1891
+ }
1892
+ function write(method, scope, args) {
1893
+ if (!isDevLoggingEnabled()) {
1894
+ return;
1895
+ }
1896
+ const log = console[method];
1897
+ log(ISSUE_STICKER_INTERNAL_LOG_PREFIX, `[${scope}]`, ...args);
1898
+ }
1899
+ const devLogger = {
1900
+ log(scope, ...args) {
1901
+ write("log", scope, args);
1902
+ },
1903
+ info(scope, ...args) {
1904
+ write("info", scope, args);
1905
+ },
1906
+ warn(scope, ...args) {
1907
+ write("warn", scope, args);
1908
+ },
1909
+ error(scope, ...args) {
1910
+ write("error", scope, args);
1911
+ },
1912
+ debug(scope, ...args) {
1913
+ write("debug", scope, args);
1914
+ }
1915
+ };
1621
1916
  class DirectUploadError extends Error {
1622
1917
  constructor(message2, status, data) {
1623
1918
  super(message2);
@@ -3515,13 +3810,6 @@ async function uploadScreenshotDataUrls(screenshotDataUrls, uploadSignedMedia, i
3515
3810
  }
3516
3811
  throw new Error("스크린샷 업로드 응답에 URL이 없습니다.");
3517
3812
  }
3518
- const ISSUE_STICKER_INTERNAL_LOG_PREFIX = "__ISSUE_STICKER_INTERNAL_DEBUG__";
3519
- function isInternalDebugLog$1(message2) {
3520
- return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
3521
- }
3522
- function isIssueStickerInternalConsoleLog$1(log) {
3523
- return isInternalDebugLog$1(log.message);
3524
- }
3525
3813
  function getCurrentPageUrl() {
3526
3814
  if (typeof window === "undefined") {
3527
3815
  return "";
@@ -13477,40 +13765,6 @@ function buildIssueIntegrationsPayload({
13477
13765
  }
13478
13766
  return payload.jira || payload.notion ? payload : void 0;
13479
13767
  }
13480
- const __vite_import_meta_env__ = { "DEV": false };
13481
- function isDevLoggingEnabled() {
13482
- const env = __vite_import_meta_env__;
13483
- {
13484
- return env.DEV;
13485
- }
13486
- }
13487
- function isInternalDebugLog(message2) {
13488
- return Boolean(message2 == null ? void 0 : message2.includes(ISSUE_STICKER_INTERNAL_LOG_PREFIX));
13489
- }
13490
- function write(method, scope, args) {
13491
- if (!isDevLoggingEnabled()) {
13492
- return;
13493
- }
13494
- const log = console[method];
13495
- log(ISSUE_STICKER_INTERNAL_LOG_PREFIX, `[${scope}]`, ...args);
13496
- }
13497
- const devLogger = {
13498
- log(scope, ...args) {
13499
- write("log", scope, args);
13500
- },
13501
- info(scope, ...args) {
13502
- write("info", scope, args);
13503
- },
13504
- warn(scope, ...args) {
13505
- write("warn", scope, args);
13506
- },
13507
- error(scope, ...args) {
13508
- write("error", scope, args);
13509
- },
13510
- debug(scope, ...args) {
13511
- write("debug", scope, args);
13512
- }
13513
- };
13514
13768
  function buildGuestOwnerQuery(mode, externalUserId) {
13515
13769
  if (mode !== "public") return "";
13516
13770
  if (!externalUserId) return "";
@@ -13636,65 +13890,66 @@ function buildExternalUserPayload(user) {
13636
13890
  };
13637
13891
  }
13638
13892
  export {
13639
- DatePicker as $,
13893
+ X as $,
13640
13894
  trimLogBackupPayload as A,
13641
13895
  createSafeLogBackup as B,
13642
13896
  ConsoleCollector as C,
13643
13897
  getLogBackupSessionId as D,
13644
13898
  createIndexedDbLogBackup as E,
13645
13899
  PopoverTrigger as F,
13646
- PopoverContent as G,
13647
- Select$1 as H,
13648
- Input as I,
13649
- SelectTrigger as J,
13650
- SelectValue as K,
13900
+ Input as G,
13901
+ PopoverContent as H,
13902
+ ISSUE_STICKER_INTERNAL_LOG_PREFIX as I,
13903
+ Select$1 as J,
13904
+ SelectTrigger as K,
13651
13905
  Label as L,
13652
- SelectContent as M,
13906
+ SelectValue as M,
13653
13907
  NetworkCollector as N,
13654
- SelectItem as O,
13908
+ SelectContent as O,
13655
13909
  Popover as P,
13656
- getJiraMetadata as Q,
13657
- buildJiraIssueFields as R,
13910
+ SelectItem as Q,
13911
+ getJiraMetadata as R,
13658
13912
  ScreenRecordingController as S,
13659
- Button$1 as T,
13660
- Root2 as U,
13661
- Trigger as V,
13662
- ChevronDown as W,
13663
- Portal as X,
13664
- Content2 as Y,
13665
- Check as Z,
13666
- X as _,
13913
+ buildJiraIssueFields as T,
13914
+ Button$1 as U,
13915
+ Root2 as V,
13916
+ Trigger as W,
13917
+ ChevronDown as X,
13918
+ Portal as Y,
13919
+ Content2 as Z,
13920
+ Check as _,
13667
13921
  uploadSignedMediaWithApi as a,
13668
- getNotionPageMetadata as a0,
13669
- buildNotionFieldViews as a1,
13670
- DirectUploadError as a2,
13671
- getRegisteredUploadId as a3,
13672
- subscribe as a4,
13922
+ DatePicker as a0,
13923
+ getNotionPageMetadata as a1,
13924
+ buildNotionFieldViews as a2,
13925
+ DirectUploadError as a3,
13926
+ toast as a4,
13673
13927
  getSnapshot as a5,
13674
- toast as a6,
13675
- ChevronRight as a7,
13676
- getExposedJiraFields as a8,
13677
- isJiraFieldExposed as a9,
13678
- getPrimaryJiraMapping as aa,
13679
- createCollection as ab,
13680
- useDirection as ac,
13681
- ReactRemoveScroll as ad,
13682
- hideOthers as ae,
13683
- useFocusGuards as af,
13684
- FocusScope as ag,
13685
- getPrimaryNotionMapping as ah,
13686
- buttonVariants as ai,
13687
- findElementBySelector as aj,
13688
- findElementByXPath as ak,
13689
- OnboardingProvider as al,
13690
- useOnboardingTarget as am,
13691
- useOnboardingFlow as an,
13692
- ISSUE_STICKER_HOST_ELEMENT_ID as ao,
13693
- getIssueStickerRuntimeOwner as ap,
13694
- ISSUE_STICKER_RUNTIME_OWNER as aq,
13695
- createRuntimeHostElement as ar,
13696
- attachHostTypingGuard as as,
13697
- ISSUE_STICKER_EXTENSION_RUNTIME_READY_EVENT as at,
13928
+ subscribe as a6,
13929
+ getRegisteredUploadId as a7,
13930
+ ChevronRight as a8,
13931
+ ReactRemoveScroll as a9,
13932
+ hideOthers as aa,
13933
+ useFocusGuards as ab,
13934
+ FocusScope as ac,
13935
+ getExposedJiraFields as ad,
13936
+ isJiraFieldExposed as ae,
13937
+ getPrimaryJiraMapping as af,
13938
+ createCollection as ag,
13939
+ useDirection as ah,
13940
+ getPrimaryNotionMapping as ai,
13941
+ buttonVariants as aj,
13942
+ findElementBySelector as ak,
13943
+ findElementByXPath as al,
13944
+ OnboardingProvider as am,
13945
+ useOnboardingTarget as an,
13946
+ useOnboardingFlow as ao,
13947
+ ISSUE_STICKER_HOST_ELEMENT_ID as ap,
13948
+ getIssueStickerRuntimeOwner as aq,
13949
+ ISSUE_STICKER_RUNTIME_OWNER as ar,
13950
+ createRuntimeHostElement as as,
13951
+ attachHostTypingGuard as at,
13952
+ ISSUE_STICKER_EXTENSION_RUNTIME_READY_EVENT as au,
13698
13953
  normalizeJiraFieldId as b,
13699
13954
  captureWithModernScreenshot as c,
13700
13955
  directUploadToSignedUrl as d,