@solhun/feedback-kit-core 0.1.0 → 0.2.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
@@ -724,6 +724,10 @@ var RingBuffer = class {
724
724
  var NETWORK_BUFFER_LIMIT = 30;
725
725
  var LOG_BUFFER_LIMIT = 50;
726
726
  var MAX_LOG_MESSAGE_CHARS = 2e3;
727
+ function failureReason(error) {
728
+ const message = error instanceof Error ? error.message : String(error);
729
+ return message.slice(0, 200) || "\uB124\uD2B8\uC6CC\uD06C \uC2E4\uD328";
730
+ }
727
731
  var DiagnosticsCollector = class {
728
732
  constructor(opts = {}) {
729
733
  this.installed = false;
@@ -733,7 +737,9 @@ var DiagnosticsCollector = class {
733
737
  this.originalXhrSend = null;
734
738
  this.originalWarn = null;
735
739
  this.originalError = null;
740
+ this.originalLog = null;
736
741
  this.excludeMatcher = null;
742
+ this.logScope = "problems";
737
743
  /**
738
744
  * 패치된 fetch 안에서 동기적으로 XHR 이 열리는 깊이.
739
745
  *
@@ -766,6 +772,7 @@ var DiagnosticsCollector = class {
766
772
  if (this.installed) return;
767
773
  this.installed = true;
768
774
  this.excludeMatcher = opts.excludeMatcher ?? null;
775
+ this.logScope = opts.logs ?? "problems";
769
776
  if (opts.now) this.now = opts.now;
770
777
  if (opts.iso) this.iso = opts.iso;
771
778
  this.patchFetch();
@@ -791,8 +798,10 @@ var DiagnosticsCollector = class {
791
798
  const c = g.console;
792
799
  if (c && this.originalWarn) c.warn = this.originalWarn;
793
800
  if (c && this.originalError) c.error = this.originalError;
801
+ if (c && this.originalLog) c.log = this.originalLog;
794
802
  this.originalWarn = null;
795
803
  this.originalError = null;
804
+ this.originalLog = null;
796
805
  this.excludeMatcher = null;
797
806
  this.fetchDepth = 0;
798
807
  }
@@ -809,11 +818,15 @@ var DiagnosticsCollector = class {
809
818
  this.logs.clear();
810
819
  }
811
820
  // ── 내부: 기록 ────────────────────────────────────────────
812
- recordNetwork(method, url, status, startedAt) {
821
+ /** 실패 사유 줄. 예외 객체엔 요청 본문이 들어 있을 수 있어 메시지만, 길이도 자른다. */
822
+ recordNetwork(method, url, status, startedAt, errorMessage = null) {
813
823
  this.network.push({
814
824
  method,
815
825
  url,
816
826
  status,
827
+ // 제보를 훑는 사람이 실패한 요청만 눈으로 골라낼 수 있게 따로 싣는다.
828
+ ok: status !== null && status >= 200 && status < 300,
829
+ errorMessage,
817
830
  durationMs: Math.max(0, Math.round(this.now() - startedAt)),
818
831
  at: this.iso(startedAt)
819
832
  });
@@ -850,7 +863,7 @@ var DiagnosticsCollector = class {
850
863
  pending = Promise.resolve(originalFetch(input, init));
851
864
  } catch (err) {
852
865
  self.fetchDepth -= 1;
853
- if (!excluded) self.recordNetwork(method, url, null, start);
866
+ if (!excluded) self.recordNetwork(method, url, null, start, failureReason(err));
854
867
  throw err;
855
868
  }
856
869
  self.fetchDepth -= 1;
@@ -868,7 +881,7 @@ var DiagnosticsCollector = class {
868
881
  return raw;
869
882
  },
870
883
  (err) => {
871
- if (!excluded) self.recordNetwork(method, url, null, start);
884
+ if (!excluded) self.recordNetwork(method, url, null, start, failureReason(err));
872
885
  throw err;
873
886
  }
874
887
  );
@@ -916,6 +929,7 @@ var DiagnosticsCollector = class {
916
929
  };
917
930
  }
918
931
  patchConsole() {
932
+ if (this.logScope === "off") return;
919
933
  const c = globalThis.console;
920
934
  if (!c) return;
921
935
  const self = this;
@@ -935,6 +949,14 @@ var DiagnosticsCollector = class {
935
949
  orig(...args);
936
950
  };
937
951
  }
952
+ if (this.logScope === "all" && typeof c.log === "function") {
953
+ const orig = c.log.bind(c);
954
+ this.originalLog = orig;
955
+ c.log = function patchedLog(...args) {
956
+ self.recordLog("log", args);
957
+ orig(...args);
958
+ };
959
+ }
938
960
  }
939
961
  };
940
962
  var sharedDiagnostics = new DiagnosticsCollector();
@@ -1153,6 +1175,7 @@ function resolveConfig(config = {}) {
1153
1175
  visibility: normalizeVisibility(config.visibility, warn),
1154
1176
  captureScreenshot: config.captureScreenshot ?? true,
1155
1177
  captureDiagnostics: config.captureDiagnostics ?? true,
1178
+ captureLogs: config.captureLogs ?? "problems",
1156
1179
  position: normalizePosition(config.position, warn),
1157
1180
  isInternal: typeof config.isInternal === "function" ? config.isInternal : null,
1158
1181
  internalRoles: config.internalRoles ?? DEFAULT_INTERNAL_ROLES,
@@ -1434,6 +1457,7 @@ var ReportModalController = class {
1434
1457
  this.captureGeneration = 0;
1435
1458
  this.state = {
1436
1459
  open: false,
1460
+ element: null,
1437
1461
  comment: "",
1438
1462
  priority: "unset",
1439
1463
  screenshot: null,
@@ -1499,7 +1523,7 @@ var ReportModalController = class {
1499
1523
  this.queue.stop?.();
1500
1524
  }
1501
1525
  // ── 열기/닫기 ─────────────────────────────────────────────────────────────
1502
- async open() {
1526
+ async open(opts = {}) {
1503
1527
  if (this.state.open) return;
1504
1528
  const wasPending = this.state.submitStatus === "pending" && this.lastReport !== null;
1505
1529
  const pendingReport = wasPending ? this.lastReport : null;
@@ -1508,6 +1532,8 @@ var ReportModalController = class {
1508
1532
  this.patch({
1509
1533
  open: true,
1510
1534
  comment: pendingReport?.comment ?? "",
1535
+ // 요소를 지목해서 연 경우엔 그 요소를 물고 간다. 대기 항목 복원이 우선이다.
1536
+ element: pendingReport?.element ?? opts.element ?? null,
1511
1537
  priority: pendingReport?.priority ?? "unset",
1512
1538
  screenshot: pendingReport?.screenshot ?? null,
1513
1539
  screenshotStatus: "none",
@@ -1667,12 +1693,13 @@ var ReportModalController = class {
1667
1693
  }
1668
1694
  this.patch({ submitStatus: "sending", submitMessage: null });
1669
1695
  const parts = {
1670
- kind: "report",
1696
+ // 요소를 지목했으면 지목 제보다. 수집 쪽이 이 값으로 갈린다.
1697
+ kind: this.state.element ? "annotation" : "report",
1671
1698
  comment: this.state.comment,
1672
1699
  priority: this.state.priority,
1673
1700
  screenshot: this.state.screenshot,
1674
1701
  pin: this.state.pin,
1675
- element: null
1702
+ element: this.state.element
1676
1703
  };
1677
1704
  let outcome;
1678
1705
  try {
@@ -1824,9 +1851,14 @@ var WidgetController = class {
1824
1851
  this.listeners.clear();
1825
1852
  }
1826
1853
  // ── 플로팅 버튼 ↔ 모달 ────────────────────────────────────────────────────
1827
- async openReport() {
1854
+ /**
1855
+ * @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
1856
+ * `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
1857
+ * `picking` 에 머물러 **모달이 렌더되지 않는다**.
1858
+ */
1859
+ async openReport(opts = {}) {
1828
1860
  this.screen = "modal";
1829
- await this.modal.open();
1861
+ await this.modal.open(opts);
1830
1862
  this.emit();
1831
1863
  }
1832
1864
  /** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
@@ -2150,6 +2182,19 @@ function parseRetryAfterMs(res) {
2150
2182
  const seconds = Number(raw.trim());
2151
2183
  return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds * 1e3) : null;
2152
2184
  }
2185
+ function reasonFrom(text) {
2186
+ try {
2187
+ const body = JSON.parse(text);
2188
+ const code = typeof body.code === "string" ? body.code : null;
2189
+ const message = typeof body.message === "string" ? body.message : null;
2190
+ const joined = [code, message].filter(Boolean).join(": ");
2191
+ if (joined) return ` \u2014 ${joined.slice(0, 200)}`;
2192
+ } catch {
2193
+ const head = text.trim().slice(0, 120);
2194
+ if (head) return ` \u2014 ${head}`;
2195
+ }
2196
+ return "";
2197
+ }
2153
2198
  function fail(retryable, extra) {
2154
2199
  return { ok: false, id: null, retryable, retryAfterMs: null, ...extra };
2155
2200
  }
@@ -2204,6 +2249,12 @@ function buildLassoEnvelope(report) {
2204
2249
  comment: renderReportBody(report),
2205
2250
  pin: report.pin ? { x: report.pin.x, y: report.pin.y } : null,
2206
2251
  sourceFile: el?.attributes["data-feedback-source"] ?? null,
2252
+ web: clientType === "web" ? {
2253
+ url: c.url,
2254
+ route: c.screen,
2255
+ viewport: c.web?.viewport ?? null,
2256
+ userAgent: c.web?.userAgent ?? null
2257
+ } : null,
2207
2258
  screenshotBase64: report.screenshot?.base64 ?? null,
2208
2259
  screenshotContentType: report.screenshot?.contentType ?? null,
2209
2260
  // 서버가 없으면 빈 배열로 취급하지만, 미연동과 "없었음"을 구분해 보낸다.
@@ -2259,29 +2310,38 @@ function createLassoAdapter(opts = {}) {
2259
2310
  return fail(true);
2260
2311
  }
2261
2312
  const status = res.status;
2313
+ let text = "";
2314
+ try {
2315
+ text = await res.text();
2316
+ } catch {
2317
+ }
2262
2318
  if (status < 200 || status >= 300) {
2319
+ const reason = reasonFrom(text);
2263
2320
  if (status === 429) {
2264
- emit("feedback-kit: \uC218\uC9D1 \uD55C\uB3C4\uC5D0 \uAC78\uB838\uB2E4(429). \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.");
2321
+ emit(`feedback-kit: \uC218\uC9D1 \uD55C\uB3C4\uC5D0 \uAC78\uB838\uB2E4(429). \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.${reason}`);
2265
2322
  return fail(true, { retryAfterMs: parseRetryAfterMs(res), rateLimited: true });
2266
2323
  }
2267
2324
  const retryable = RETRYABLE_STATUS.has(status);
2268
2325
  emit(
2269
- `feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status}${retryable ? " (\uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4)" : " (\uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4)"}`
2326
+ `feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status}${retryable ? " (\uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4)" : " (\uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4)"}${reason}`
2270
2327
  );
2271
2328
  return fail(retryable, { retryAfterMs: retryable ? parseRetryAfterMs(res) : null });
2272
2329
  }
2273
2330
  let parsed;
2274
2331
  try {
2275
- const text = await res.text();
2276
2332
  const value = JSON.parse(text);
2277
2333
  if (value === null || typeof value !== "object") throw new Error("\uAC1D\uCCB4\uAC00 \uC544\uB2D8");
2278
2334
  parsed = value;
2279
2335
  } catch {
2280
- emit(`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status} \uC758 \uBCF8\uBB38\uC744 \uC77D\uC9C0 \uBABB\uD588\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.`);
2336
+ emit(
2337
+ `feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status} \uC758 \uBCF8\uBB38\uC774 JSON \uC774 \uC544\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.${reasonFrom(text)}`
2338
+ );
2281
2339
  return fail(true);
2282
2340
  }
2283
2341
  if (parsed.ok !== true) {
2284
- emit(`feedback-kit: \uC218\uC9D1 \uC11C\uBC84\uAC00 \uC81C\uBCF4\uB97C \uAC70\uC808\uD588\uB2E4(${status}). \uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4.`);
2342
+ emit(
2343
+ `feedback-kit: \uC218\uC9D1 \uC11C\uBC84\uAC00 \uC81C\uBCF4\uB97C \uAC70\uC808\uD588\uB2E4(${status}). \uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4.${reasonFrom(text)}`
2344
+ );
2285
2345
  return fail(false);
2286
2346
  }
2287
2347
  return { ok: true, id: idFrom(parsed), retryable: false, retryAfterMs: null };