@solhun/feedback-kit-core 0.1.0 → 0.2.1
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 +83 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +81 -7
- package/dist/index.d.ts +81 -7
- package/dist/index.js +83 -34
- 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
|
}
|
|
@@ -2171,27 +2216,6 @@ function buildLassoEnvelope(report) {
|
|
|
2171
2216
|
const parsed = Date.parse(report.createdAt);
|
|
2172
2217
|
const timestamp = Number.isFinite(parsed) ? parsed : 0;
|
|
2173
2218
|
const el = report.element;
|
|
2174
|
-
if (report.kind === "annotation") {
|
|
2175
|
-
return {
|
|
2176
|
-
clientSubmissionId: report.clientSubmissionId,
|
|
2177
|
-
clientType,
|
|
2178
|
-
event: "annotation.add",
|
|
2179
|
-
timestamp,
|
|
2180
|
-
url: c.url ?? "",
|
|
2181
|
-
priority: report.priority,
|
|
2182
|
-
annotation: {
|
|
2183
|
-
id: report.clientSubmissionId,
|
|
2184
|
-
comment: renderReportBody(report),
|
|
2185
|
-
x: report.pin?.x ?? null,
|
|
2186
|
-
y: report.pin?.y ?? null,
|
|
2187
|
-
element: el ? el.tag : null,
|
|
2188
|
-
elementPath: el?.attributes["data-feedback-source"] ?? null,
|
|
2189
|
-
selectedText: el?.text ?? null,
|
|
2190
|
-
boundingBox: el?.boundingBox ?? null,
|
|
2191
|
-
timestamp
|
|
2192
|
-
}
|
|
2193
|
-
};
|
|
2194
|
-
}
|
|
2195
2219
|
return {
|
|
2196
2220
|
clientSubmissionId: report.clientSubmissionId,
|
|
2197
2221
|
clientType,
|
|
@@ -2204,6 +2228,22 @@ function buildLassoEnvelope(report) {
|
|
|
2204
2228
|
comment: renderReportBody(report),
|
|
2205
2229
|
pin: report.pin ? { x: report.pin.x, y: report.pin.y } : null,
|
|
2206
2230
|
sourceFile: el?.attributes["data-feedback-source"] ?? null,
|
|
2231
|
+
element: el ? {
|
|
2232
|
+
tag: el.tag,
|
|
2233
|
+
// 요소를 다시 찾는 선택자. 없으면 사람이 화면에서 찾아야 하지만
|
|
2234
|
+
// 제보 자체는 성립한다.
|
|
2235
|
+
path: el.selector ?? null,
|
|
2236
|
+
text: el.text,
|
|
2237
|
+
cssClasses: el.className,
|
|
2238
|
+
reactComponents: el.attributes["react-components"] ?? null,
|
|
2239
|
+
boundingBox: el.boundingBox
|
|
2240
|
+
} : null,
|
|
2241
|
+
web: clientType === "web" ? {
|
|
2242
|
+
url: c.url,
|
|
2243
|
+
route: c.screen,
|
|
2244
|
+
viewport: c.web?.viewport ?? null,
|
|
2245
|
+
userAgent: c.web?.userAgent ?? null
|
|
2246
|
+
} : null,
|
|
2207
2247
|
screenshotBase64: report.screenshot?.base64 ?? null,
|
|
2208
2248
|
screenshotContentType: report.screenshot?.contentType ?? null,
|
|
2209
2249
|
// 서버가 없으면 빈 배열로 취급하지만, 미연동과 "없었음"을 구분해 보낸다.
|
|
@@ -2259,29 +2299,38 @@ function createLassoAdapter(opts = {}) {
|
|
|
2259
2299
|
return fail(true);
|
|
2260
2300
|
}
|
|
2261
2301
|
const status = res.status;
|
|
2302
|
+
let text = "";
|
|
2303
|
+
try {
|
|
2304
|
+
text = await res.text();
|
|
2305
|
+
} catch {
|
|
2306
|
+
}
|
|
2262
2307
|
if (status < 200 || status >= 300) {
|
|
2308
|
+
const reason = reasonFrom(text);
|
|
2263
2309
|
if (status === 429) {
|
|
2264
|
-
emit(
|
|
2310
|
+
emit(`feedback-kit: \uC218\uC9D1 \uD55C\uB3C4\uC5D0 \uAC78\uB838\uB2E4(429). \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.${reason}`);
|
|
2265
2311
|
return fail(true, { retryAfterMs: parseRetryAfterMs(res), rateLimited: true });
|
|
2266
2312
|
}
|
|
2267
2313
|
const retryable = RETRYABLE_STATUS.has(status);
|
|
2268
2314
|
emit(
|
|
2269
|
-
`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status}${retryable ? " (\uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4)" : " (\uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4)"}`
|
|
2315
|
+
`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status}${retryable ? " (\uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4)" : " (\uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4)"}${reason}`
|
|
2270
2316
|
);
|
|
2271
2317
|
return fail(retryable, { retryAfterMs: retryable ? parseRetryAfterMs(res) : null });
|
|
2272
2318
|
}
|
|
2273
2319
|
let parsed;
|
|
2274
2320
|
try {
|
|
2275
|
-
const text = await res.text();
|
|
2276
2321
|
const value = JSON.parse(text);
|
|
2277
2322
|
if (value === null || typeof value !== "object") throw new Error("\uAC1D\uCCB4\uAC00 \uC544\uB2D8");
|
|
2278
2323
|
parsed = value;
|
|
2279
2324
|
} catch {
|
|
2280
|
-
emit(
|
|
2325
|
+
emit(
|
|
2326
|
+
`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status} \uC758 \uBCF8\uBB38\uC774 JSON \uC774 \uC544\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.${reasonFrom(text)}`
|
|
2327
|
+
);
|
|
2281
2328
|
return fail(true);
|
|
2282
2329
|
}
|
|
2283
2330
|
if (parsed.ok !== true) {
|
|
2284
|
-
emit(
|
|
2331
|
+
emit(
|
|
2332
|
+
`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)}`
|
|
2333
|
+
);
|
|
2285
2334
|
return fail(false);
|
|
2286
2335
|
}
|
|
2287
2336
|
return { ok: true, id: idFrom(parsed), retryable: false, retryAfterMs: null };
|