@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.d.cts
CHANGED
|
@@ -95,13 +95,22 @@ interface DiagNetworkEntry {
|
|
|
95
95
|
url: string;
|
|
96
96
|
/** 응답을 못 받았으면(네트워크 실패·중단) null. */
|
|
97
97
|
status: number | null;
|
|
98
|
+
/**
|
|
99
|
+
* 2xx 였는가. status 로 계산할 수 있지만 따로 싣는다 —
|
|
100
|
+
* 제보를 훑는 사람이 실패한 요청만 눈으로 골라내는 게 목적이다.
|
|
101
|
+
* 응답 자체를 못 받았으면 false.
|
|
102
|
+
*/
|
|
103
|
+
ok: boolean;
|
|
104
|
+
/** 네트워크 실패·중단의 사유. 성공했으면 null. 응답 본문은 담지 않는다. */
|
|
105
|
+
errorMessage: string | null;
|
|
98
106
|
durationMs: number;
|
|
99
107
|
/** 기록 시각(ISO 8601). */
|
|
100
108
|
at: string;
|
|
101
109
|
}
|
|
102
110
|
/** 콘솔 기록 1건. `console.warn` / `console.error` 만 대상. */
|
|
103
111
|
interface DiagLogEntry {
|
|
104
|
-
|
|
112
|
+
/** `log` 은 captureLogs: "all" 일 때만 담긴다. */
|
|
113
|
+
level: "log" | "warn" | "error";
|
|
105
114
|
message: string;
|
|
106
115
|
at: string;
|
|
107
116
|
}
|
|
@@ -517,14 +526,16 @@ declare const MAX_LOG_MESSAGE_CHARS = 2000;
|
|
|
517
526
|
interface DiagnosticsInstallOpts {
|
|
518
527
|
/** true 를 반환하면 그 요청은 기록하지 않는다(위젯 자신의 전송 등). */
|
|
519
528
|
excludeMatcher?: DiagnosticsExcludeMatcher;
|
|
529
|
+
/**
|
|
530
|
+
* 콘솔 로그를 어디까지 담을지. 기본 `problems`(warn·error 만).
|
|
531
|
+
* `all` 은 log·info 까지 담는다 — 흐름 추적엔 좋지만 평범한 로그가 많은 앱에서는
|
|
532
|
+
* 그게 제보를 덮어 정작 중요한 경고가 묻힌다. 그래서 프로젝트마다 정한다.
|
|
533
|
+
*/
|
|
534
|
+
logs?: "problems" | "all" | "off";
|
|
520
535
|
/** 시각 주입(테스트용). */
|
|
521
536
|
now?: () => number;
|
|
522
537
|
iso?: (n: number) => string;
|
|
523
538
|
}
|
|
524
|
-
/**
|
|
525
|
-
* 진단 수집기. `install()` 로 글로벌을 패치하고 `uninstall()` 로 원복한다.
|
|
526
|
-
* 생성 자체는 부작용이 없다 — `install()` 을 호출해야 패치가 일어난다.
|
|
527
|
-
*/
|
|
528
539
|
declare class DiagnosticsCollector {
|
|
529
540
|
readonly network: RingBuffer<DiagNetworkEntry>;
|
|
530
541
|
readonly logs: RingBuffer<DiagLogEntry>;
|
|
@@ -534,7 +545,9 @@ declare class DiagnosticsCollector {
|
|
|
534
545
|
private originalXhrSend;
|
|
535
546
|
private originalWarn;
|
|
536
547
|
private originalError;
|
|
548
|
+
private originalLog;
|
|
537
549
|
private excludeMatcher;
|
|
550
|
+
private logScope;
|
|
538
551
|
private now;
|
|
539
552
|
private iso;
|
|
540
553
|
/**
|
|
@@ -568,6 +581,7 @@ declare class DiagnosticsCollector {
|
|
|
568
581
|
snapshot(): DiagnosticsPayload;
|
|
569
582
|
/** 두 버퍼를 비운다. */
|
|
570
583
|
clear(): void;
|
|
584
|
+
/** 실패 사유 한 줄. 예외 객체엔 요청 본문이 들어 있을 수 있어 메시지만, 길이도 자른다. */
|
|
571
585
|
private recordNetwork;
|
|
572
586
|
private recordLog;
|
|
573
587
|
private isExcluded;
|
|
@@ -661,6 +675,16 @@ interface FeedbackConfig {
|
|
|
661
675
|
captureScreenshot?: boolean;
|
|
662
676
|
/** network·logs 링버퍼를 수집할지. false 면 아예 설치하지 않는다. */
|
|
663
677
|
captureDiagnostics?: boolean;
|
|
678
|
+
/**
|
|
679
|
+
* 콘솔 로그를 담을 범위. 프로젝트마다 다르다 — 로그를 정돈해 쓰는 팀에는
|
|
680
|
+
* `all` 이 가장 유용하고, 평범한 로그가 수백 줄씩 나오는 앱에서는 그게
|
|
681
|
+
* 제보를 덮어 정작 중요한 경고가 묻힌다.
|
|
682
|
+
*
|
|
683
|
+
* - `problems`(기본): warn·error 만. 노이즈 없이 문제만 남는다.
|
|
684
|
+
* - `all`: log·info 까지 전부. 개발 중 흐름 추적에 유리하다.
|
|
685
|
+
* - `off`: 로그를 담지 않는다. 네트워크는 그대로 수집한다.
|
|
686
|
+
*/
|
|
687
|
+
captureLogs?: LogCapture;
|
|
664
688
|
/** 플로팅 버튼 위치. 모서리만 주면 여백은 기본값. */
|
|
665
689
|
position?: WidgetCorner | Partial<WidgetPosition>;
|
|
666
690
|
/** `internal` 판정을 프로젝트가 직접 할 때. 주면 `internalRoles` 보다 우선한다. */
|
|
@@ -672,6 +696,8 @@ interface FeedbackConfig {
|
|
|
672
696
|
/** 경고 출구. 안 주면 `resolveConfig` 결과의 `warnings` 로만 남는다. */
|
|
673
697
|
onWarn?: (warning: ConfigWarning) => void;
|
|
674
698
|
}
|
|
699
|
+
/** 콘솔 로그 수집 범위. 프로젝트마다 정한다. */
|
|
700
|
+
type LogCapture = "problems" | "all" | "off";
|
|
675
701
|
/** 기본값이 모두 채워진 설정. 위젯은 이것만 본다. */
|
|
676
702
|
interface ResolvedConfig {
|
|
677
703
|
token: string | null;
|
|
@@ -680,6 +706,7 @@ interface ResolvedConfig {
|
|
|
680
706
|
visibility: Visibility;
|
|
681
707
|
captureScreenshot: boolean;
|
|
682
708
|
captureDiagnostics: boolean;
|
|
709
|
+
captureLogs: LogCapture;
|
|
683
710
|
position: WidgetPosition;
|
|
684
711
|
isInternal: VisibilityFn | null;
|
|
685
712
|
internalRoles: readonly string[];
|
|
@@ -877,6 +904,11 @@ declare const MODAL_ACTION_SEND = "send";
|
|
|
877
904
|
interface ReportModalState {
|
|
878
905
|
open: boolean;
|
|
879
906
|
comment: string;
|
|
907
|
+
/**
|
|
908
|
+
* 요소 지목 모드에서 고른 요소. 없으면 화면 전체에 대한 제보다.
|
|
909
|
+
* 이게 있으면 제보 종류가 `annotation` 이 되어 수집 쪽에서 지목 제보로 갈린다.
|
|
910
|
+
*/
|
|
911
|
+
element: ElementInfo | null;
|
|
880
912
|
priority: FeedbackPriority;
|
|
881
913
|
screenshot: FeedbackScreenshot | null;
|
|
882
914
|
screenshotStatus: ScreenshotStatus;
|
|
@@ -947,7 +979,9 @@ declare class ReportModalController {
|
|
|
947
979
|
dispose(): void;
|
|
948
980
|
/** WidgetController가 소유한 큐 lifecycle을 함께 정리한다. */
|
|
949
981
|
stopQueue(): void;
|
|
950
|
-
open(
|
|
982
|
+
open(opts?: {
|
|
983
|
+
element?: ElementInfo | null;
|
|
984
|
+
}): Promise<void>;
|
|
951
985
|
/**
|
|
952
986
|
* 닫기 요청. 쓰던 내용이 있으면 곧장 닫지 않고 확인을 받는다.
|
|
953
987
|
* @returns 실제로 닫혔으면 `"closed"`, 확인이 필요하면 `"confirm"`.
|
|
@@ -1024,7 +1058,14 @@ declare class WidgetController {
|
|
|
1024
1058
|
get isPicking(): boolean;
|
|
1025
1059
|
subscribe(listener: WidgetListener): () => void;
|
|
1026
1060
|
dispose(): void;
|
|
1027
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
|
|
1063
|
+
* `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
|
|
1064
|
+
* `picking` 에 머물러 **모달이 렌더되지 않는다**.
|
|
1065
|
+
*/
|
|
1066
|
+
openReport(opts?: {
|
|
1067
|
+
element?: ElementInfo | null;
|
|
1068
|
+
}): Promise<void>;
|
|
1028
1069
|
/** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
|
|
1029
1070
|
closeReport(): "closed" | "confirm";
|
|
1030
1071
|
confirmCloseReport(): void;
|
|
@@ -1191,6 +1232,20 @@ interface LassoEnvelope {
|
|
|
1191
1232
|
y: number;
|
|
1192
1233
|
} | null;
|
|
1193
1234
|
sourceFile: string | null;
|
|
1235
|
+
/**
|
|
1236
|
+
* 대시보드가 구조화 필드로 읽는 자리. 본문 마크다운에도 같은 값이 들어가지만,
|
|
1237
|
+
* 거기 있는 건 사람이 읽는 용도라 화면의 뷰포트 칸이 비어 있었다(실측).
|
|
1238
|
+
*/
|
|
1239
|
+
web: {
|
|
1240
|
+
url: string | null;
|
|
1241
|
+
route: string | null;
|
|
1242
|
+
viewport: {
|
|
1243
|
+
width: number | null;
|
|
1244
|
+
height: number | null;
|
|
1245
|
+
devicePixelRatio: number | null;
|
|
1246
|
+
} | null;
|
|
1247
|
+
userAgent: string | null;
|
|
1248
|
+
} | null;
|
|
1194
1249
|
screenshotBase64: string | null;
|
|
1195
1250
|
screenshotContentType: "image/jpeg" | "image/png" | null;
|
|
1196
1251
|
network: unknown[];
|
package/dist/index.d.ts
CHANGED
|
@@ -95,13 +95,22 @@ interface DiagNetworkEntry {
|
|
|
95
95
|
url: string;
|
|
96
96
|
/** 응답을 못 받았으면(네트워크 실패·중단) null. */
|
|
97
97
|
status: number | null;
|
|
98
|
+
/**
|
|
99
|
+
* 2xx 였는가. status 로 계산할 수 있지만 따로 싣는다 —
|
|
100
|
+
* 제보를 훑는 사람이 실패한 요청만 눈으로 골라내는 게 목적이다.
|
|
101
|
+
* 응답 자체를 못 받았으면 false.
|
|
102
|
+
*/
|
|
103
|
+
ok: boolean;
|
|
104
|
+
/** 네트워크 실패·중단의 사유. 성공했으면 null. 응답 본문은 담지 않는다. */
|
|
105
|
+
errorMessage: string | null;
|
|
98
106
|
durationMs: number;
|
|
99
107
|
/** 기록 시각(ISO 8601). */
|
|
100
108
|
at: string;
|
|
101
109
|
}
|
|
102
110
|
/** 콘솔 기록 1건. `console.warn` / `console.error` 만 대상. */
|
|
103
111
|
interface DiagLogEntry {
|
|
104
|
-
|
|
112
|
+
/** `log` 은 captureLogs: "all" 일 때만 담긴다. */
|
|
113
|
+
level: "log" | "warn" | "error";
|
|
105
114
|
message: string;
|
|
106
115
|
at: string;
|
|
107
116
|
}
|
|
@@ -517,14 +526,16 @@ declare const MAX_LOG_MESSAGE_CHARS = 2000;
|
|
|
517
526
|
interface DiagnosticsInstallOpts {
|
|
518
527
|
/** true 를 반환하면 그 요청은 기록하지 않는다(위젯 자신의 전송 등). */
|
|
519
528
|
excludeMatcher?: DiagnosticsExcludeMatcher;
|
|
529
|
+
/**
|
|
530
|
+
* 콘솔 로그를 어디까지 담을지. 기본 `problems`(warn·error 만).
|
|
531
|
+
* `all` 은 log·info 까지 담는다 — 흐름 추적엔 좋지만 평범한 로그가 많은 앱에서는
|
|
532
|
+
* 그게 제보를 덮어 정작 중요한 경고가 묻힌다. 그래서 프로젝트마다 정한다.
|
|
533
|
+
*/
|
|
534
|
+
logs?: "problems" | "all" | "off";
|
|
520
535
|
/** 시각 주입(테스트용). */
|
|
521
536
|
now?: () => number;
|
|
522
537
|
iso?: (n: number) => string;
|
|
523
538
|
}
|
|
524
|
-
/**
|
|
525
|
-
* 진단 수집기. `install()` 로 글로벌을 패치하고 `uninstall()` 로 원복한다.
|
|
526
|
-
* 생성 자체는 부작용이 없다 — `install()` 을 호출해야 패치가 일어난다.
|
|
527
|
-
*/
|
|
528
539
|
declare class DiagnosticsCollector {
|
|
529
540
|
readonly network: RingBuffer<DiagNetworkEntry>;
|
|
530
541
|
readonly logs: RingBuffer<DiagLogEntry>;
|
|
@@ -534,7 +545,9 @@ declare class DiagnosticsCollector {
|
|
|
534
545
|
private originalXhrSend;
|
|
535
546
|
private originalWarn;
|
|
536
547
|
private originalError;
|
|
548
|
+
private originalLog;
|
|
537
549
|
private excludeMatcher;
|
|
550
|
+
private logScope;
|
|
538
551
|
private now;
|
|
539
552
|
private iso;
|
|
540
553
|
/**
|
|
@@ -568,6 +581,7 @@ declare class DiagnosticsCollector {
|
|
|
568
581
|
snapshot(): DiagnosticsPayload;
|
|
569
582
|
/** 두 버퍼를 비운다. */
|
|
570
583
|
clear(): void;
|
|
584
|
+
/** 실패 사유 한 줄. 예외 객체엔 요청 본문이 들어 있을 수 있어 메시지만, 길이도 자른다. */
|
|
571
585
|
private recordNetwork;
|
|
572
586
|
private recordLog;
|
|
573
587
|
private isExcluded;
|
|
@@ -661,6 +675,16 @@ interface FeedbackConfig {
|
|
|
661
675
|
captureScreenshot?: boolean;
|
|
662
676
|
/** network·logs 링버퍼를 수집할지. false 면 아예 설치하지 않는다. */
|
|
663
677
|
captureDiagnostics?: boolean;
|
|
678
|
+
/**
|
|
679
|
+
* 콘솔 로그를 담을 범위. 프로젝트마다 다르다 — 로그를 정돈해 쓰는 팀에는
|
|
680
|
+
* `all` 이 가장 유용하고, 평범한 로그가 수백 줄씩 나오는 앱에서는 그게
|
|
681
|
+
* 제보를 덮어 정작 중요한 경고가 묻힌다.
|
|
682
|
+
*
|
|
683
|
+
* - `problems`(기본): warn·error 만. 노이즈 없이 문제만 남는다.
|
|
684
|
+
* - `all`: log·info 까지 전부. 개발 중 흐름 추적에 유리하다.
|
|
685
|
+
* - `off`: 로그를 담지 않는다. 네트워크는 그대로 수집한다.
|
|
686
|
+
*/
|
|
687
|
+
captureLogs?: LogCapture;
|
|
664
688
|
/** 플로팅 버튼 위치. 모서리만 주면 여백은 기본값. */
|
|
665
689
|
position?: WidgetCorner | Partial<WidgetPosition>;
|
|
666
690
|
/** `internal` 판정을 프로젝트가 직접 할 때. 주면 `internalRoles` 보다 우선한다. */
|
|
@@ -672,6 +696,8 @@ interface FeedbackConfig {
|
|
|
672
696
|
/** 경고 출구. 안 주면 `resolveConfig` 결과의 `warnings` 로만 남는다. */
|
|
673
697
|
onWarn?: (warning: ConfigWarning) => void;
|
|
674
698
|
}
|
|
699
|
+
/** 콘솔 로그 수집 범위. 프로젝트마다 정한다. */
|
|
700
|
+
type LogCapture = "problems" | "all" | "off";
|
|
675
701
|
/** 기본값이 모두 채워진 설정. 위젯은 이것만 본다. */
|
|
676
702
|
interface ResolvedConfig {
|
|
677
703
|
token: string | null;
|
|
@@ -680,6 +706,7 @@ interface ResolvedConfig {
|
|
|
680
706
|
visibility: Visibility;
|
|
681
707
|
captureScreenshot: boolean;
|
|
682
708
|
captureDiagnostics: boolean;
|
|
709
|
+
captureLogs: LogCapture;
|
|
683
710
|
position: WidgetPosition;
|
|
684
711
|
isInternal: VisibilityFn | null;
|
|
685
712
|
internalRoles: readonly string[];
|
|
@@ -877,6 +904,11 @@ declare const MODAL_ACTION_SEND = "send";
|
|
|
877
904
|
interface ReportModalState {
|
|
878
905
|
open: boolean;
|
|
879
906
|
comment: string;
|
|
907
|
+
/**
|
|
908
|
+
* 요소 지목 모드에서 고른 요소. 없으면 화면 전체에 대한 제보다.
|
|
909
|
+
* 이게 있으면 제보 종류가 `annotation` 이 되어 수집 쪽에서 지목 제보로 갈린다.
|
|
910
|
+
*/
|
|
911
|
+
element: ElementInfo | null;
|
|
880
912
|
priority: FeedbackPriority;
|
|
881
913
|
screenshot: FeedbackScreenshot | null;
|
|
882
914
|
screenshotStatus: ScreenshotStatus;
|
|
@@ -947,7 +979,9 @@ declare class ReportModalController {
|
|
|
947
979
|
dispose(): void;
|
|
948
980
|
/** WidgetController가 소유한 큐 lifecycle을 함께 정리한다. */
|
|
949
981
|
stopQueue(): void;
|
|
950
|
-
open(
|
|
982
|
+
open(opts?: {
|
|
983
|
+
element?: ElementInfo | null;
|
|
984
|
+
}): Promise<void>;
|
|
951
985
|
/**
|
|
952
986
|
* 닫기 요청. 쓰던 내용이 있으면 곧장 닫지 않고 확인을 받는다.
|
|
953
987
|
* @returns 실제로 닫혔으면 `"closed"`, 확인이 필요하면 `"confirm"`.
|
|
@@ -1024,7 +1058,14 @@ declare class WidgetController {
|
|
|
1024
1058
|
get isPicking(): boolean;
|
|
1025
1059
|
subscribe(listener: WidgetListener): () => void;
|
|
1026
1060
|
dispose(): void;
|
|
1027
|
-
|
|
1061
|
+
/**
|
|
1062
|
+
* @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
|
|
1063
|
+
* `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
|
|
1064
|
+
* `picking` 에 머물러 **모달이 렌더되지 않는다**.
|
|
1065
|
+
*/
|
|
1066
|
+
openReport(opts?: {
|
|
1067
|
+
element?: ElementInfo | null;
|
|
1068
|
+
}): Promise<void>;
|
|
1028
1069
|
/** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
|
|
1029
1070
|
closeReport(): "closed" | "confirm";
|
|
1030
1071
|
confirmCloseReport(): void;
|
|
@@ -1191,6 +1232,20 @@ interface LassoEnvelope {
|
|
|
1191
1232
|
y: number;
|
|
1192
1233
|
} | null;
|
|
1193
1234
|
sourceFile: string | null;
|
|
1235
|
+
/**
|
|
1236
|
+
* 대시보드가 구조화 필드로 읽는 자리. 본문 마크다운에도 같은 값이 들어가지만,
|
|
1237
|
+
* 거기 있는 건 사람이 읽는 용도라 화면의 뷰포트 칸이 비어 있었다(실측).
|
|
1238
|
+
*/
|
|
1239
|
+
web: {
|
|
1240
|
+
url: string | null;
|
|
1241
|
+
route: string | null;
|
|
1242
|
+
viewport: {
|
|
1243
|
+
width: number | null;
|
|
1244
|
+
height: number | null;
|
|
1245
|
+
devicePixelRatio: number | null;
|
|
1246
|
+
} | null;
|
|
1247
|
+
userAgent: string | null;
|
|
1248
|
+
} | null;
|
|
1194
1249
|
screenshotBase64: string | null;
|
|
1195
1250
|
screenshotContentType: "image/jpeg" | "image/png" | null;
|
|
1196
1251
|
network: unknown[];
|
package/dist/index.js
CHANGED
|
@@ -627,6 +627,10 @@ var RingBuffer = class {
|
|
|
627
627
|
var NETWORK_BUFFER_LIMIT = 30;
|
|
628
628
|
var LOG_BUFFER_LIMIT = 50;
|
|
629
629
|
var MAX_LOG_MESSAGE_CHARS = 2e3;
|
|
630
|
+
function failureReason(error) {
|
|
631
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
632
|
+
return message.slice(0, 200) || "\uB124\uD2B8\uC6CC\uD06C \uC2E4\uD328";
|
|
633
|
+
}
|
|
630
634
|
var DiagnosticsCollector = class {
|
|
631
635
|
constructor(opts = {}) {
|
|
632
636
|
this.installed = false;
|
|
@@ -636,7 +640,9 @@ var DiagnosticsCollector = class {
|
|
|
636
640
|
this.originalXhrSend = null;
|
|
637
641
|
this.originalWarn = null;
|
|
638
642
|
this.originalError = null;
|
|
643
|
+
this.originalLog = null;
|
|
639
644
|
this.excludeMatcher = null;
|
|
645
|
+
this.logScope = "problems";
|
|
640
646
|
/**
|
|
641
647
|
* 패치된 fetch 안에서 동기적으로 XHR 이 열리는 깊이.
|
|
642
648
|
*
|
|
@@ -669,6 +675,7 @@ var DiagnosticsCollector = class {
|
|
|
669
675
|
if (this.installed) return;
|
|
670
676
|
this.installed = true;
|
|
671
677
|
this.excludeMatcher = opts.excludeMatcher ?? null;
|
|
678
|
+
this.logScope = opts.logs ?? "problems";
|
|
672
679
|
if (opts.now) this.now = opts.now;
|
|
673
680
|
if (opts.iso) this.iso = opts.iso;
|
|
674
681
|
this.patchFetch();
|
|
@@ -694,8 +701,10 @@ var DiagnosticsCollector = class {
|
|
|
694
701
|
const c = g.console;
|
|
695
702
|
if (c && this.originalWarn) c.warn = this.originalWarn;
|
|
696
703
|
if (c && this.originalError) c.error = this.originalError;
|
|
704
|
+
if (c && this.originalLog) c.log = this.originalLog;
|
|
697
705
|
this.originalWarn = null;
|
|
698
706
|
this.originalError = null;
|
|
707
|
+
this.originalLog = null;
|
|
699
708
|
this.excludeMatcher = null;
|
|
700
709
|
this.fetchDepth = 0;
|
|
701
710
|
}
|
|
@@ -712,11 +721,15 @@ var DiagnosticsCollector = class {
|
|
|
712
721
|
this.logs.clear();
|
|
713
722
|
}
|
|
714
723
|
// ── 내부: 기록 ────────────────────────────────────────────
|
|
715
|
-
|
|
724
|
+
/** 실패 사유 한 줄. 예외 객체엔 요청 본문이 들어 있을 수 있어 메시지만, 길이도 자른다. */
|
|
725
|
+
recordNetwork(method, url, status, startedAt, errorMessage = null) {
|
|
716
726
|
this.network.push({
|
|
717
727
|
method,
|
|
718
728
|
url,
|
|
719
729
|
status,
|
|
730
|
+
// 제보를 훑는 사람이 실패한 요청만 눈으로 골라낼 수 있게 따로 싣는다.
|
|
731
|
+
ok: status !== null && status >= 200 && status < 300,
|
|
732
|
+
errorMessage,
|
|
720
733
|
durationMs: Math.max(0, Math.round(this.now() - startedAt)),
|
|
721
734
|
at: this.iso(startedAt)
|
|
722
735
|
});
|
|
@@ -753,7 +766,7 @@ var DiagnosticsCollector = class {
|
|
|
753
766
|
pending = Promise.resolve(originalFetch(input, init));
|
|
754
767
|
} catch (err) {
|
|
755
768
|
self.fetchDepth -= 1;
|
|
756
|
-
if (!excluded) self.recordNetwork(method, url, null, start);
|
|
769
|
+
if (!excluded) self.recordNetwork(method, url, null, start, failureReason(err));
|
|
757
770
|
throw err;
|
|
758
771
|
}
|
|
759
772
|
self.fetchDepth -= 1;
|
|
@@ -771,7 +784,7 @@ var DiagnosticsCollector = class {
|
|
|
771
784
|
return raw;
|
|
772
785
|
},
|
|
773
786
|
(err) => {
|
|
774
|
-
if (!excluded) self.recordNetwork(method, url, null, start);
|
|
787
|
+
if (!excluded) self.recordNetwork(method, url, null, start, failureReason(err));
|
|
775
788
|
throw err;
|
|
776
789
|
}
|
|
777
790
|
);
|
|
@@ -819,6 +832,7 @@ var DiagnosticsCollector = class {
|
|
|
819
832
|
};
|
|
820
833
|
}
|
|
821
834
|
patchConsole() {
|
|
835
|
+
if (this.logScope === "off") return;
|
|
822
836
|
const c = globalThis.console;
|
|
823
837
|
if (!c) return;
|
|
824
838
|
const self = this;
|
|
@@ -838,6 +852,14 @@ var DiagnosticsCollector = class {
|
|
|
838
852
|
orig(...args);
|
|
839
853
|
};
|
|
840
854
|
}
|
|
855
|
+
if (this.logScope === "all" && typeof c.log === "function") {
|
|
856
|
+
const orig = c.log.bind(c);
|
|
857
|
+
this.originalLog = orig;
|
|
858
|
+
c.log = function patchedLog(...args) {
|
|
859
|
+
self.recordLog("log", args);
|
|
860
|
+
orig(...args);
|
|
861
|
+
};
|
|
862
|
+
}
|
|
841
863
|
}
|
|
842
864
|
};
|
|
843
865
|
var sharedDiagnostics = new DiagnosticsCollector();
|
|
@@ -1056,6 +1078,7 @@ function resolveConfig(config = {}) {
|
|
|
1056
1078
|
visibility: normalizeVisibility(config.visibility, warn),
|
|
1057
1079
|
captureScreenshot: config.captureScreenshot ?? true,
|
|
1058
1080
|
captureDiagnostics: config.captureDiagnostics ?? true,
|
|
1081
|
+
captureLogs: config.captureLogs ?? "problems",
|
|
1059
1082
|
position: normalizePosition(config.position, warn),
|
|
1060
1083
|
isInternal: typeof config.isInternal === "function" ? config.isInternal : null,
|
|
1061
1084
|
internalRoles: config.internalRoles ?? DEFAULT_INTERNAL_ROLES,
|
|
@@ -1337,6 +1360,7 @@ var ReportModalController = class {
|
|
|
1337
1360
|
this.captureGeneration = 0;
|
|
1338
1361
|
this.state = {
|
|
1339
1362
|
open: false,
|
|
1363
|
+
element: null,
|
|
1340
1364
|
comment: "",
|
|
1341
1365
|
priority: "unset",
|
|
1342
1366
|
screenshot: null,
|
|
@@ -1402,7 +1426,7 @@ var ReportModalController = class {
|
|
|
1402
1426
|
this.queue.stop?.();
|
|
1403
1427
|
}
|
|
1404
1428
|
// ── 열기/닫기 ─────────────────────────────────────────────────────────────
|
|
1405
|
-
async open() {
|
|
1429
|
+
async open(opts = {}) {
|
|
1406
1430
|
if (this.state.open) return;
|
|
1407
1431
|
const wasPending = this.state.submitStatus === "pending" && this.lastReport !== null;
|
|
1408
1432
|
const pendingReport = wasPending ? this.lastReport : null;
|
|
@@ -1411,6 +1435,8 @@ var ReportModalController = class {
|
|
|
1411
1435
|
this.patch({
|
|
1412
1436
|
open: true,
|
|
1413
1437
|
comment: pendingReport?.comment ?? "",
|
|
1438
|
+
// 요소를 지목해서 연 경우엔 그 요소를 물고 간다. 대기 항목 복원이 우선이다.
|
|
1439
|
+
element: pendingReport?.element ?? opts.element ?? null,
|
|
1414
1440
|
priority: pendingReport?.priority ?? "unset",
|
|
1415
1441
|
screenshot: pendingReport?.screenshot ?? null,
|
|
1416
1442
|
screenshotStatus: "none",
|
|
@@ -1570,12 +1596,13 @@ var ReportModalController = class {
|
|
|
1570
1596
|
}
|
|
1571
1597
|
this.patch({ submitStatus: "sending", submitMessage: null });
|
|
1572
1598
|
const parts = {
|
|
1573
|
-
|
|
1599
|
+
// 요소를 지목했으면 지목 제보다. 수집 쪽이 이 값으로 갈린다.
|
|
1600
|
+
kind: this.state.element ? "annotation" : "report",
|
|
1574
1601
|
comment: this.state.comment,
|
|
1575
1602
|
priority: this.state.priority,
|
|
1576
1603
|
screenshot: this.state.screenshot,
|
|
1577
1604
|
pin: this.state.pin,
|
|
1578
|
-
element:
|
|
1605
|
+
element: this.state.element
|
|
1579
1606
|
};
|
|
1580
1607
|
let outcome;
|
|
1581
1608
|
try {
|
|
@@ -1727,9 +1754,14 @@ var WidgetController = class {
|
|
|
1727
1754
|
this.listeners.clear();
|
|
1728
1755
|
}
|
|
1729
1756
|
// ── 플로팅 버튼 ↔ 모달 ────────────────────────────────────────────────────
|
|
1730
|
-
|
|
1757
|
+
/**
|
|
1758
|
+
* @param opts.element 요소를 지목해서 열 때 그 요소. 지목 모드에서도 화면을
|
|
1759
|
+
* `modal` 로 바꾼다 — `modal.open()` 을 직접 부르면 상태만 열리고 화면은
|
|
1760
|
+
* `picking` 에 머물러 **모달이 렌더되지 않는다**.
|
|
1761
|
+
*/
|
|
1762
|
+
async openReport(opts = {}) {
|
|
1731
1763
|
this.screen = "modal";
|
|
1732
|
-
await this.modal.open();
|
|
1764
|
+
await this.modal.open(opts);
|
|
1733
1765
|
this.emit();
|
|
1734
1766
|
}
|
|
1735
1767
|
/** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
|
|
@@ -2053,6 +2085,19 @@ function parseRetryAfterMs(res) {
|
|
|
2053
2085
|
const seconds = Number(raw.trim());
|
|
2054
2086
|
return Number.isFinite(seconds) && seconds >= 0 ? Math.round(seconds * 1e3) : null;
|
|
2055
2087
|
}
|
|
2088
|
+
function reasonFrom(text) {
|
|
2089
|
+
try {
|
|
2090
|
+
const body = JSON.parse(text);
|
|
2091
|
+
const code = typeof body.code === "string" ? body.code : null;
|
|
2092
|
+
const message = typeof body.message === "string" ? body.message : null;
|
|
2093
|
+
const joined = [code, message].filter(Boolean).join(": ");
|
|
2094
|
+
if (joined) return ` \u2014 ${joined.slice(0, 200)}`;
|
|
2095
|
+
} catch {
|
|
2096
|
+
const head = text.trim().slice(0, 120);
|
|
2097
|
+
if (head) return ` \u2014 ${head}`;
|
|
2098
|
+
}
|
|
2099
|
+
return "";
|
|
2100
|
+
}
|
|
2056
2101
|
function fail(retryable, extra) {
|
|
2057
2102
|
return { ok: false, id: null, retryable, retryAfterMs: null, ...extra };
|
|
2058
2103
|
}
|
|
@@ -2107,6 +2152,12 @@ function buildLassoEnvelope(report) {
|
|
|
2107
2152
|
comment: renderReportBody(report),
|
|
2108
2153
|
pin: report.pin ? { x: report.pin.x, y: report.pin.y } : null,
|
|
2109
2154
|
sourceFile: el?.attributes["data-feedback-source"] ?? null,
|
|
2155
|
+
web: clientType === "web" ? {
|
|
2156
|
+
url: c.url,
|
|
2157
|
+
route: c.screen,
|
|
2158
|
+
viewport: c.web?.viewport ?? null,
|
|
2159
|
+
userAgent: c.web?.userAgent ?? null
|
|
2160
|
+
} : null,
|
|
2110
2161
|
screenshotBase64: report.screenshot?.base64 ?? null,
|
|
2111
2162
|
screenshotContentType: report.screenshot?.contentType ?? null,
|
|
2112
2163
|
// 서버가 없으면 빈 배열로 취급하지만, 미연동과 "없었음"을 구분해 보낸다.
|
|
@@ -2162,29 +2213,38 @@ function createLassoAdapter(opts = {}) {
|
|
|
2162
2213
|
return fail(true);
|
|
2163
2214
|
}
|
|
2164
2215
|
const status = res.status;
|
|
2216
|
+
let text = "";
|
|
2217
|
+
try {
|
|
2218
|
+
text = await res.text();
|
|
2219
|
+
} catch {
|
|
2220
|
+
}
|
|
2165
2221
|
if (status < 200 || status >= 300) {
|
|
2222
|
+
const reason = reasonFrom(text);
|
|
2166
2223
|
if (status === 429) {
|
|
2167
|
-
emit(
|
|
2224
|
+
emit(`feedback-kit: \uC218\uC9D1 \uD55C\uB3C4\uC5D0 \uAC78\uB838\uB2E4(429). \uC7A0\uC2DC \uB4A4 \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.${reason}`);
|
|
2168
2225
|
return fail(true, { retryAfterMs: parseRetryAfterMs(res), rateLimited: true });
|
|
2169
2226
|
}
|
|
2170
2227
|
const retryable = RETRYABLE_STATUS.has(status);
|
|
2171
2228
|
emit(
|
|
2172
|
-
`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status}${retryable ? " (\uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4)" : " (\uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4)"}`
|
|
2229
|
+
`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status}${retryable ? " (\uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4)" : " (\uC7AC\uC2DC\uB3C4\uD558\uC9C0 \uC54A\uB294\uB2E4)"}${reason}`
|
|
2173
2230
|
);
|
|
2174
2231
|
return fail(retryable, { retryAfterMs: retryable ? parseRetryAfterMs(res) : null });
|
|
2175
2232
|
}
|
|
2176
2233
|
let parsed;
|
|
2177
2234
|
try {
|
|
2178
|
-
const text = await res.text();
|
|
2179
2235
|
const value = JSON.parse(text);
|
|
2180
2236
|
if (value === null || typeof value !== "object") throw new Error("\uAC1D\uCCB4\uAC00 \uC544\uB2D8");
|
|
2181
2237
|
parsed = value;
|
|
2182
2238
|
} catch {
|
|
2183
|
-
emit(
|
|
2239
|
+
emit(
|
|
2240
|
+
`feedback-kit: \uC218\uC9D1 \uC751\uB2F5 ${status} \uC758 \uBCF8\uBB38\uC774 JSON \uC774 \uC544\uB2C8\uB2E4. \uB2E4\uC2DC \uC2DC\uB3C4\uD55C\uB2E4.${reasonFrom(text)}`
|
|
2241
|
+
);
|
|
2184
2242
|
return fail(true);
|
|
2185
2243
|
}
|
|
2186
2244
|
if (parsed.ok !== true) {
|
|
2187
|
-
emit(
|
|
2245
|
+
emit(
|
|
2246
|
+
`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)}`
|
|
2247
|
+
);
|
|
2188
2248
|
return fail(false);
|
|
2189
2249
|
}
|
|
2190
2250
|
return { ok: true, id: idFrom(parsed), retryable: false, retryAfterMs: null };
|