@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.cjs
CHANGED
|
@@ -63,8 +63,11 @@ __export(index_exports, {
|
|
|
63
63
|
PinController: () => PinController,
|
|
64
64
|
RATE_LIMIT_MIN_DELAY_MS: () => RATE_LIMIT_MIN_DELAY_MS,
|
|
65
65
|
REDACTED: () => REDACTED,
|
|
66
|
+
ROUTE_TRAIL_KEY: () => ROUTE_TRAIL_KEY,
|
|
67
|
+
ROUTE_TRAIL_LIMIT: () => ROUTE_TRAIL_LIMIT,
|
|
66
68
|
ReportModalController: () => ReportModalController,
|
|
67
69
|
RingBuffer: () => RingBuffer,
|
|
70
|
+
RouteTrail: () => RouteTrail,
|
|
68
71
|
SCORE_ELEMENT: () => SCORE_ELEMENT,
|
|
69
72
|
SCORE_PATH_EXACT: () => SCORE_PATH_EXACT,
|
|
70
73
|
SCORE_PATH_PATTERN: () => SCORE_PATH_PATTERN,
|
|
@@ -100,6 +103,7 @@ __export(index_exports, {
|
|
|
100
103
|
matchElement: () => matchElement,
|
|
101
104
|
matchPath: () => matchPath,
|
|
102
105
|
normalizePin: () => normalizePin,
|
|
106
|
+
normalizeRoutePath: () => normalizeRoutePath,
|
|
103
107
|
normalizeUser: () => normalizeUser,
|
|
104
108
|
parseHintCatalog: () => parseHintCatalog,
|
|
105
109
|
parseSourceAttr: () => parseSourceAttr,
|
|
@@ -269,6 +273,7 @@ function normalizeDiagnostics(snap) {
|
|
|
269
273
|
if (!snap || !Array.isArray(snap.network) || !Array.isArray(snap.logs)) {
|
|
270
274
|
return null;
|
|
271
275
|
}
|
|
276
|
+
if (!Array.isArray(snap.routes)) return { ...snap, routes: [] };
|
|
272
277
|
return snap;
|
|
273
278
|
}
|
|
274
279
|
function safeCall(fn, fallback) {
|
|
@@ -743,6 +748,130 @@ var RingBuffer = class {
|
|
|
743
748
|
}
|
|
744
749
|
};
|
|
745
750
|
|
|
751
|
+
// src/route-trail.ts
|
|
752
|
+
var ROUTE_TRAIL_LIMIT = 20;
|
|
753
|
+
var ROUTE_TRAIL_KEY = "feedback-kit:route-trail";
|
|
754
|
+
function normalizeRoutePath(route) {
|
|
755
|
+
const raw = typeof route === "string" ? route : "";
|
|
756
|
+
const cut = [raw.indexOf("?"), raw.indexOf("#")].filter((i) => i >= 0);
|
|
757
|
+
const trimmed = (cut.length > 0 ? raw.slice(0, Math.min(...cut)) : raw).trim();
|
|
758
|
+
return trimmed.length > 0 ? trimmed : "/";
|
|
759
|
+
}
|
|
760
|
+
function parseEntries(raw) {
|
|
761
|
+
if (!raw) return [];
|
|
762
|
+
let parsed;
|
|
763
|
+
try {
|
|
764
|
+
parsed = JSON.parse(raw);
|
|
765
|
+
} catch {
|
|
766
|
+
return [];
|
|
767
|
+
}
|
|
768
|
+
if (!Array.isArray(parsed)) return [];
|
|
769
|
+
const out = [];
|
|
770
|
+
for (const item of parsed) {
|
|
771
|
+
if (typeof item !== "object" || item === null) continue;
|
|
772
|
+
const e = item;
|
|
773
|
+
if (typeof e.to !== "string" || typeof e.at !== "number") continue;
|
|
774
|
+
out.push({
|
|
775
|
+
at: e.at,
|
|
776
|
+
from: typeof e.from === "string" ? e.from : null,
|
|
777
|
+
to: e.to,
|
|
778
|
+
count: typeof e.count === "number" && e.count > 0 ? Math.floor(e.count) : 1
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return out;
|
|
782
|
+
}
|
|
783
|
+
var RouteTrail = class {
|
|
784
|
+
constructor(opts = {}) {
|
|
785
|
+
this.entries = [];
|
|
786
|
+
this.unwatch = null;
|
|
787
|
+
this.limit = opts.limit !== void 0 && opts.limit > 0 ? Math.floor(opts.limit) : ROUTE_TRAIL_LIMIT;
|
|
788
|
+
this.key = opts.key ?? ROUTE_TRAIL_KEY;
|
|
789
|
+
this.now = opts.now ?? (() => Date.now());
|
|
790
|
+
this.storage = opts.storage ?? null;
|
|
791
|
+
this.restore();
|
|
792
|
+
}
|
|
793
|
+
/** 감시자를 붙인다. 이미 붙어 있으면 먼저 끊는다(감시자가 둘이면 이동이 두 번 기록된다). */
|
|
794
|
+
watch(watcher) {
|
|
795
|
+
this.unwatchRoutes();
|
|
796
|
+
try {
|
|
797
|
+
this.unwatch = watcher((route) => this.record(route));
|
|
798
|
+
} catch {
|
|
799
|
+
this.unwatch = null;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
/** 감시자를 뗀다. 버퍼 내용은 그대로 남는다. */
|
|
803
|
+
unwatchRoutes() {
|
|
804
|
+
if (!this.unwatch) return;
|
|
805
|
+
const off = this.unwatch;
|
|
806
|
+
this.unwatch = null;
|
|
807
|
+
try {
|
|
808
|
+
off();
|
|
809
|
+
} catch {
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* 이동 하나를 기록한다.
|
|
814
|
+
*
|
|
815
|
+
* 같은 경로로 연속 이동하면 새 줄을 만들지 않고 `count` 만 올린다. 판정은 **직전 항목의
|
|
816
|
+
* `to`** 와 비교한다 — 새로고침 직후 같은 화면에서 다시 시작해도 줄이 늘지 않는다.
|
|
817
|
+
*/
|
|
818
|
+
record(route) {
|
|
819
|
+
const to = normalizeRoutePath(route);
|
|
820
|
+
const last = this.entries[this.entries.length - 1];
|
|
821
|
+
if (last && last.to === to) {
|
|
822
|
+
last.count += 1;
|
|
823
|
+
} else {
|
|
824
|
+
this.entries.push({ at: this.now(), from: last ? last.to : null, to, count: 1 });
|
|
825
|
+
const overflow = this.entries.length - this.limit;
|
|
826
|
+
if (overflow > 0) this.entries.splice(0, overflow);
|
|
827
|
+
}
|
|
828
|
+
this.persist();
|
|
829
|
+
}
|
|
830
|
+
/** 지금까지의 궤적. 사본이라 이후 이동이 이미 만든 제보를 바꾸지 않는다. */
|
|
831
|
+
snapshot() {
|
|
832
|
+
return this.entries.map((e) => ({ ...e }));
|
|
833
|
+
}
|
|
834
|
+
get size() {
|
|
835
|
+
return this.entries.length;
|
|
836
|
+
}
|
|
837
|
+
/** 버퍼와 저장분을 모두 비운다. */
|
|
838
|
+
clear() {
|
|
839
|
+
this.entries = [];
|
|
840
|
+
this.persist();
|
|
841
|
+
}
|
|
842
|
+
/** 저장소를 나중에 붙인다. 이미 저장돼 있던 궤적이 있으면 그것을 이어받는다. */
|
|
843
|
+
attachStorage(storage) {
|
|
844
|
+
this.storage = storage;
|
|
845
|
+
if (!storage) return;
|
|
846
|
+
const restored = parseEntries(this.readRaw());
|
|
847
|
+
if (restored.length > 0) {
|
|
848
|
+
this.entries = [...restored, ...this.entries].slice(-this.limit);
|
|
849
|
+
}
|
|
850
|
+
this.persist();
|
|
851
|
+
}
|
|
852
|
+
// ── 내부: 저장소 ──────────────────────────────────────────
|
|
853
|
+
// 저장은 **거들 뿐**이다. 던지면 이동 한 번에 앱이 죽는다(프라이빗 모드 등에서
|
|
854
|
+
// getItem/setItem 이 실제로 던진다). 그래서 전부 삼키고 메모리로 계속 간다.
|
|
855
|
+
readRaw() {
|
|
856
|
+
if (!this.storage) return null;
|
|
857
|
+
try {
|
|
858
|
+
return this.storage.getItem(this.key);
|
|
859
|
+
} catch {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
restore() {
|
|
864
|
+
this.entries = parseEntries(this.readRaw()).slice(-this.limit);
|
|
865
|
+
}
|
|
866
|
+
persist() {
|
|
867
|
+
if (!this.storage) return;
|
|
868
|
+
try {
|
|
869
|
+
this.storage.setItem(this.key, JSON.stringify(this.entries));
|
|
870
|
+
} catch {
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
|
|
746
875
|
// src/diagnostics.ts
|
|
747
876
|
var NETWORK_BUFFER_LIMIT = 30;
|
|
748
877
|
var LOG_BUFFER_LIMIT = 50;
|
|
@@ -789,6 +918,11 @@ var DiagnosticsCollector = class {
|
|
|
789
918
|
opts.networkLimit ?? NETWORK_BUFFER_LIMIT
|
|
790
919
|
);
|
|
791
920
|
this.logs = new RingBuffer(opts.logLimit ?? LOG_BUFFER_LIMIT);
|
|
921
|
+
this.routes = new RouteTrail({
|
|
922
|
+
limit: opts.routeLimit ?? ROUTE_TRAIL_LIMIT,
|
|
923
|
+
storage: opts.routeStorage ?? null,
|
|
924
|
+
now: () => this.now()
|
|
925
|
+
});
|
|
792
926
|
}
|
|
793
927
|
/** 글로벌 fetch/XHR/console 을 패치한다. 이미 설치했으면 no-op. */
|
|
794
928
|
install(opts = {}) {
|
|
@@ -801,6 +935,8 @@ var DiagnosticsCollector = class {
|
|
|
801
935
|
this.patchFetch();
|
|
802
936
|
this.patchXhr();
|
|
803
937
|
this.patchConsole();
|
|
938
|
+
if (opts.routeStorage !== void 0) this.routes.attachStorage(opts.routeStorage);
|
|
939
|
+
if (opts.watchRoutes) this.routes.watch(opts.watchRoutes);
|
|
804
940
|
}
|
|
805
941
|
/** 글로벌 패치를 모두 원복한다. 버퍼 내용은 그대로 남는다. */
|
|
806
942
|
uninstall() {
|
|
@@ -827,18 +963,24 @@ var DiagnosticsCollector = class {
|
|
|
827
963
|
this.originalLog = null;
|
|
828
964
|
this.excludeMatcher = null;
|
|
829
965
|
this.fetchDepth = 0;
|
|
966
|
+
this.routes.unwatchRoutes();
|
|
830
967
|
}
|
|
831
968
|
/**
|
|
832
969
|
* 지금까지 모인 것을 그대로 스냅샷으로 낸다.
|
|
833
970
|
* 반환 배열은 사본이라 이후 수집이 제보 페이로드를 바꾸지 않는다.
|
|
834
971
|
*/
|
|
835
972
|
snapshot() {
|
|
836
|
-
return {
|
|
973
|
+
return {
|
|
974
|
+
network: this.network.toArray(),
|
|
975
|
+
logs: this.logs.toArray(),
|
|
976
|
+
routes: this.routes.snapshot()
|
|
977
|
+
};
|
|
837
978
|
}
|
|
838
|
-
/**
|
|
979
|
+
/** 세 버퍼를 비운다. */
|
|
839
980
|
clear() {
|
|
840
981
|
this.network.clear();
|
|
841
982
|
this.logs.clear();
|
|
983
|
+
this.routes.clear();
|
|
842
984
|
}
|
|
843
985
|
// ── 내부: 기록 ────────────────────────────────────────────
|
|
844
986
|
/** 실패 사유 한 줄. 예외 객체엔 요청 본문이 들어 있을 수 있어 메시지만, 길이도 자른다. */
|
|
@@ -1243,9 +1385,9 @@ function shouldShowWidget(resolved, input = {}) {
|
|
|
1243
1385
|
return false;
|
|
1244
1386
|
}
|
|
1245
1387
|
}
|
|
1246
|
-
function diagnosticsProviderFor(resolved, collector) {
|
|
1388
|
+
function diagnosticsProviderFor(resolved, collector, installOpts) {
|
|
1247
1389
|
if (!resolved.captureDiagnostics) return void 0;
|
|
1248
|
-
collector.install();
|
|
1390
|
+
collector.install(installOpts);
|
|
1249
1391
|
return () => collector.snapshot();
|
|
1250
1392
|
}
|
|
1251
1393
|
|
|
@@ -1482,7 +1624,7 @@ var ReportModalController = class {
|
|
|
1482
1624
|
/** 지금 들고 있는 핀의 기준. 핀이 없으면 의미 없다(기본 `picture`). */
|
|
1483
1625
|
this.pinAnchor = "picture";
|
|
1484
1626
|
/**
|
|
1485
|
-
* 사용자가 [
|
|
1627
|
+
* 사용자가 [이런 건가요? ▾] 를 직접 건드렸는가(방향 무관). 한 번이라도 건드리면 이후
|
|
1486
1628
|
* 타이핑에 의한 자동 접힘/펼침이 멈춘다 — "사람이 편 상태는 모달이 닫힐 때까지 유지"를
|
|
1487
1629
|
* 이렇게 구현한다. `open()` 에서 매번 리셋된다.
|
|
1488
1630
|
*/
|
|
@@ -1572,13 +1714,13 @@ var ReportModalController = class {
|
|
|
1572
1714
|
this.submitAttempted = false;
|
|
1573
1715
|
this.pinAnchor = opts.pin && !pendingReport ? "viewport" : "picture";
|
|
1574
1716
|
this.userToggledHints = false;
|
|
1575
|
-
const resolvedComment = pendingReport?.comment ??
|
|
1717
|
+
const resolvedComment = pendingReport?.comment ?? this.state.comment;
|
|
1576
1718
|
this.patch({
|
|
1577
1719
|
open: true,
|
|
1578
1720
|
comment: resolvedComment,
|
|
1579
1721
|
// 요소를 지목해서 연 경우엔 그 요소를 물고 간다. 대기 항목 복원이 우선이다.
|
|
1580
1722
|
element: pendingReport?.element ?? opts.element ?? null,
|
|
1581
|
-
priority: pendingReport?.priority ??
|
|
1723
|
+
priority: pendingReport?.priority ?? this.state.priority,
|
|
1582
1724
|
screenshot: pendingReport?.screenshot ?? null,
|
|
1583
1725
|
screenshotStatus: "none",
|
|
1584
1726
|
screenshotMessage: null,
|
|
@@ -1615,16 +1757,32 @@ var ReportModalController = class {
|
|
|
1615
1757
|
cancelClose() {
|
|
1616
1758
|
this.patch({ closeConfirmVisible: false });
|
|
1617
1759
|
}
|
|
1618
|
-
/**
|
|
1760
|
+
/**
|
|
1761
|
+
* 확인 없이 닫는다(요소 지목 모드로 넘어갈 때처럼 흐름이 이어지는 경우).
|
|
1762
|
+
*
|
|
1763
|
+
* **쓰던 글은 남긴다.** 지목으로 넘어가는 건 이 제보를 버린 게 아니라 "가리킬 요소를
|
|
1764
|
+
* 고르러 나간" 것이다. 예전엔 여기서 코멘트를 통째로 비웠고, 그래서 모달에 몇 줄 써 놓고
|
|
1765
|
+
* [요소 지목 모드]를 켜면 **확인도 없이 글이 사라졌다** — 사용자가 직접 닫을 때
|
|
1766
|
+
* (`requestClose`)는 확인을 받는데 이 경로만 안 받았다.
|
|
1767
|
+
*
|
|
1768
|
+
* 그림과 좌표는 남기지 않는다. 지목하러 간다는 건 **어느 화면의 무엇을 가리킬지 다시
|
|
1769
|
+
* 정한다**는 뜻이라, 그때의 캡처와 핀은 다음 화면에서 새로 잡는 게 맞다.
|
|
1770
|
+
*/
|
|
1619
1771
|
dismiss() {
|
|
1620
|
-
this.close();
|
|
1772
|
+
this.close({ keepText: true });
|
|
1621
1773
|
}
|
|
1774
|
+
/**
|
|
1775
|
+
* @param opts.keepResult 전송 결과까지 통째로 보존한다(대기 중 제보 복원용).
|
|
1776
|
+
* @param opts.keepText 사람이 입력한 것(코멘트·우선순위)만 보존한다. 그림·좌표·전송
|
|
1777
|
+
* 상태는 평소처럼 비운다.
|
|
1778
|
+
*/
|
|
1622
1779
|
close(opts) {
|
|
1623
1780
|
this.captureGeneration += 1;
|
|
1624
1781
|
const keepDraft = opts?.keepResult || this.state.submitStatus === "pending";
|
|
1782
|
+
const keepText = keepDraft || opts?.keepText === true;
|
|
1625
1783
|
const cleared = keepDraft ? {} : {
|
|
1626
|
-
comment: "",
|
|
1627
|
-
priority: "unset",
|
|
1784
|
+
comment: keepText ? this.state.comment : "",
|
|
1785
|
+
priority: keepText ? this.state.priority : "unset",
|
|
1628
1786
|
screenshot: null,
|
|
1629
1787
|
screenshotStatus: "none",
|
|
1630
1788
|
screenshotMessage: null,
|
|
@@ -1766,7 +1924,7 @@ ${hint.draft}`;
|
|
|
1766
1924
|
...this.hintsPatchFor(comment)
|
|
1767
1925
|
});
|
|
1768
1926
|
}
|
|
1769
|
-
/** [
|
|
1927
|
+
/** [이런 건가요? ▾] 토글. 방향과 무관하게 이후 자동 접힘/펼침을 멈춘다(`userToggledHints`). */
|
|
1770
1928
|
toggleHints() {
|
|
1771
1929
|
this.userToggledHints = true;
|
|
1772
1930
|
this.patch({ hintsExpanded: !this.state.hintsExpanded });
|
|
@@ -2379,6 +2537,9 @@ function buildLassoContext(report) {
|
|
|
2379
2537
|
device: { ...c.native.device },
|
|
2380
2538
|
display: { ...c.native.display }
|
|
2381
2539
|
} : null,
|
|
2540
|
+
// 미수집(진단 자체가 null)과 "이동이 없었음"을 굳이 나누지 않는다 — 둘 다 빈 배열.
|
|
2541
|
+
// 나누려면 서버가 null 을 보존해야 하는데, 그건 확인된 계약이 아니다.
|
|
2542
|
+
routes: [...c.diagnostics?.routes ?? []],
|
|
2382
2543
|
extra: c.extra
|
|
2383
2544
|
};
|
|
2384
2545
|
}
|
|
@@ -3043,8 +3204,11 @@ function parseCacheEntry(value) {
|
|
|
3043
3204
|
PinController,
|
|
3044
3205
|
RATE_LIMIT_MIN_DELAY_MS,
|
|
3045
3206
|
REDACTED,
|
|
3207
|
+
ROUTE_TRAIL_KEY,
|
|
3208
|
+
ROUTE_TRAIL_LIMIT,
|
|
3046
3209
|
ReportModalController,
|
|
3047
3210
|
RingBuffer,
|
|
3211
|
+
RouteTrail,
|
|
3048
3212
|
SCORE_ELEMENT,
|
|
3049
3213
|
SCORE_PATH_EXACT,
|
|
3050
3214
|
SCORE_PATH_PATTERN,
|
|
@@ -3080,6 +3244,7 @@ function parseCacheEntry(value) {
|
|
|
3080
3244
|
matchElement,
|
|
3081
3245
|
matchPath,
|
|
3082
3246
|
normalizePin,
|
|
3247
|
+
normalizeRoutePath,
|
|
3083
3248
|
normalizeUser,
|
|
3084
3249
|
parseHintCatalog,
|
|
3085
3250
|
parseSourceAttr,
|