@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 +73 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +62 -7
- package/dist/index.d.ts +62 -7
- package/dist/index.js +73 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapters/adapters.test.ts +0 -730
- package/src/adapters/body.ts +0 -280
- package/src/adapters/index.ts +0 -19
- package/src/adapters/lasso.live.test.ts +0 -156
- package/src/adapters/lasso.ts +0 -316
- package/src/adapters/linear.ts +0 -39
- package/src/adapters/notion.ts +0 -30
- package/src/config.test.ts +0 -284
- package/src/config.ts +0 -396
- package/src/context.test.ts +0 -427
- package/src/context.ts +0 -326
- package/src/diagnostics.test.ts +0 -382
- package/src/diagnostics.ts +0 -502
- package/src/guest-id.ts +0 -36
- package/src/index.ts +0 -179
- package/src/keepalive.test.ts +0 -72
- package/src/keepalive.ts +0 -37
- package/src/queue.test.ts +0 -848
- package/src/queue.ts +0 -546
- package/src/report.ts +0 -58
- package/src/ring-buffer.test.ts +0 -62
- package/src/ring-buffer.ts +0 -42
- package/src/source-attr.test.ts +0 -77
- package/src/source-attr.ts +0 -86
- package/src/types.ts +0 -320
- package/src/uuid.test.ts +0 -116
- package/src/uuid.ts +0 -54
- package/src/widget/controller.ts +0 -224
- package/src/widget/focus.ts +0 -66
- package/src/widget/index.ts +0 -59
- package/src/widget/modal.test.ts +0 -472
- package/src/widget/modal.ts +0 -526
- package/src/widget/pin.ts +0 -110
- package/src/widget/screenshot.ts +0 -109
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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 };
|