@solhun/feedback-kit-core 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 { network: this.network.toArray(), logs: this.logs.toArray() };
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
 
@@ -2395,6 +2537,9 @@ function buildLassoContext(report) {
2395
2537
  device: { ...c.native.device },
2396
2538
  display: { ...c.native.display }
2397
2539
  } : null,
2540
+ // 미수집(진단 자체가 null)과 "이동이 없었음"을 굳이 나누지 않는다 — 둘 다 빈 배열.
2541
+ // 나누려면 서버가 null 을 보존해야 하는데, 그건 확인된 계약이 아니다.
2542
+ routes: [...c.diagnostics?.routes ?? []],
2398
2543
  extra: c.extra
2399
2544
  };
2400
2545
  }
@@ -3059,8 +3204,11 @@ function parseCacheEntry(value) {
3059
3204
  PinController,
3060
3205
  RATE_LIMIT_MIN_DELAY_MS,
3061
3206
  REDACTED,
3207
+ ROUTE_TRAIL_KEY,
3208
+ ROUTE_TRAIL_LIMIT,
3062
3209
  ReportModalController,
3063
3210
  RingBuffer,
3211
+ RouteTrail,
3064
3212
  SCORE_ELEMENT,
3065
3213
  SCORE_PATH_EXACT,
3066
3214
  SCORE_PATH_PATTERN,
@@ -3096,6 +3244,7 @@ function parseCacheEntry(value) {
3096
3244
  matchElement,
3097
3245
  matchPath,
3098
3246
  normalizePin,
3247
+ normalizeRoutePath,
3099
3248
  normalizeUser,
3100
3249
  parseHintCatalog,
3101
3250
  parseSourceAttr,