@solhun/feedback-kit-core 0.6.1 → 0.8.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.
- package/dist/index.cjs +177 -12
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -9
- package/dist/index.d.ts +141 -9
- package/dist/index.js +173 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -151,6 +151,7 @@ function normalizeDiagnostics(snap) {
|
|
|
151
151
|
if (!snap || !Array.isArray(snap.network) || !Array.isArray(snap.logs)) {
|
|
152
152
|
return null;
|
|
153
153
|
}
|
|
154
|
+
if (!Array.isArray(snap.routes)) return { ...snap, routes: [] };
|
|
154
155
|
return snap;
|
|
155
156
|
}
|
|
156
157
|
function safeCall(fn, fallback) {
|
|
@@ -625,6 +626,130 @@ var RingBuffer = class {
|
|
|
625
626
|
}
|
|
626
627
|
};
|
|
627
628
|
|
|
629
|
+
// src/route-trail.ts
|
|
630
|
+
var ROUTE_TRAIL_LIMIT = 20;
|
|
631
|
+
var ROUTE_TRAIL_KEY = "feedback-kit:route-trail";
|
|
632
|
+
function normalizeRoutePath(route) {
|
|
633
|
+
const raw = typeof route === "string" ? route : "";
|
|
634
|
+
const cut = [raw.indexOf("?"), raw.indexOf("#")].filter((i) => i >= 0);
|
|
635
|
+
const trimmed = (cut.length > 0 ? raw.slice(0, Math.min(...cut)) : raw).trim();
|
|
636
|
+
return trimmed.length > 0 ? trimmed : "/";
|
|
637
|
+
}
|
|
638
|
+
function parseEntries(raw) {
|
|
639
|
+
if (!raw) return [];
|
|
640
|
+
let parsed;
|
|
641
|
+
try {
|
|
642
|
+
parsed = JSON.parse(raw);
|
|
643
|
+
} catch {
|
|
644
|
+
return [];
|
|
645
|
+
}
|
|
646
|
+
if (!Array.isArray(parsed)) return [];
|
|
647
|
+
const out = [];
|
|
648
|
+
for (const item of parsed) {
|
|
649
|
+
if (typeof item !== "object" || item === null) continue;
|
|
650
|
+
const e = item;
|
|
651
|
+
if (typeof e.to !== "string" || typeof e.at !== "number") continue;
|
|
652
|
+
out.push({
|
|
653
|
+
at: e.at,
|
|
654
|
+
from: typeof e.from === "string" ? e.from : null,
|
|
655
|
+
to: e.to,
|
|
656
|
+
count: typeof e.count === "number" && e.count > 0 ? Math.floor(e.count) : 1
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
return out;
|
|
660
|
+
}
|
|
661
|
+
var RouteTrail = class {
|
|
662
|
+
constructor(opts = {}) {
|
|
663
|
+
this.entries = [];
|
|
664
|
+
this.unwatch = null;
|
|
665
|
+
this.limit = opts.limit !== void 0 && opts.limit > 0 ? Math.floor(opts.limit) : ROUTE_TRAIL_LIMIT;
|
|
666
|
+
this.key = opts.key ?? ROUTE_TRAIL_KEY;
|
|
667
|
+
this.now = opts.now ?? (() => Date.now());
|
|
668
|
+
this.storage = opts.storage ?? null;
|
|
669
|
+
this.restore();
|
|
670
|
+
}
|
|
671
|
+
/** 감시자를 붙인다. 이미 붙어 있으면 먼저 끊는다(감시자가 둘이면 이동이 두 번 기록된다). */
|
|
672
|
+
watch(watcher) {
|
|
673
|
+
this.unwatchRoutes();
|
|
674
|
+
try {
|
|
675
|
+
this.unwatch = watcher((route) => this.record(route));
|
|
676
|
+
} catch {
|
|
677
|
+
this.unwatch = null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
/** 감시자를 뗀다. 버퍼 내용은 그대로 남는다. */
|
|
681
|
+
unwatchRoutes() {
|
|
682
|
+
if (!this.unwatch) return;
|
|
683
|
+
const off = this.unwatch;
|
|
684
|
+
this.unwatch = null;
|
|
685
|
+
try {
|
|
686
|
+
off();
|
|
687
|
+
} catch {
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* 이동 하나를 기록한다.
|
|
692
|
+
*
|
|
693
|
+
* 같은 경로로 연속 이동하면 새 줄을 만들지 않고 `count` 만 올린다. 판정은 **직전 항목의
|
|
694
|
+
* `to`** 와 비교한다 — 새로고침 직후 같은 화면에서 다시 시작해도 줄이 늘지 않는다.
|
|
695
|
+
*/
|
|
696
|
+
record(route) {
|
|
697
|
+
const to = normalizeRoutePath(route);
|
|
698
|
+
const last = this.entries[this.entries.length - 1];
|
|
699
|
+
if (last && last.to === to) {
|
|
700
|
+
last.count += 1;
|
|
701
|
+
} else {
|
|
702
|
+
this.entries.push({ at: this.now(), from: last ? last.to : null, to, count: 1 });
|
|
703
|
+
const overflow = this.entries.length - this.limit;
|
|
704
|
+
if (overflow > 0) this.entries.splice(0, overflow);
|
|
705
|
+
}
|
|
706
|
+
this.persist();
|
|
707
|
+
}
|
|
708
|
+
/** 지금까지의 궤적. 사본이라 이후 이동이 이미 만든 제보를 바꾸지 않는다. */
|
|
709
|
+
snapshot() {
|
|
710
|
+
return this.entries.map((e) => ({ ...e }));
|
|
711
|
+
}
|
|
712
|
+
get size() {
|
|
713
|
+
return this.entries.length;
|
|
714
|
+
}
|
|
715
|
+
/** 버퍼와 저장분을 모두 비운다. */
|
|
716
|
+
clear() {
|
|
717
|
+
this.entries = [];
|
|
718
|
+
this.persist();
|
|
719
|
+
}
|
|
720
|
+
/** 저장소를 나중에 붙인다. 이미 저장돼 있던 궤적이 있으면 그것을 이어받는다. */
|
|
721
|
+
attachStorage(storage) {
|
|
722
|
+
this.storage = storage;
|
|
723
|
+
if (!storage) return;
|
|
724
|
+
const restored = parseEntries(this.readRaw());
|
|
725
|
+
if (restored.length > 0) {
|
|
726
|
+
this.entries = [...restored, ...this.entries].slice(-this.limit);
|
|
727
|
+
}
|
|
728
|
+
this.persist();
|
|
729
|
+
}
|
|
730
|
+
// ── 내부: 저장소 ──────────────────────────────────────────
|
|
731
|
+
// 저장은 **거들 뿐**이다. 던지면 이동 한 번에 앱이 죽는다(프라이빗 모드 등에서
|
|
732
|
+
// getItem/setItem 이 실제로 던진다). 그래서 전부 삼키고 메모리로 계속 간다.
|
|
733
|
+
readRaw() {
|
|
734
|
+
if (!this.storage) return null;
|
|
735
|
+
try {
|
|
736
|
+
return this.storage.getItem(this.key);
|
|
737
|
+
} catch {
|
|
738
|
+
return null;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
restore() {
|
|
742
|
+
this.entries = parseEntries(this.readRaw()).slice(-this.limit);
|
|
743
|
+
}
|
|
744
|
+
persist() {
|
|
745
|
+
if (!this.storage) return;
|
|
746
|
+
try {
|
|
747
|
+
this.storage.setItem(this.key, JSON.stringify(this.entries));
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
};
|
|
752
|
+
|
|
628
753
|
// src/diagnostics.ts
|
|
629
754
|
var NETWORK_BUFFER_LIMIT = 30;
|
|
630
755
|
var LOG_BUFFER_LIMIT = 50;
|
|
@@ -671,6 +796,11 @@ var DiagnosticsCollector = class {
|
|
|
671
796
|
opts.networkLimit ?? NETWORK_BUFFER_LIMIT
|
|
672
797
|
);
|
|
673
798
|
this.logs = new RingBuffer(opts.logLimit ?? LOG_BUFFER_LIMIT);
|
|
799
|
+
this.routes = new RouteTrail({
|
|
800
|
+
limit: opts.routeLimit ?? ROUTE_TRAIL_LIMIT,
|
|
801
|
+
storage: opts.routeStorage ?? null,
|
|
802
|
+
now: () => this.now()
|
|
803
|
+
});
|
|
674
804
|
}
|
|
675
805
|
/** 글로벌 fetch/XHR/console 을 패치한다. 이미 설치했으면 no-op. */
|
|
676
806
|
install(opts = {}) {
|
|
@@ -683,6 +813,8 @@ var DiagnosticsCollector = class {
|
|
|
683
813
|
this.patchFetch();
|
|
684
814
|
this.patchXhr();
|
|
685
815
|
this.patchConsole();
|
|
816
|
+
if (opts.routeStorage !== void 0) this.routes.attachStorage(opts.routeStorage);
|
|
817
|
+
if (opts.watchRoutes) this.routes.watch(opts.watchRoutes);
|
|
686
818
|
}
|
|
687
819
|
/** 글로벌 패치를 모두 원복한다. 버퍼 내용은 그대로 남는다. */
|
|
688
820
|
uninstall() {
|
|
@@ -709,18 +841,24 @@ var DiagnosticsCollector = class {
|
|
|
709
841
|
this.originalLog = null;
|
|
710
842
|
this.excludeMatcher = null;
|
|
711
843
|
this.fetchDepth = 0;
|
|
844
|
+
this.routes.unwatchRoutes();
|
|
712
845
|
}
|
|
713
846
|
/**
|
|
714
847
|
* 지금까지 모인 것을 그대로 스냅샷으로 낸다.
|
|
715
848
|
* 반환 배열은 사본이라 이후 수집이 제보 페이로드를 바꾸지 않는다.
|
|
716
849
|
*/
|
|
717
850
|
snapshot() {
|
|
718
|
-
return {
|
|
851
|
+
return {
|
|
852
|
+
network: this.network.toArray(),
|
|
853
|
+
logs: this.logs.toArray(),
|
|
854
|
+
routes: this.routes.snapshot()
|
|
855
|
+
};
|
|
719
856
|
}
|
|
720
|
-
/**
|
|
857
|
+
/** 세 버퍼를 비운다. */
|
|
721
858
|
clear() {
|
|
722
859
|
this.network.clear();
|
|
723
860
|
this.logs.clear();
|
|
861
|
+
this.routes.clear();
|
|
724
862
|
}
|
|
725
863
|
// ── 내부: 기록 ────────────────────────────────────────────
|
|
726
864
|
/** 실패 사유 한 줄. 예외 객체엔 요청 본문이 들어 있을 수 있어 메시지만, 길이도 자른다. */
|
|
@@ -1125,9 +1263,9 @@ function shouldShowWidget(resolved, input = {}) {
|
|
|
1125
1263
|
return false;
|
|
1126
1264
|
}
|
|
1127
1265
|
}
|
|
1128
|
-
function diagnosticsProviderFor(resolved, collector) {
|
|
1266
|
+
function diagnosticsProviderFor(resolved, collector, installOpts) {
|
|
1129
1267
|
if (!resolved.captureDiagnostics) return void 0;
|
|
1130
|
-
collector.install();
|
|
1268
|
+
collector.install(installOpts);
|
|
1131
1269
|
return () => collector.snapshot();
|
|
1132
1270
|
}
|
|
1133
1271
|
|
|
@@ -1364,7 +1502,7 @@ var ReportModalController = class {
|
|
|
1364
1502
|
/** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
|
|
1365
1503
|
this.pinAnchor = "picture";
|
|
1366
1504
|
/**
|
|
1367
|
-
* 사용자가 [
|
|
1505
|
+
* 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가(방향 무관). 한 번이라도 건드리면 이후
|
|
1368
1506
|
* 타이핑에 의한 자동 접힘/펼침이 멈춘다 — "사람이 편 상태는 모달이 닫힐 때까지 유지"를
|
|
1369
1507
|
* 이렇게 구현한다. `open()` 에서 매번 리셋된다.
|
|
1370
1508
|
*/
|
|
@@ -1454,13 +1592,13 @@ var ReportModalController = class {
|
|
|
1454
1592
|
this.submitAttempted = false;
|
|
1455
1593
|
this.pinAnchor = opts.pin && !pendingReport ? "viewport" : "picture";
|
|
1456
1594
|
this.userToggledHints = false;
|
|
1457
|
-
const resolvedComment = pendingReport?.comment ??
|
|
1595
|
+
const resolvedComment = pendingReport?.comment ?? this.state.comment;
|
|
1458
1596
|
this.patch({
|
|
1459
1597
|
open: true,
|
|
1460
1598
|
comment: resolvedComment,
|
|
1461
1599
|
// 요소를 지목해서 연 경우엔 그 요소를 물고 간다. 대기 항목 복원이 우선이다.
|
|
1462
1600
|
element: pendingReport?.element ?? opts.element ?? null,
|
|
1463
|
-
priority: pendingReport?.priority ??
|
|
1601
|
+
priority: pendingReport?.priority ?? this.state.priority,
|
|
1464
1602
|
screenshot: pendingReport?.screenshot ?? null,
|
|
1465
1603
|
screenshotStatus: "none",
|
|
1466
1604
|
screenshotMessage: null,
|
|
@@ -1497,16 +1635,32 @@ var ReportModalController = class {
|
|
|
1497
1635
|
cancelClose() {
|
|
1498
1636
|
this.patch({ closeConfirmVisible: false });
|
|
1499
1637
|
}
|
|
1500
|
-
/**
|
|
1638
|
+
/**
|
|
1639
|
+
* 확인 없이 닫는다(요소 지목 모드로 넘어갈 때처럼 흐름이 이어지는 경우).
|
|
1640
|
+
*
|
|
1641
|
+
* **쓰던 글은 남긴다.** 지목으로 넘어가는 건 이 제보를 버린 게 아니라 "가리킬 요소를
|
|
1642
|
+
* 고르러 나간" 것이다. 예전엔 여기서 코멘트를 통째로 비웠고, 그래서 모달에 몇 줄 써 놓고
|
|
1643
|
+
* [요소 지목 모드]를 켜면 **확인도 없이 글이 사라졌다** — 사용자가 직접 닫을 때
|
|
1644
|
+
* (`requestClose`)는 확인을 받는데 이 경로만 안 받았다.
|
|
1645
|
+
*
|
|
1646
|
+
* 그림과 좌표는 남기지 않는다. 지목하러 간다는 건 **어느 화면의 무엇을 가리킬지 다시
|
|
1647
|
+
* 정한다**는 뜻이라, 그때의 캡처와 핀은 다음 화면에서 새로 잡는 게 맞다.
|
|
1648
|
+
*/
|
|
1501
1649
|
dismiss() {
|
|
1502
|
-
this.close();
|
|
1650
|
+
this.close({ keepText: true });
|
|
1503
1651
|
}
|
|
1652
|
+
/**
|
|
1653
|
+
* @param opts.keepResult 전송 결과까지 통째로 보존한다(대기 중 제보 복원용).
|
|
1654
|
+
* @param opts.keepText 사람이 입력한 것(코멘트·우선순위)만 보존한다. 그림·좌표·전송
|
|
1655
|
+
* 상태는 평소처럼 비운다.
|
|
1656
|
+
*/
|
|
1504
1657
|
close(opts) {
|
|
1505
1658
|
this.captureGeneration += 1;
|
|
1506
1659
|
const keepDraft = opts?.keepResult || this.state.submitStatus === "pending";
|
|
1660
|
+
const keepText = keepDraft || opts?.keepText === true;
|
|
1507
1661
|
const cleared = keepDraft ? {} : {
|
|
1508
|
-
comment: "",
|
|
1509
|
-
priority: "unset",
|
|
1662
|
+
comment: keepText ? this.state.comment : "",
|
|
1663
|
+
priority: keepText ? this.state.priority : "unset",
|
|
1510
1664
|
screenshot: null,
|
|
1511
1665
|
screenshotStatus: "none",
|
|
1512
1666
|
screenshotMessage: null,
|
|
@@ -1648,7 +1802,7 @@ ${hint.draft}`;
|
|
|
1648
1802
|
...this.hintsPatchFor(comment)
|
|
1649
1803
|
});
|
|
1650
1804
|
}
|
|
1651
|
-
/** [
|
|
1805
|
+
/** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다(`userToggledHints`). */
|
|
1652
1806
|
toggleHints() {
|
|
1653
1807
|
this.userToggledHints = true;
|
|
1654
1808
|
this.patch({ hintsExpanded: !this.state.hintsExpanded });
|
|
@@ -2261,6 +2415,9 @@ function buildLassoContext(report) {
|
|
|
2261
2415
|
device: { ...c.native.device },
|
|
2262
2416
|
display: { ...c.native.display }
|
|
2263
2417
|
} : null,
|
|
2418
|
+
// 미수집(진단 자체가 null)과 "이동이 없었음"을 굳이 나누지 않는다 — 둘 다 빈 배열.
|
|
2419
|
+
// 나누려면 서버가 null 을 보존해야 하는데, 그건 확인된 계약이 아니다.
|
|
2420
|
+
routes: [...c.diagnostics?.routes ?? []],
|
|
2264
2421
|
extra: c.extra
|
|
2265
2422
|
};
|
|
2266
2423
|
}
|
|
@@ -2924,8 +3081,11 @@ export {
|
|
|
2924
3081
|
PinController,
|
|
2925
3082
|
RATE_LIMIT_MIN_DELAY_MS,
|
|
2926
3083
|
REDACTED,
|
|
3084
|
+
ROUTE_TRAIL_KEY,
|
|
3085
|
+
ROUTE_TRAIL_LIMIT,
|
|
2927
3086
|
ReportModalController,
|
|
2928
3087
|
RingBuffer,
|
|
3088
|
+
RouteTrail,
|
|
2929
3089
|
SCORE_ELEMENT,
|
|
2930
3090
|
SCORE_PATH_EXACT,
|
|
2931
3091
|
SCORE_PATH_PATTERN,
|
|
@@ -2961,6 +3121,7 @@ export {
|
|
|
2961
3121
|
matchElement,
|
|
2962
3122
|
matchPath,
|
|
2963
3123
|
normalizePin,
|
|
3124
|
+
normalizeRoutePath,
|
|
2964
3125
|
normalizeUser,
|
|
2965
3126
|
parseHintCatalog,
|
|
2966
3127
|
parseSourceAttr,
|