@solhun/feedback-kit-core 0.1.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.
@@ -0,0 +1,1234 @@
1
+ /** 제보 종류. `report`=일반 의견, `annotation`=화면 위 핀 찍힌 주석. */
2
+ type FeedbackKind = "report" | "annotation";
3
+ /**
4
+ * 우선순위.
5
+ * - unset: 사용자가 고르지 않음(기본)
6
+ * - normal / high / urgent: 사용자가 명시적으로 선택
7
+ */
8
+ type FeedbackPriority = "unset" | "normal" | "high" | "urgent";
9
+ /** 스크린샷 캡처 결과. base64 인코딩된 이미지 데이터 + MIME. 없으면 null. */
10
+ interface FeedbackScreenshot {
11
+ base64: string;
12
+ contentType: "image/png" | "image/jpeg";
13
+ }
14
+ /**
15
+ * 핀 좌표. 스크린샷 이미지 기준 0~1 정규화 실수.
16
+ * (0,0)=좌상단, (1,1)=우하단. 핀이 없으면 null.
17
+ */
18
+ interface FeedbackPin {
19
+ x: number;
20
+ y: number;
21
+ }
22
+ /** 핀이 찍힌 DOM/RN 요소 정보(선택). 호스트가 채워 넣는다. */
23
+ interface ElementInfo {
24
+ tag: string;
25
+ id: string | null;
26
+ className: string | null;
27
+ /** 잘린 가시 텍스트(너무 길면 자른다). */
28
+ text: string | null;
29
+ /** 뷰포트/스크린샷 기준 bounding box. 알 수 없으면 null. */
30
+ boundingBox: {
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ } | null;
36
+ /** 기타 속성. 값은 문자열로 직렬화한다. */
37
+ attributes: Record<string, string>;
38
+ /**
39
+ * 요소를 다시 찾기 위한 CSS 선택자 경로(웹 전용). 앱처럼 DOM 이 없는 곳은 생략한다.
40
+ * 선택 필드로 둔 이유: 이 값을 못 구해도 제보 자체는 성립해야 하기 때문이다.
41
+ */
42
+ selector?: string | null;
43
+ }
44
+ /**
45
+ * 호스트가 `getUser` 로 넘기는 제보자 신원.
46
+ * id는 필수(게스트면 영구 게스트 식별자). 나머지는 선택.
47
+ */
48
+ interface FeedbackUser {
49
+ id: string;
50
+ name?: string;
51
+ email?: string;
52
+ role?: string;
53
+ /** 로그인 사용자가 아니라 게스트면 true. 생략하면 로그인 사용자로 본다. */
54
+ isGuest?: boolean;
55
+ }
56
+ /**
57
+ * 제보에 실제로 실리는, 해석이 끝난 사용자.
58
+ *
59
+ * **게스트도 객체를 채운다** — `isGuest: true` + 기기에 영구 저장된 게스트 식별자.
60
+ * 전체가 `null`인 경우는 게스트 식별자조차 아직 없는 최초 실행 순간뿐이다
61
+ * (저장소 접근이 실패해 id를 만들지도 읽지도 못한 상태).
62
+ */
63
+ interface ContextUser {
64
+ id: string | null;
65
+ email: string | null;
66
+ isGuest: boolean;
67
+ name?: string;
68
+ role?: string;
69
+ }
70
+ /**
71
+ * 빌드 플러그인이 심는 화면↔소스파일 매핑. 후속 wave에서 채워진다.
72
+ * 코어는 통과만 한다(값을 해석하지 않는다).
73
+ */
74
+ interface SourceMapping {
75
+ screenId: string | null;
76
+ sourceFile: string | null;
77
+ /**
78
+ * 소스 파일 안의 줄 번호(선택).
79
+ *
80
+ * 요소 단위 매핑(빌드 플러그인이 심는 `data-fk-source`)에서만 채워진다.
81
+ * 화면 단위 매핑에는 줄 번호가 없으므로 생략되거나 null 이다.
82
+ */
83
+ sourceLine?: number | null;
84
+ }
85
+ /**
86
+ * 네트워크 기록 1건.
87
+ *
88
+ * 기록 항목은 **메서드·URL·상태코드·소요시간까지**다.
89
+ * `Authorization` 헤더·쿠키·요청 본문·응답 본문은 어떤 경우에도 담기지 않는다
90
+ * (수집 코드가 애초에 읽지 않는다 — 필드가 없는 게 아니라 접근 자체를 안 한다).
91
+ */
92
+ interface DiagNetworkEntry {
93
+ method: string;
94
+ /** 쿼리스트링의 자격증명형 파라미터는 값이 가려진 상태. */
95
+ url: string;
96
+ /** 응답을 못 받았으면(네트워크 실패·중단) null. */
97
+ status: number | null;
98
+ durationMs: number;
99
+ /** 기록 시각(ISO 8601). */
100
+ at: string;
101
+ }
102
+ /** 콘솔 기록 1건. `console.warn` / `console.error` 만 대상. */
103
+ interface DiagLogEntry {
104
+ level: "warn" | "error";
105
+ message: string;
106
+ at: string;
107
+ }
108
+ /**
109
+ * 제보에 실리는 진단 스냅샷.
110
+ *
111
+ * 두 배열은 **수집기가 실제로 모은 결과**다. 제출 시점에 비어 있는 것은 정상이고
112
+ * (그 세션에 요청·경고가 없었다는 뜻), 배선이 안 됐을 때는 배열이 아니라
113
+ * 컨텍스트의 `diagnostics` 자체가 `null`이 된다. 그래서 "빈 배열"과
114
+ * "수집 안 함"이 페이로드에서 구분된다.
115
+ */
116
+ interface DiagnosticsPayload {
117
+ network: DiagNetworkEntry[];
118
+ logs: DiagLogEntry[];
119
+ }
120
+ /** 앱 빌드 정보. 어느 빌드에서 났는지 특정하는 데 쓴다. */
121
+ interface AppInfo {
122
+ version: string | null;
123
+ channel: string | null;
124
+ updateId: string | null;
125
+ runtimeVersion: string | null;
126
+ }
127
+ /** 기기 정보. **화면 크기는 여기가 아니라 `display` 다.** */
128
+ interface DeviceInfo {
129
+ model: string | null;
130
+ osName: string | null;
131
+ osVersion: string | null;
132
+ deviceType: string | null;
133
+ }
134
+ /** 화면 크기/배율. */
135
+ interface DisplayInfo {
136
+ width: number | null;
137
+ height: number | null;
138
+ pixelRatio: number | null;
139
+ fontScale: number | null;
140
+ }
141
+ /** 앱(React Native) 전용 컨텍스트. */
142
+ interface NativeContext {
143
+ /** 화면↔파일 매핑이 있을 때의 소스 파일 경로. 매핑이 없으면 null. */
144
+ screenPath: string | null;
145
+ /** 전체 라우트 스택. 바깥(루트)부터 안쪽(현재 화면) 순서. */
146
+ navPath: string[];
147
+ /** 현재 화면의 파라미터. JSON 왕복으로 함수·순환참조를 제거한 값. */
148
+ routeParams: Record<string, unknown> | null;
149
+ appInfo: AppInfo;
150
+ device: DeviceInfo;
151
+ display: DisplayInfo;
152
+ }
153
+ /** 웹 전용 컨텍스트. */
154
+ interface WebContext {
155
+ viewport: {
156
+ width: number | null;
157
+ height: number | null;
158
+ devicePixelRatio: number | null;
159
+ };
160
+ /** userAgent 원문(파싱하지 않고 그대로 싣는다). */
161
+ userAgent: string | null;
162
+ }
163
+ /**
164
+ * 제보가 만들어진 환경 컨텍스트. 사용자 신원(user)도 여기에 포함된다.
165
+ * (FeedbackReport의 8개 스펙 필드를 건드리지 않고 신원을 전달하기 위해 context 안에 둔다.)
166
+ *
167
+ * 이 타입은 **수집까지**를 정의한다. 여기 담긴 값을 어느 필드에 실어 보낼지는
168
+ * 어댑터가 정한다(코어는 특정 어댑터의 필드명을 모른다).
169
+ */
170
+ interface FeedbackContext {
171
+ /** 앱/제품 식별자. config.app에서 온다. */
172
+ app: string;
173
+ /** 현재 화면 이름/경로(navigationRef에서 채움). 없으면 null. */
174
+ screen: string | null;
175
+ /** 현재 URL(웹) 또는 딥링크(앱). 없으면 null. */
176
+ url: string | null;
177
+ /** 세션 식별자(UUID). 위젯 인스턴스 단위로 하나. */
178
+ sessionId: string;
179
+ /** 제출 시점에 결정된 사용자. 게스트도 객체를 채운다. */
180
+ user: ContextUser | null;
181
+ /** 빌드 플러그인 매핑(선택). 없으면 screenId/sourceFile 이 null. */
182
+ source: SourceMapping;
183
+ /** 실행 플랫폼. */
184
+ platform: "web" | "native" | "unknown";
185
+ /** IANA 타임존 이름(예: `Asia/Seoul`). 알 수 없으면 null. */
186
+ timezone: string | null;
187
+ /** 클라이언트 기준 제출 시각(ISO 8601). */
188
+ clientTimestamp: string;
189
+ /** 앱 전용 컨텍스트. 웹이거나 공급자가 없으면 null. */
190
+ native: NativeContext | null;
191
+ /** 웹 전용 컨텍스트. 앱이거나 공급자가 없으면 null. */
192
+ web: WebContext | null;
193
+ /** 진단 링버퍼 스냅샷. 수집이 배선되지 않았으면 null. */
194
+ diagnostics: DiagnosticsPayload | null;
195
+ /** 자유 형식 추가 메타. 직렬화 가능한 값만. */
196
+ extra: Record<string, unknown>;
197
+ }
198
+ /**
199
+ * 하나의 피드백 제보. 통합 계약의 핵심.
200
+ * clientSubmissionId / context / createdAt 은 코어가 채운다.
201
+ * 나머지는 호스트가 submit() 호출 시 제공한다.
202
+ */
203
+ interface FeedbackReport {
204
+ /** 클라이언트에서 만든 UUID v4. 멱등 키(중복 제출 식별용). */
205
+ clientSubmissionId: string;
206
+ kind: FeedbackKind;
207
+ comment: string;
208
+ priority: FeedbackPriority;
209
+ screenshot: FeedbackScreenshot | null;
210
+ pin: FeedbackPin | null;
211
+ element: ElementInfo | null;
212
+ context: FeedbackContext;
213
+ /** 제출 시각(ISO 8601). */
214
+ createdAt: string;
215
+ }
216
+ /**
217
+ * 어댑터 제출 결과.
218
+ * - ok: 백엔드가 정상 수락. **HTTP 2xx 만으로는 부족하다** — 응답 본문의 `ok=true`까지
219
+ * 확인한 뒤에만 true 여야 한다(2xx + ok=false 는 실패로 취급).
220
+ * - id: 백엔드가 부여한 제보 id(수락 안 됐으면 null). 표시용이며 로컬 키로 쓰지 않는다.
221
+ * - retryable: 일시적 실패면 true(코어가 재시도).
222
+ * - retryAfterMs: 백엔드가 권장한 대기 시간. 없으면 null(코어가 백오프로 계산).
223
+ * - rateLimited: 429(요청 한도 초과)면 true. 재시도 간격의 일반 상한(5분)을 넘겨
224
+ * 최소 10분 뒤에 다시 시도해야 한다는 신호. 정책 판단은 코어가 한다
225
+ * (어댑터는 "429였다"는 사실만 전달한다).
226
+ */
227
+ interface SubmitResult {
228
+ ok: boolean;
229
+ id: string | null;
230
+ retryable: boolean;
231
+ retryAfterMs: number | null;
232
+ rateLimited?: boolean;
233
+ }
234
+ /**
235
+ * 영속 저장소 인터페이스. 코어는 인터페이스만 안다.
236
+ * - web: localStorage 구현 제공
237
+ * - native: AsyncStorage 구현 제공
238
+ * 큐/게스트 식별자가 이 저장소에 의존한다 → 저장소를 바꿔도 동작이 동일(TC4).
239
+ */
240
+ interface FeedbackStorage {
241
+ get(key: string): Promise<string | null>;
242
+ set(key: string, value: string): Promise<void>;
243
+ remove(key: string): Promise<void>;
244
+ }
245
+ /** 제보를 수집 백엔드로 보내는 어댑터. 호스트(또는 SDK)가 구현. */
246
+ interface FeedbackAdapter {
247
+ submit(report: FeedbackReport): Promise<SubmitResult>;
248
+ }
249
+ /** 제출 시점에 호출돼 사용자를 반환. 동기/비동기 모두 허용. */
250
+ type GetUserFn = () => FeedbackUser | Promise<FeedbackUser>;
251
+ /** 현재 화면(라우트/스크린 이름)을 반환. 없으면 null. */
252
+ type GetCurrentScreenFn = () => string | null;
253
+ /**
254
+ * 진단 버퍼가 요청을 캡처할지 결정하는 매처.
255
+ * true 반환 = 이 요청은 캡처에서 제외(예: 위젯 자신의 전송 요청).
256
+ */
257
+ interface DiagRequestRef {
258
+ url: string;
259
+ method: string;
260
+ }
261
+ type DiagnosticsExcludeMatcher = (req: DiagRequestRef) => boolean;
262
+
263
+ /**
264
+ * 웹 공급자가 돌려주는 원자료.
265
+ * 모든 칸이 선택이다 — 못 구한 값은 빼면 되고, 코어가 null 로 채운다.
266
+ */
267
+ interface WebContextInput {
268
+ viewport?: {
269
+ width?: number | null;
270
+ height?: number | null;
271
+ devicePixelRatio?: number | null;
272
+ };
273
+ userAgent?: string | null;
274
+ }
275
+ /** 앱/웹 컨텍스트를 채우는 공급자 묶음. 전부 선택이다. */
276
+ interface ContextProviders {
277
+ /** 현재 URL(웹) 또는 딥링크(앱). */
278
+ getUrl?: () => string | null;
279
+ /** 라우트 스택. 바깥(루트)부터 안쪽(현재 화면) 순서. */
280
+ getNavPath?: () => string[] | null;
281
+ /** 현재 화면 파라미터 원본. JSON 왕복으로 정제해서 싣는다. */
282
+ getRouteParams?: () => unknown;
283
+ /** 앱 빌드 정보(version/channel/updateId/runtimeVersion). */
284
+ getAppInfo?: () => Partial<AppInfo> | null;
285
+ /** 기기 정보(model/osName/osVersion/deviceType). */
286
+ getDevice?: () => Partial<DeviceInfo> | null;
287
+ /** 화면 크기·배율(width/height/pixelRatio/fontScale). */
288
+ getDisplay?: () => Partial<DisplayInfo> | null;
289
+ /** 웹 뷰포트·userAgent. */
290
+ getWebContext?: () => WebContextInput | null;
291
+ /**
292
+ * 진단 스냅샷 공급자.
293
+ * 주지 않으면 `diagnostics` 는 **빈 배열이 아니라 null** 이다
294
+ * (수집이 배선되지 않았다는 뜻과 "그 세션에 아무 일도 없었다"는 뜻을 구분한다).
295
+ */
296
+ getDiagnostics?: () => DiagnosticsPayload | null;
297
+ /** 화면 이름 → 소스 파일 경로. 빌드 플러그인이 생성한다. 없으면 screenPath: null. */
298
+ screenSourceMap?: Record<string, string> | null;
299
+ /** 타임존 결정(테스트 주입용). 없으면 Intl 로 알아낸다. */
300
+ getTimezone?: () => string | null;
301
+ }
302
+ interface BuildContextOpts extends ContextProviders {
303
+ app: string;
304
+ sessionId: string;
305
+ storage: FeedbackStorage;
306
+ platform: FeedbackContext["platform"];
307
+ /** 제출 시점 사용자 결정. 없으면 게스트. */
308
+ getUser?: GetUserFn;
309
+ /** 현재 화면 이름. 없거나 null 반환 → screen: null. */
310
+ getCurrentScreen?: GetCurrentScreenFn;
311
+ /** 빌드 플러그인 매핑. 없으면 screenId/sourceFile 이 null. */
312
+ source?: SourceMapping;
313
+ /** 자유 메타. 기본 {}. */
314
+ extra?: Record<string, unknown>;
315
+ /** 시간 주입(테스트용). */
316
+ now?: () => number;
317
+ iso?: (n: number) => string;
318
+ }
319
+ /**
320
+ * getUser prop을 **제출 시점에** 호출해 사용자를 결정한다(캐싱하지 않는다).
321
+ * - getUser 없음 → 게스트(기기에 영구 저장된 id).
322
+ * - getUser 는 있지만 빈 결과/예외 → 게스트 폴백(제보 자체는 잃지 않는다).
323
+ * - getUser 결과에 id 있음 → 그대로 사용.
324
+ *
325
+ * 게스트도 `isGuest: true` 로 **객체를 채운다**. 반환이 null 인 경우는 저장소 접근이
326
+ * 실패해 게스트 식별자조차 만들지 못한 최초 실행 순간뿐이다.
327
+ */
328
+ declare function resolveContextUser(getUser: GetUserFn | undefined, storage: FeedbackStorage): Promise<ContextUser | null>;
329
+ /** 호스트가 준 FeedbackUser 를 제보에 실리는 ContextUser 로 정규화한다. */
330
+ declare function normalizeUser(u: FeedbackUser): ContextUser;
331
+ /** FeedbackReport.context 를 조립한다. 사용자·화면·기기·진단 수집을 모두 포함. */
332
+ declare function buildContext(opts: BuildContextOpts): Promise<FeedbackContext>;
333
+
334
+ /** 호스트가 submit() 호출 시 넘기는 부분 데이터. 계약의 나머지는 코어가 채운다. */
335
+ interface ReportParts {
336
+ kind: FeedbackKind;
337
+ comment: string;
338
+ priority: FeedbackPriority;
339
+ screenshot: FeedbackScreenshot | null;
340
+ pin: FeedbackPin | null;
341
+ element: ElementInfo | null;
342
+ }
343
+ /** 제보 조립 옵션. 컨텍스트 조립 옵션과 동일하다(제보 쪽에 추가 입력이 없다). */
344
+ type BuildReportOpts = BuildContextOpts;
345
+ /**
346
+ * 부분 데이터 + 옵션으로 FeedbackReport 하나를 만든다.
347
+ * clientSubmissionId(UUID)와 createdAt(ISO)은 매 호출마다 새로 생성.
348
+ */
349
+ declare function buildReport(parts: ReportParts, opts: BuildReportOpts): Promise<FeedbackReport>;
350
+
351
+ /** 대기 큐 최대 길이. 넘치면 오래된 것부터 버린다. */
352
+ declare const MAX_QUEUE_ITEMS = 50;
353
+ /** 대기 큐 최대 보관 기간(ms). 7일. */
354
+ declare const MAX_QUEUE_AGE_MS: number;
355
+ /** 백오프 시작 간격(ms). */
356
+ declare const BACKOFF_BASE_MS: number;
357
+ /** 백오프 상한(ms). 429 는 이 상한의 예외다. */
358
+ declare const BACKOFF_MAX_MS: number;
359
+ /** 429 를 받았을 때의 최소 대기(ms). 상한 5분을 넘겨 잡는다. */
360
+ declare const RATE_LIMIT_MIN_DELAY_MS: number;
361
+ /** UI 가 구독하는 전송 상태. */
362
+ interface QueueStatus {
363
+ /** idle=대기 중 제보 없음, sending=전송 중, pending=대기 중(연결되면 자동 전송). */
364
+ state: "idle" | "sending" | "pending";
365
+ /** 대기 중인 제보 수. */
366
+ pending: number;
367
+ /** 마지막 실패 요약(사용자 노출용 아님, 개발자 진단용). 성공/초기엔 null. */
368
+ lastError: string | null;
369
+ }
370
+ /** `submit()` 한 건의 결과. */
371
+ interface SubmitOutcome {
372
+ /** 서버가 수락(2xx + ok=true)했는가. */
373
+ delivered: boolean;
374
+ /** 서버가 부여한 id(표시용). 수락 전이면 null. */
375
+ id: string | null;
376
+ /** 아직 큐에 남아 있는가(=나중에 자동 재전송). */
377
+ queued: boolean;
378
+ }
379
+ type QueueStatusListener = (status: QueueStatus) => void;
380
+ interface FeedbackQueueOpts {
381
+ storage: FeedbackStorage;
382
+ adapter: FeedbackAdapter;
383
+ /** 주기적 flush 간격(ms). 기본 30000. 0/음수면 주기 flush 를 끈다. */
384
+ flushIntervalMs?: number;
385
+ /** 시각 주입(테스트용). */
386
+ now?: () => number;
387
+ /** 백오프 계산 주입(테스트용). */
388
+ backoff?: (attempts: number, retryAfterMs: number | null, rateLimited: boolean) => number;
389
+ /**
390
+ * 개발자 콘솔 경고 싱크. 기본은 `console.warn`.
391
+ * 진단 버퍼가 콘솔을 패치한 환경에서는 원본 콘솔(`DiagnosticsCollector.originalConsole.warn`)을
392
+ * 넘겨 위젯 자신의 경고가 다시 수집되는 되먹임을 막는다.
393
+ */
394
+ warn?: (...a: unknown[]) => void;
395
+ }
396
+ /**
397
+ * 지수 백오프 계산.
398
+ * - 429(rateLimited)면 상한 예외: `max(10분, 서버 권장)`.
399
+ * - 서버가 `retryAfterMs` 를 줬으면 그것을 쓰되 5분으로 자른다.
400
+ * - 아니면 5초 * 2^(attempts-1), 상한 5분.
401
+ *
402
+ * attempts=10 이면 5s*2^9 = 2560s 지만 상한에 걸려 정확히 5분이다(연속 실패해도 간격이
403
+ * 5분을 넘지 않는다).
404
+ */
405
+ declare function defaultBackoff(attempts: number, retryAfterMs: number | null, rateLimited?: boolean): number;
406
+ declare class FeedbackQueue {
407
+ private storage;
408
+ private adapter;
409
+ private flushIntervalMs;
410
+ private now;
411
+ private backoff;
412
+ private warn;
413
+ /**
414
+ * 저장소 접근 직렬화 체인.
415
+ *
416
+ * 왜 필요한가: `load → 수정 → save` 사이에 다른 경로가 끼어들면 그 사이 쓰기가 통째로
417
+ * 사라진다(flush 가 [A]를 읽는 동안 enqueue 가 [A,B]를 저장 → flush 가 []를 저장 →
418
+ * B 유실). 단순한 `flushing` 불리언은 재진입을 "무시"할 뿐이라 이 경합을 못 막고,
419
+ * 무시된 flush 를 기다리던 `submit()` 이 자기 전송을 await 할 수도 없다.
420
+ */
421
+ private chain;
422
+ /** 아직 시작하지 않은 flush 요청(있으면 재사용해서 타이머 연타를 합친다). */
423
+ private queuedFlush;
424
+ private timer;
425
+ private onlineHandler;
426
+ private pending;
427
+ private sending;
428
+ private lastError;
429
+ private listeners;
430
+ /** 제출 단위 결과 캐시. `submit()` 이 자기 건의 결말을 알아내는 데 쓴다. */
431
+ private outcomes;
432
+ constructor(opts: FeedbackQueueOpts);
433
+ /**
434
+ * 제보를 큐에 넣고 전송을 시도한 뒤, 그 한 건의 결말을 돌려준다.
435
+ * 실패해도 예외를 던지지 않는다 — 제보는 큐에 남고 `queued: true` 로 알린다.
436
+ */
437
+ submit(report: FeedbackReport): Promise<SubmitOutcome>;
438
+ /** 큐에 제보를 추가하고, 전송을 백그라운드로 시도한다. */
439
+ enqueue(report: FeedbackReport): Promise<void>;
440
+ /**
441
+ * 만기된 큐 항목들을 한 건씩 순서대로 전송한다.
442
+ * 이미 진행 중이면 그 뒤에 한 번만 예약해 붙는다(타이머 연타로 flush 가 쌓이지 않는다).
443
+ */
444
+ flush(): Promise<void>;
445
+ /**
446
+ * 지금 즉시 재시도한다. 네트워크 복귀·앱 포그라운드 복귀·[다시 보내기] 용.
447
+ * 백오프 대기를 앞당기되, **429 로 묶인 항목은 앞당기지 않는다**(서버가 명시한 한도라
448
+ * 무시하면 다시 429 만 받는다).
449
+ */
450
+ retryNow(): Promise<void>;
451
+ /** 현재 큐 길이(대기 중 제보 수). 검증/진단용. */
452
+ size(): Promise<number>;
453
+ /** 현재 전송 상태 스냅샷. */
454
+ getStatus(): QueueStatus;
455
+ /** 특정 clientSubmissionId의 마지막 확정 결말. 큐에 있거나 아직 시도 전이면 null이다. */
456
+ getOutcome(clientSubmissionId: string): Pick<SubmitOutcome, "delivered" | "id"> | null;
457
+ /** 전송 상태 구독. 반환값을 호출하면 해지된다. 구독 즉시 현재 상태를 1회 통지한다. */
458
+ subscribe(listener: QueueStatusListener): () => void;
459
+ /**
460
+ * 마운트 시 호출. 만기 항목 정리 + 즉시 재시도 + 주기 flush + `online` 재시도를 건다.
461
+ * 앱(RN)에는 `online` 이벤트가 없으므로, 호스트가 포그라운드 복귀 시 `retryNow()` 를
462
+ * 직접 부른다(코어는 플랫폼 API 를 모른다).
463
+ */
464
+ start(): void;
465
+ /** 타이머와 이벤트 구독을 해제한다. 큐 내용은 그대로 남는다. */
466
+ stop(): void;
467
+ private enqueueInner;
468
+ private doFlush;
469
+ /** 어댑터 호출. 어댑터가 예외를 던져도 결과값으로 흡수한다(전송 보장 우선). */
470
+ private submitOne;
471
+ private load;
472
+ /**
473
+ * 큐를 저장한다. 저장소 한도(쿼터)에 걸리면 **스크린샷부터** 떼어내고, 그래도 안 되면
474
+ * 가장 오래된 제보를 버린다. 사용자가 쓴 글이 스크린샷보다 우선이다.
475
+ * 인자로 받은 배열을 제자리에서 수정한다(호출부가 들고 있는 항목 참조는 유지된다).
476
+ */
477
+ private persist;
478
+ /** 7일이 지난 항목을 걸러낸다. 만기 항목은 전송을 시도하지 않는다. */
479
+ private purgeExpired;
480
+ private recordOutcome;
481
+ private setPending;
482
+ private emit;
483
+ private serialize;
484
+ }
485
+
486
+ /** `fetch` keepalive 요청 본문 한도(바이트). Fetch 표준의 inflight keepalive quota. */
487
+ declare const KEEPALIVE_BODY_LIMIT_BYTES: number;
488
+ /**
489
+ * 이 본문을 keepalive 로 보내도 되는지.
490
+ * @param bodyByteLength 직렬화된 요청 본문의 바이트 수.
491
+ */
492
+ declare function canUseKeepalive(bodyByteLength: number): boolean;
493
+ /**
494
+ * 문자열 본문의 UTF-8 바이트 수. `TextEncoder` 가 없으면(구형 RN 등) 보수적으로
495
+ * 근사한다 — 근사는 항상 실제보다 크거나 같게 잡아 한도를 넘겨 켜는 일이 없게 한다.
496
+ */
497
+ declare function byteLengthOf(body: string): number;
498
+
499
+ declare class RingBuffer<T> {
500
+ /** 보관 상한. 0 이하/비정상 값이면 0(아무것도 담지 않음)으로 떨어진다. */
501
+ readonly capacity: number;
502
+ private items;
503
+ constructor(capacity: number);
504
+ push(item: T): void;
505
+ /** 현재 내용의 복사본. 호출자가 들고 있어도 이후 push 에 영향받지 않는다. */
506
+ toArray(): T[];
507
+ get size(): number;
508
+ clear(): void;
509
+ }
510
+
511
+ /** 네트워크 링버퍼 용량. 넘치면 오래된 것부터 밀려난다. */
512
+ declare const NETWORK_BUFFER_LIMIT = 30;
513
+ /** 콘솔 로그(warn/error) 링버퍼 용량. 넘치면 오래된 것부터 밀려난다. */
514
+ declare const LOG_BUFFER_LIMIT = 50;
515
+ /** 로그 한 건의 최대 길이. 스택 덤프 하나가 버퍼를 통째로 먹는 걸 막는다. */
516
+ declare const MAX_LOG_MESSAGE_CHARS = 2000;
517
+ interface DiagnosticsInstallOpts {
518
+ /** true 를 반환하면 그 요청은 기록하지 않는다(위젯 자신의 전송 등). */
519
+ excludeMatcher?: DiagnosticsExcludeMatcher;
520
+ /** 시각 주입(테스트용). */
521
+ now?: () => number;
522
+ iso?: (n: number) => string;
523
+ }
524
+ /**
525
+ * 진단 수집기. `install()` 로 글로벌을 패치하고 `uninstall()` 로 원복한다.
526
+ * 생성 자체는 부작용이 없다 — `install()` 을 호출해야 패치가 일어난다.
527
+ */
528
+ declare class DiagnosticsCollector {
529
+ readonly network: RingBuffer<DiagNetworkEntry>;
530
+ readonly logs: RingBuffer<DiagLogEntry>;
531
+ private installed;
532
+ private originalFetch;
533
+ private originalXhrOpen;
534
+ private originalXhrSend;
535
+ private originalWarn;
536
+ private originalError;
537
+ private excludeMatcher;
538
+ private now;
539
+ private iso;
540
+ /**
541
+ * 패치된 fetch 안에서 동기적으로 XHR 이 열리는 깊이.
542
+ *
543
+ * React Native 의 `fetch` 는 whatwg-fetch 폴리필이라 내부에서 XMLHttpRequest 를 쓴다.
544
+ * 둘 다 패치해두면 요청 1건이 2건으로 기록된다. fetch 폴리필은 Promise 실행자 안에서
545
+ * **동기적으로** `send()` 를 부르므로, 그 구간을 세어 XHR 쪽 기록만 건너뛴다.
546
+ */
547
+ private fetchDepth;
548
+ constructor(opts?: {
549
+ now?: () => number;
550
+ iso?: (n: number) => string;
551
+ networkLimit?: number;
552
+ logLimit?: number;
553
+ });
554
+ /** 위젯이 자신의 로그를 캡처 없이 찍을 때 쓰는 원본 console. */
555
+ readonly originalConsole: {
556
+ warn: (...a: unknown[]) => void;
557
+ error: (...a: unknown[]) => void;
558
+ log: (...a: unknown[]) => void;
559
+ };
560
+ /** 글로벌 fetch/XHR/console 을 패치한다. 이미 설치했으면 no-op. */
561
+ install(opts?: DiagnosticsInstallOpts): void;
562
+ /** 글로벌 패치를 모두 원복한다. 버퍼 내용은 그대로 남는다. */
563
+ uninstall(): void;
564
+ /**
565
+ * 지금까지 모인 것을 그대로 스냅샷으로 낸다.
566
+ * 반환 배열은 사본이라 이후 수집이 제보 페이로드를 바꾸지 않는다.
567
+ */
568
+ snapshot(): DiagnosticsPayload;
569
+ /** 두 버퍼를 비운다. */
570
+ clear(): void;
571
+ private recordNetwork;
572
+ private recordLog;
573
+ private isExcluded;
574
+ private patchFetch;
575
+ private patchXhr;
576
+ private patchConsole;
577
+ }
578
+ /**
579
+ * 모듈 스코프 공유 수집기.
580
+ *
581
+ * 화면·컴포넌트마다 수집기를 연결할 필요가 없어야 한다는 요구사항 때문에 여기 하나를 둔다.
582
+ * 위젯이 어느 화면에서 열리든 이 인스턴스의 스냅샷을 싣는다.
583
+ */
584
+ declare const sharedDiagnostics: DiagnosticsCollector;
585
+ /** 가려진 값 표시. 파라미터가 "있었다"는 사실은 남기고 값만 지운다. */
586
+ declare const REDACTED = "REDACTED";
587
+ /**
588
+ * URL 에서 자격증명을 걷어낸다.
589
+ * - `#fragment` 는 통째로 버린다(진단 가치는 없고 토큰이 실리는 경우가 있다).
590
+ * - `scheme://user:pass@host` 의 userinfo 를 지운다.
591
+ * - 자격증명형 쿼리 파라미터의 **값만** `REDACTED` 로 바꾼다.
592
+ */
593
+ declare function sanitizeUrl(raw: string): string;
594
+
595
+ /** 노출 판정에 쓰이는 현재 상황. 함수형 `visibility` 가 그대로 받는다. */
596
+ interface VisibilityEnv {
597
+ /** 현재 사용자. 로그인 전이면 게스트이거나 null. */
598
+ user: ContextUser | FeedbackUser | null;
599
+ /** 개발 빌드 여부. */
600
+ isDev: boolean;
601
+ platform: "web" | "native" | "unknown";
602
+ }
603
+ /** 프로젝트가 직접 판정하는 형태. true 면 노출. */
604
+ type VisibilityFn = (env: VisibilityEnv) => boolean;
605
+ /**
606
+ * 누구에게 위젯을 노출할지.
607
+ *
608
+ * 기본값은 `all` 이다. 실사용자 제보가 실제로 가치를 냈기 때문에 패키지가 먼저 막지
609
+ * 않는다. 좁혀야 하는 프로젝트가 `internal`·`dev-only`·함수로 좁힌다.
610
+ */
611
+ type Visibility = "all" | "internal" | "dev-only" | VisibilityFn;
612
+ /** `internal` 판정의 기본 근거. 프로젝트가 `internalRoles` 로 갈아끼운다. */
613
+ declare const DEFAULT_INTERNAL_ROLES: readonly string[];
614
+ type WidgetCorner = "bottom-right" | "bottom-left" | "top-right" | "top-left";
615
+ interface WidgetPosition {
616
+ corner: WidgetCorner;
617
+ /** 모서리로부터의 가로 여백(px). */
618
+ offsetX: number;
619
+ /** 모서리로부터의 세로 여백(px). */
620
+ offsetY: number;
621
+ }
622
+ declare const DEFAULT_POSITION: WidgetPosition;
623
+ /** 내장 어댑터 이름. 실제 구현과 전송 규약은 어댑터 섹션이 갖는다. */
624
+ type AdapterName = "lasso" | "linear" | "notion";
625
+ declare const DEFAULT_ADAPTER: AdapterName;
626
+ /**
627
+ * 수집 엔드포인트 기본값은 **어댑터가 갖는다.**
628
+ *
629
+ * 코어가 특정 백엔드 URL 을 상수로 들고 있으면 어댑터 교체가 무의미해지고, 코어가
630
+ * 어댑터별 사정을 아는 셈이 된다. 그래서 여기서는 "지정 안 함"을 `null` 로 표현하고,
631
+ * 어댑터가 자기 내장 기본값을 쓰도록 넘긴다.
632
+ */
633
+ declare const ENDPOINT_FROM_ADAPTER: null;
634
+ type ConfigWarningCode =
635
+ /** 토큰이 없다 — 전송은 되지만 미귀속이다. */
636
+ "missing-token"
637
+ /** 환경변수가 치환되지 않은 채로 들어왔다(빌드 설정 사고). */
638
+ | "unsubstituted-token"
639
+ /** 모르는 어댑터 이름 — 기본 어댑터로 되돌린다. */
640
+ | "unknown-adapter"
641
+ /** 모르는 visibility 값 — `all` 로 되돌린다. */
642
+ | "unknown-visibility"
643
+ /** 모르는 위치 값 — 우하단으로 되돌린다. */
644
+ | "unknown-position"
645
+ /** 사용자가 준 판정 함수가 예외를 던졌다 — 숨김으로 처리한다. */
646
+ | "visibility-threw";
647
+ interface ConfigWarning {
648
+ code: ConfigWarningCode;
649
+ message: string;
650
+ }
651
+ interface FeedbackConfig {
652
+ /** 수집 토큰. 없으면 미귀속으로 전송된다(막지 않는다). */
653
+ token?: string | null;
654
+ /** 수집 엔드포인트. 없으면 어댑터 내장 기본값. */
655
+ endpoint?: string | null;
656
+ /** 어댑터 이름 또는 어댑터 구현체. */
657
+ adapter?: AdapterName | FeedbackAdapter;
658
+ /** 노출 범위. 기본 `all`. */
659
+ visibility?: Visibility;
660
+ /** 모달을 열 때 화면을 자동 캡처할지. */
661
+ captureScreenshot?: boolean;
662
+ /** network·logs 링버퍼를 수집할지. false 면 아예 설치하지 않는다. */
663
+ captureDiagnostics?: boolean;
664
+ /** 플로팅 버튼 위치. 모서리만 주면 여백은 기본값. */
665
+ position?: WidgetCorner | Partial<WidgetPosition>;
666
+ /** `internal` 판정을 프로젝트가 직접 할 때. 주면 `internalRoles` 보다 우선한다. */
667
+ isInternal?: VisibilityFn;
668
+ /** `internal` 로 볼 역할 목록. 기본 {@link DEFAULT_INTERNAL_ROLES}. */
669
+ internalRoles?: readonly string[];
670
+ /** 개발 빌드 여부. 안 주면 런타임 전역에서 추론한다. */
671
+ isDev?: boolean;
672
+ /** 경고 출구. 안 주면 `resolveConfig` 결과의 `warnings` 로만 남는다. */
673
+ onWarn?: (warning: ConfigWarning) => void;
674
+ }
675
+ /** 기본값이 모두 채워진 설정. 위젯은 이것만 본다. */
676
+ interface ResolvedConfig {
677
+ token: string | null;
678
+ endpoint: string | null;
679
+ adapter: AdapterName | FeedbackAdapter;
680
+ visibility: Visibility;
681
+ captureScreenshot: boolean;
682
+ captureDiagnostics: boolean;
683
+ position: WidgetPosition;
684
+ isInternal: VisibilityFn | null;
685
+ internalRoles: readonly string[];
686
+ isDev: boolean;
687
+ warnings: ConfigWarning[];
688
+ }
689
+ /**
690
+ * 개발 빌드 여부 추론.
691
+ *
692
+ * 코어는 플랫폼 무관이라 `__DEV__`(RN)·`process.env.NODE_ENV`(웹/노드)를 **globalThis
693
+ * 를 통해서만** 본다. 어느 쪽도 없으면 프로덕션으로 본다 — 모르는 상태에서 개발 빌드로
694
+ * 단정하면 `dev-only` 가 실사용자에게 새기 때문이다.
695
+ */
696
+ declare function detectDevBuild(): boolean;
697
+ /**
698
+ * 부분 설정을 기본값으로 채운다.
699
+ *
700
+ * **던지지 않는다.** 설정이 틀렸다고 위젯이 호스트 앱을 깨거나 사용자 화면에 오류를
701
+ * 띄우면, 붙이는 비용이 한 줄이라는 전제가 무너진다. 모르는 값은 기본값으로 되돌리고
702
+ * 경고만 남긴다.
703
+ */
704
+ declare function resolveConfig(config?: FeedbackConfig): ResolvedConfig;
705
+ declare function isInternalUser(resolved: ResolvedConfig, env: VisibilityEnv): boolean;
706
+ /**
707
+ * 지금 이 사용자에게 플로팅 버튼을 보여줄지.
708
+ *
709
+ * 판정에 실패하면(사용자가 준 함수가 던지면) **숨긴다.** 노출 범위를 좁힌 프로젝트가
710
+ * 있다는 건 보이면 안 되는 이유가 있다는 뜻이므로, 애매할 때 새는 쪽보다 안 보이는
711
+ * 쪽이 안전하다.
712
+ */
713
+ declare function shouldShowWidget(resolved: ResolvedConfig, input?: Partial<VisibilityEnv>): boolean;
714
+ /** `DiagnosticsCollector` 중 설정 배선에 필요한 최소 형태. */
715
+ interface DiagnosticsSource {
716
+ install(): void;
717
+ snapshot(): DiagnosticsPayload;
718
+ }
719
+ /**
720
+ * `captureDiagnostics` 를 실제 배선으로 옮긴다.
721
+ *
722
+ * 꺼져 있으면 **공급자를 만들지 않고 수집기도 설치하지 않는다.** 빈 배열을 상수로
723
+ * 돌려주는 공급자를 만들면 "수집했는데 아무 일도 없었다"와 "애초에 수집하지 않았다"가
724
+ * 구분되지 않는다. 공급자가 없으면 `buildContext` 가 `diagnostics: null` 을 쓴다.
725
+ */
726
+ declare function diagnosticsProviderFor(resolved: Pick<ResolvedConfig, "captureDiagnostics">, collector: DiagnosticsSource): (() => DiagnosticsPayload) | undefined;
727
+
728
+ /** 빌드 플러그인이 심는 속성 이름. `data-` 라서 DOM 이 그대로 통과시킨다. */
729
+ declare const SOURCE_ATTR = "data-fk-source";
730
+ /** 해석된 소스 위치. */
731
+ interface SourceLocation {
732
+ /** 원본 파일 경로(번들 경로가 아니다). */
733
+ file: string;
734
+ /** 1부터 시작하는 줄 번호. */
735
+ line: number;
736
+ /** 1부터 시작하는 칼럼. 없으면 null. */
737
+ column: number | null;
738
+ }
739
+ /**
740
+ * `"src/pages/Home.tsx:42:7"` 을 해석한다.
741
+ *
742
+ * 뒤에서부터 숫자 꼬리를 떼어낸다 — 파일 경로 자체에 콜론이 들어갈 수 있어서
743
+ * (윈도우 드라이브 문자 `C:\`) 앞에서부터 자르면 깨진다.
744
+ */
745
+ declare function parseSourceAttr(value: unknown): SourceLocation | null;
746
+ /**
747
+ * 지목된 요소에서 소스 매핑을 뽑는다.
748
+ *
749
+ * `base` 는 화면 단위 매핑(빌드 플러그인의 screen-map)이다. 요소에서 얻은 파일 경로가
750
+ * 더 구체적이므로 그걸 우선하고, 화면 식별자는 base 것을 유지한다.
751
+ */
752
+ declare function sourceFromElement(element: Pick<ElementInfo, "attributes"> | null | undefined, base?: SourceMapping | null): SourceMapping;
753
+
754
+ /** 계약상 스크린샷 상한. base64 문자열 기준 8 MiB. */
755
+ declare const SCREENSHOT_MAX_BASE64_BYTES: number;
756
+ /** 캡처가 불가능할 때 사용자에게 보여줄 문구. */
757
+ declare const SCREENSHOT_FAILED_MESSAGE = "\uCEA1\uCC98 \uC2E4\uD328 \u2014 \uD30C\uC77C\uB85C \uCCA8\uBD80\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4";
758
+ /** 상한을 넘겼을 때 차례로 시도할 재인코딩 품질. 앞에서부터 시도하고 처음 통과하는 값을 쓴다. */
759
+ declare const SCREENSHOT_QUALITY_STEPS: readonly number[];
760
+ /**
761
+ * base64 페이로드의 바이트 수.
762
+ * base64 는 ASCII 라 문자 수가 곧 바이트 수다(디코딩 후 원본 크기가 아니라 전송량 기준).
763
+ */
764
+ declare function screenshotBytes(shot: FeedbackScreenshot | null | undefined): number;
765
+ type ScreenshotCapture = () => FeedbackScreenshot | null | Promise<FeedbackScreenshot | null>;
766
+ type ScreenshotReencode = (shot: FeedbackScreenshot, quality: number) => FeedbackScreenshot | null | Promise<FeedbackScreenshot | null>;
767
+ interface CaptureWithinLimitOpts {
768
+ capture: ScreenshotCapture;
769
+ /** 상한 초과 시 다시 인코딩하는 훅. 없으면 재인코딩 없이 바로 포기한다. */
770
+ reencode?: ScreenshotReencode | null;
771
+ qualitySteps?: readonly number[];
772
+ limitBytes?: number;
773
+ }
774
+ interface ScreenshotOutcome {
775
+ screenshot: FeedbackScreenshot | null;
776
+ /** 캡처를 끝내 얻지 못했다. 제출은 계속 가능해야 한다. */
777
+ failed: boolean;
778
+ /** 실패했을 때만 채워지는 안내 문구. */
779
+ message: string | null;
780
+ /** 품질을 낮춰 다시 인코딩한 결과인가. */
781
+ reencoded: boolean;
782
+ }
783
+ /**
784
+ * 캡처한 뒤 용량 상한 안으로 들여보낸다.
785
+ *
786
+ * 1. 캡처 → 실패하거나 빈 결과면 곧장 실패로 내린다.
787
+ * 2. 상한 이내면 그대로 쓴다.
788
+ * 3. 초과하면 품질을 단계별로 낮춰 재인코딩하고, 처음 통과한 결과를 쓴다.
789
+ * 4. 끝내 못 맞추면 스크린샷을 버리고 실패 문구로 대체한다(제출은 막지 않는다).
790
+ */
791
+ declare function captureWithinLimit(opts: CaptureWithinLimitOpts): Promise<ScreenshotOutcome>;
792
+
793
+ interface PixelPoint {
794
+ x: number;
795
+ y: number;
796
+ }
797
+ interface PixelSize {
798
+ width: number;
799
+ height: number;
800
+ }
801
+ /**
802
+ * 픽셀 좌표를 스크린샷 기준 0~1 상대 좌표로 바꾼다.
803
+ * 크기가 0 이하이면(아직 레이아웃 전) 나눗셈이 무의미하므로 0 으로 떨어뜨린다.
804
+ */
805
+ declare function normalizePin(point: PixelPoint, size: PixelSize): FeedbackPin;
806
+ /** 상대 좌표를 다시 픽셀로 되돌린다(마커를 그릴 때 쓴다). */
807
+ declare function denormalizePin(pin: FeedbackPin, size: PixelSize): PixelPoint;
808
+ /**
809
+ * 핀 지정 화면의 상태기계.
810
+ *
811
+ * `draft` 는 지정 화면에서 만지는 중인 값, `confirmed` 는 모달로 돌아간 확정값이다.
812
+ * 둘을 나눠 둔 이유는 취소했을 때 이전 확정값이 살아남아야 하기 때문이다.
813
+ */
814
+ declare class PinController {
815
+ private confirmedPin;
816
+ private draftPin;
817
+ private opened;
818
+ constructor(initial?: FeedbackPin | null);
819
+ get isOpen(): boolean;
820
+ get draft(): FeedbackPin | null;
821
+ get confirmed(): FeedbackPin | null;
822
+ /** 화면에 떠 있는 핀 개수. 단일 핀 규칙이라 0 아니면 1이다. */
823
+ get count(): number;
824
+ open(): void;
825
+ /** 탭한 자리에 핀을 찍는다. 기존 핀이 있으면 그 자리로 옮긴다(추가가 아니다). */
826
+ place(point: PixelPoint, size: PixelSize): FeedbackPin;
827
+ /** 이미 상대 좌표를 갖고 있을 때 쓰는 경로. */
828
+ placeRelative(pin: FeedbackPin): FeedbackPin;
829
+ confirm(): FeedbackPin | null;
830
+ /** 취소 — 이전에 확정한 좌표로 되돌린다. */
831
+ cancel(): FeedbackPin | null;
832
+ reset(): void;
833
+ }
834
+
835
+ /** 위젯 플로팅 버튼의 고정 id. 모달을 닫으면 여기로 포커스를 되돌린다. */
836
+ declare const FLOATING_BUTTON_ID = "feedback-kit-floating-button";
837
+ /**
838
+ * 순환하는 포커스 링. 끝에서 Tab 하면 처음으로 돌아온다 = 포커스가 모달 밖으로 새지 않는다.
839
+ */
840
+ declare class FocusRing {
841
+ private items;
842
+ private index;
843
+ constructor(items?: readonly string[]);
844
+ get order(): readonly string[];
845
+ get current(): string | null;
846
+ /**
847
+ * 항목 목록을 바꾼다. 지금 포커스된 항목이 새 목록에도 있으면 그 자리를 유지한다
848
+ * (스크린샷을 지웠다고 포커스가 처음으로 튀면 키보드 사용자가 길을 잃는다).
849
+ */
850
+ setItems(items: readonly string[]): void;
851
+ focus(id: string): boolean;
852
+ next(): string | null;
853
+ prev(): string | null;
854
+ contains(id: string): boolean;
855
+ }
856
+
857
+ /** 코멘트 상한. 넘으면 자르지 않고 거부한다(사용자가 쓴 글을 임의로 삭제하지 않는다). */
858
+ declare const COMMENT_MAX_CHARS = 4000;
859
+ declare const COMMENT_REQUIRED_MESSAGE = "\uC758\uACAC\uC744 \uC785\uB825\uD574\uC8FC\uC138\uC694";
860
+ declare const COMMENT_TOO_LONG_MESSAGE = "4,000\uC790 \uC774\uD558\uB85C \uC785\uB825\uD574\uC8FC\uC138\uC694";
861
+ declare const SUBMIT_DONE_MESSAGE = "\uBCF4\uB0C8\uC2B5\uB2C8\uB2E4";
862
+ declare const SUBMIT_PENDING_MESSAGE = "\uB300\uAE30 \uC911";
863
+ declare const SUBMIT_FAILED_MESSAGE = "\uBCF4\uB0B4\uC9C0 \uBABB\uD588\uC2B5\uB2C8\uB2E4";
864
+ type WidgetPlatform = "web" | "native";
865
+ type ScreenshotStatus = "none" | "capturing" | "ready" | "failed";
866
+ type ModalSubmitStatus = "idle" | "sending" | "sent" | "pending" | "failed";
867
+ /** 모달 안에서 포커스를 받는 요소들의 고정 id. 렌더러가 그대로 매단다. */
868
+ declare const MODAL_FIELD_COMMENT = "comment";
869
+ declare const MODAL_FIELD_PRIORITY = "priority";
870
+ declare const MODAL_ACTION_ATTACH = "attach";
871
+ declare const MODAL_ACTION_REMOVE_SCREENSHOT = "remove-screenshot";
872
+ declare const MODAL_ACTION_PIN = "pin";
873
+ declare const MODAL_ACTION_PICK = "pick";
874
+ declare const MODAL_ACTION_RETRY = "retry";
875
+ declare const MODAL_ACTION_CANCEL = "cancel";
876
+ declare const MODAL_ACTION_SEND = "send";
877
+ interface ReportModalState {
878
+ open: boolean;
879
+ comment: string;
880
+ priority: FeedbackPriority;
881
+ screenshot: FeedbackScreenshot | null;
882
+ screenshotStatus: ScreenshotStatus;
883
+ screenshotMessage: string | null;
884
+ pin: FeedbackPin | null;
885
+ submitStatus: ModalSubmitStatus;
886
+ submitMessage: string | null;
887
+ commentError: string | null;
888
+ canSubmit: boolean;
889
+ /** 핀 찍기를 열 수 있는가. 스크린샷이 있어야 좌표가 의미를 갖는다. */
890
+ canPin: boolean;
891
+ showRetry: boolean;
892
+ /** 대기 큐에 남아 있는 건수. */
893
+ pending: number;
894
+ closeConfirmVisible: boolean;
895
+ /** 이 플랫폼에서 노출되는 부가 동작들. 웹은 요소 지목, 앱은 핀 찍기. */
896
+ actions: readonly string[];
897
+ focusOrder: readonly string[];
898
+ focused: string | null;
899
+ /** 모달이 닫힌 뒤 포커스를 돌려줄 곳. */
900
+ restoreFocusTo: string | null;
901
+ }
902
+ /** 모달이 큐에게 요구하는 최소 계약. `FeedbackQueue` 가 그대로 만족한다. */
903
+ interface ModalQueueLike {
904
+ submit(report: FeedbackReport): Promise<SubmitOutcome>;
905
+ retryNow(): Promise<void>;
906
+ size(): Promise<number>;
907
+ subscribe(listener: (status: QueueStatus) => void): () => void;
908
+ /** 현재 모달 제보만 다른 대기 항목과 구분하기 위한 제출 단위 결말. */
909
+ getOutcome?(clientSubmissionId: string): Pick<SubmitOutcome, "delivered" | "id"> | null;
910
+ /** 실제 FeedbackQueue의 online/주기 재시도 lifecycle. 테스트 대역은 생략할 수 있다. */
911
+ start?(): void;
912
+ stop?(): void;
913
+ }
914
+ interface ReportModalOpts {
915
+ queue: ModalQueueLike;
916
+ /** 제보 조립. 보통 `buildReport` 를 부분 적용해 넘긴다. */
917
+ createReport: (parts: ReportParts) => Promise<FeedbackReport>;
918
+ platform: WidgetPlatform;
919
+ /** 모달을 열 때 자동 캡처. 없으면 캡처 단계를 건너뛴다. */
920
+ capture?: ScreenshotCapture | null;
921
+ reencode?: ScreenshotReencode | null;
922
+ screenshotLimitBytes?: number;
923
+ }
924
+ type ReportModalListener = (state: ReportModalState) => void;
925
+ declare class ReportModalController {
926
+ private readonly queue;
927
+ private readonly createReport;
928
+ private readonly platform;
929
+ private readonly capture;
930
+ private readonly reencode;
931
+ private readonly screenshotLimitBytes;
932
+ private readonly listeners;
933
+ private readonly ring;
934
+ private readonly unsubscribeQueue;
935
+ /** 사용자가 입력란을 건드렸는가. 열자마자 빨간 문구가 뜨는 걸 막으려고 둔다. */
936
+ private touched;
937
+ private submitAttempted;
938
+ private lastReport;
939
+ /** 이전 open/recapture가 늦게 끝나 최신 상태를 덮지 못하게 하는 세대 번호. */
940
+ private captureGeneration;
941
+ private state;
942
+ constructor(opts: ReportModalOpts);
943
+ getState(): ReportModalState;
944
+ /** 마지막으로 조립한 제보. 재시도가 같은 멱등키를 쓰는지 확인할 때 쓴다. */
945
+ getLastReport(): FeedbackReport | null;
946
+ subscribe(listener: ReportModalListener): () => void;
947
+ dispose(): void;
948
+ /** WidgetController가 소유한 큐 lifecycle을 함께 정리한다. */
949
+ stopQueue(): void;
950
+ open(): Promise<void>;
951
+ /**
952
+ * 닫기 요청. 쓰던 내용이 있으면 곧장 닫지 않고 확인을 받는다.
953
+ * @returns 실제로 닫혔으면 `"closed"`, 확인이 필요하면 `"confirm"`.
954
+ */
955
+ requestClose(): "closed" | "confirm";
956
+ /** 확인 대화에서 "닫기"를 고른 경우. */
957
+ confirmClose(): void;
958
+ /** 확인 대화에서 "계속 쓰기"를 고른 경우. 입력은 그대로 남는다. */
959
+ cancelClose(): void;
960
+ /** 확인 없이 닫는다(요소 지목 모드로 넘어갈 때처럼 흐름이 이어지는 경우). */
961
+ dismiss(): void;
962
+ private close;
963
+ /** 상한을 넘겨도 값을 자르지 않는다. 거부는 하되 사용자가 쓴 글은 보존한다. */
964
+ setComment(value: string): void;
965
+ setPriority(priority: FeedbackPriority): void;
966
+ setPin(pin: FeedbackPin | null): void;
967
+ /** 스크린샷 제거 — 핀은 스크린샷 위 좌표라 함께 사라진다. */
968
+ removeScreenshot(): void;
969
+ recapture(): Promise<void>;
970
+ /** 캡처가 실패했을 때의 대체 경로 — 사용자가 직접 고른 파일. */
971
+ attachFile(shot: FeedbackScreenshot): Promise<void>;
972
+ private runCapture;
973
+ private applyScreenshot;
974
+ tabNext(): string | null;
975
+ tabPrev(): string | null;
976
+ focus(id: string): boolean;
977
+ /**
978
+ * 제출. 실패해도 예외를 던지지 않는다 — 대기 큐에 남기고 입력을 보존한다.
979
+ * @returns 검증에 걸려 아무것도 보내지 않았으면 `null`.
980
+ */
981
+ submit(): Promise<SubmitOutcome | null>;
982
+ /** [다시 보내기]. 큐가 비면 완료로 넘어간다. */
983
+ retry(): Promise<void>;
984
+ private isCommentValid;
985
+ private isDraftLocked;
986
+ private commentErrorFor;
987
+ private actionsFor;
988
+ private focusOrderFor;
989
+ private recompute;
990
+ private patch;
991
+ private emit;
992
+ }
993
+
994
+ /** 위젯이 보여줄 수 있는 화면. */
995
+ type WidgetScreen = "button" | "modal" | "pin" | "picking";
996
+ interface WidgetControllerOpts extends ReportModalOpts {
997
+ /** 저장돼 있던 요소 지목 모드를 복원할 때 쓴다(새로고침·페이지 이동 후). */
998
+ initialPicking?: boolean;
999
+ /** 지목 모드가 켜지고 꺼질 때 호출. 호스트가 저장소에 기록한다. */
1000
+ onPickingChange?: (active: boolean) => void;
1001
+ }
1002
+ interface WidgetState {
1003
+ screen: WidgetScreen;
1004
+ modal: ReportModalState;
1005
+ /** 핀 지정 화면에서 만지는 중인 좌표. */
1006
+ pinDraft: FeedbackPin | null;
1007
+ /** 모달로 돌아간 확정 좌표. */
1008
+ pinConfirmed: FeedbackPin | null;
1009
+ pickingActive: boolean;
1010
+ }
1011
+ type WidgetListener = (state: WidgetState) => void;
1012
+ declare class WidgetController {
1013
+ readonly modal: ReportModalController;
1014
+ readonly pin: PinController;
1015
+ private readonly platform;
1016
+ private readonly onPickingChange;
1017
+ private readonly listeners;
1018
+ private readonly unsubscribeModal;
1019
+ private screen;
1020
+ private picking;
1021
+ constructor(opts: WidgetControllerOpts);
1022
+ getScreen(): WidgetScreen;
1023
+ getState(): WidgetState;
1024
+ get isPicking(): boolean;
1025
+ subscribe(listener: WidgetListener): () => void;
1026
+ dispose(): void;
1027
+ openReport(): Promise<void>;
1028
+ /** 모달 닫기 요청. 쓰던 내용이 있으면 확인부터 받는다(화면은 그대로 모달). */
1029
+ closeReport(): "closed" | "confirm";
1030
+ confirmCloseReport(): void;
1031
+ cancelCloseReport(): void;
1032
+ submitReport(): Promise<SubmitOutcome | null>;
1033
+ retryReport(): Promise<void>;
1034
+ /**
1035
+ * 핀 지정 화면 열기. 스크린샷이 없으면 좌표가 가리킬 대상이 없으므로 열지 않는다.
1036
+ * @returns 열렸으면 true.
1037
+ */
1038
+ openPinScreen(): boolean;
1039
+ /** 핀 지정 화면에서 탭. 기존 핀이 있으면 그 자리로 옮긴다. */
1040
+ placePin(point: PixelPoint, size: PixelSize): FeedbackPin | null;
1041
+ /** 확정 — 모달로 돌아가고 좌표가 제출에 실린다. */
1042
+ confirmPin(): FeedbackPin | null;
1043
+ /** 취소 — 이전 확정 좌표를 그대로 두고 모달로 돌아간다. */
1044
+ cancelPin(): FeedbackPin | null;
1045
+ /**
1046
+ * 요소 지목 모드 시작. 모달에서 넘어오는 흐름이라 닫기 확인을 묻지 않는다
1047
+ * (사용자가 이미 "지목하러 가겠다"고 밝힌 상태다).
1048
+ */
1049
+ startPicking(): boolean;
1050
+ /** [지목 종료]. 플로팅 버튼 화면으로 돌아가고 저장된 모드 표시도 지운다. */
1051
+ stopPicking(): void;
1052
+ private setPicking;
1053
+ private emit;
1054
+ }
1055
+
1056
+ /** 제목 최대 길이. 대상마다 제한이 다르지만, 한 줄로 읽히는 길이가 실질 상한이다. */
1057
+ declare const TITLE_MAX_CHARS = 80;
1058
+ /**
1059
+ * 제보 하나를 사람이 읽는 본문으로 만든다.
1060
+ *
1061
+ * 형태: `사용자 코멘트` + 빈 줄 + `접힌 진단 블록 하나`.
1062
+ * 코멘트는 **이스케이프하지 않는다** — 접힌 블록보다 앞에 있어서 우리 블록을 깨뜨릴 수 없고,
1063
+ * 사람이 쓴 문장을 우리가 변형하는 쪽이 더 나쁘다.
1064
+ */
1065
+ declare function renderReportBody(report: FeedbackReport): string;
1066
+ /**
1067
+ * 제목. 코멘트 첫 줄을 쓰되, 비어 있으면 종류와 화면으로 대신한다.
1068
+ * 제목이 비면 대상에서 목록이 읽히지 않으므로 항상 비어 있지 않은 문자열을 돌려준다.
1069
+ */
1070
+ declare function buildTitle(report: FeedbackReport): string;
1071
+
1072
+ /**
1073
+ * Linear 이슈 생성 입력의 최소 형태.
1074
+ *
1075
+ * 필드명이 라쏘런 봉투와 다르다는 게 핵심이다 — 같은 제보라도 대상마다 이름이 다르고,
1076
+ * 코어는 그 어느 쪽도 모른다.
1077
+ */
1078
+ interface LinearIssueInput {
1079
+ /** 이슈 제목. 목록에서 한 줄로 보인다. */
1080
+ title: string;
1081
+ /** 이슈 본문(마크다운). 대상이 못 받는 값은 전부 여기 접힌 블록으로 들어간다. */
1082
+ description: string;
1083
+ }
1084
+ /**
1085
+ * 제보를 Linear 이슈 입력으로 바꾼다.
1086
+ *
1087
+ * 본문은 공용 렌더러가 만든 것을 **그대로** 쓴다. 여기서 다시 조립하면
1088
+ * "Linear 로 간 제보에는 소스 경로가 없다" 같은 차이가 조용히 생긴다.
1089
+ */
1090
+ declare function buildLinearIssue(report: FeedbackReport): LinearIssueInput;
1091
+
1092
+ /** Notion 페이지 생성 입력의 최소 형태. */
1093
+ interface NotionPageInput {
1094
+ /** 페이지 제목 속성. */
1095
+ title: string;
1096
+ /**
1097
+ * 페이지 본문(마크다운 원문).
1098
+ *
1099
+ * 블록 배열로 쪼개지 않는 이유: 쪼개는 규칙(어디서 문단을 나눌지, 접힌 블록을
1100
+ * toggle 로 옮길지)은 실제 Notion API 계약이 정해진 뒤에야 의미가 있다.
1101
+ * 지금 추측으로 쪼개두면 본문 내용이 대상마다 갈라지고, 그게 이 wave 가 막으려는 것이다.
1102
+ */
1103
+ content: string;
1104
+ }
1105
+ /** 제보를 Notion 페이지 입력으로 바꾼다. 본문은 공용 렌더러 결과 그대로다. */
1106
+ declare function buildNotionPage(report: FeedbackReport): NotionPageInput;
1107
+
1108
+ /**
1109
+ * 엔드포인트 기본값. 코어가 아니라 **어댑터**가 갖는다(config.ts 의 ENDPOINT_FROM_ADAPTER).
1110
+ *
1111
+ * 상대 경로인 이유: 웹은 같은 오리진의 수집 라우트로 보내는 게 기본이고, 그래야
1112
+ * Origin 허용 목록이라는 실제 방어선이 그대로 작동한다. 앱(RN)에는 오리진이 없으므로
1113
+ * `endpoint` 를 절대 URL 로 지정해야 한다 — 지정 없이 앱에서 쓰면 아래에서 설정 오류로 끊는다.
1114
+ */
1115
+ declare const LASSO_DEFAULT_ENDPOINT = "/api/feedback";
1116
+ /** 최소한의 응답 형태. 코어는 DOM 타입(lib)에 의존하지 않으므로 구조적으로 정의한다. */
1117
+ interface LassoResponseLike {
1118
+ status: number;
1119
+ headers?: {
1120
+ get(name: string): string | null;
1121
+ } | undefined;
1122
+ text(): Promise<string>;
1123
+ }
1124
+ /** 최소한의 요청 형태. 표준 fetch 의 부분집합이라 그대로 꽂힌다. */
1125
+ interface LassoRequestInit {
1126
+ method: string;
1127
+ headers: Record<string, string>;
1128
+ body: string;
1129
+ keepalive?: boolean;
1130
+ }
1131
+ type LassoFetch = (url: string, init: LassoRequestInit) => Promise<LassoResponseLike>;
1132
+ interface LassoAdapterOpts {
1133
+ /** 수집 토큰. 없으면 `Authorization` 헤더 자체를 붙이지 않는다(미귀속 전송). */
1134
+ token?: string | null;
1135
+ /** 수집 엔드포인트. 없으면 LASSO_DEFAULT_ENDPOINT. */
1136
+ endpoint?: string | null;
1137
+ /** 전송 함수 주입(테스트용). 없으면 globalThis.fetch. */
1138
+ fetch?: LassoFetch | null;
1139
+ /** 경고 출력 주입. 위젯은 캡처를 우회하는 원본 console 을 넘긴다. */
1140
+ warn?: ((message: string, ...rest: unknown[]) => void) | null;
1141
+ }
1142
+ /**
1143
+ * 라쏘런으로 보내는 봉투.
1144
+ *
1145
+ * 필드명이 코어의 FeedbackReport 와 일부러 다르다. 코어는 이 이름들을 **모른다** —
1146
+ * 대상마다 받는 모양이 다르다는 게 어댑터를 두는 이유 자체다(DP-255 TC5).
1147
+ */
1148
+ /**
1149
+ * 라쏘런 `dp-agentation-ingest` 가 실제로 요구하는 봉투.
1150
+ *
1151
+ * 이 모양은 **상상하지 않고 실호출로 확정했다.** 이전 버전은 필드명을 지어내서
1152
+ * (submissionKey/bodyMarkdown/severity …) 실 엔드포인트에 100% 400 으로 거절당했고,
1153
+ * 400 은 재시도 불가라 모든 제보가 조용히 데드레터로 사라졌다.
1154
+ * 계약을 바꿀 때는 반드시 실호출로 다시 확인한다(adapters.live.test.ts).
1155
+ */
1156
+ interface LassoEnvelope {
1157
+ /** UUID. **Idempotency-Key 헤더와 반드시 같은 값**이어야 서버가 받는다. */
1158
+ clientSubmissionId: string;
1159
+ /**
1160
+ * 수집 소스의 종류와 같아야 한다(서버가 대조한다). 코어의 platform 을 옮긴 값이다:
1161
+ * web → "web", native → "app". 앱은 report.create 만 보낼 수 있다(요소 지목은 DOM 이 필요).
1162
+ */
1163
+ clientType: "web" | "app";
1164
+ /** 지목 주석은 annotation.add, 리포트 모달 제출은 report.create. */
1165
+ event: "annotation.add" | "report.create";
1166
+ /** epoch ms 정수. ISO 문자열을 보내면 거절된다. */
1167
+ timestamp: number;
1168
+ /** 절대 HTTP(S) URL. 소스의 host_patterns 에 등록된 origin 이어야 한다. */
1169
+ url: string;
1170
+ priority: "unset" | "normal" | "high" | "urgent";
1171
+ annotation?: {
1172
+ id: string;
1173
+ comment: string;
1174
+ x: number | null;
1175
+ y: number | null;
1176
+ element: string | null;
1177
+ elementPath: string | null;
1178
+ selectedText: string | null;
1179
+ boundingBox: {
1180
+ x: number;
1181
+ y: number;
1182
+ width: number;
1183
+ height: number;
1184
+ } | null;
1185
+ timestamp: number;
1186
+ };
1187
+ report?: {
1188
+ comment: string;
1189
+ pin: {
1190
+ x: number;
1191
+ y: number;
1192
+ } | null;
1193
+ sourceFile: string | null;
1194
+ screenshotBase64: string | null;
1195
+ screenshotContentType: "image/jpeg" | "image/png" | null;
1196
+ network: unknown[];
1197
+ logs: unknown[];
1198
+ };
1199
+ }
1200
+ /** 제보를 라쏘런 봉투로 바꾼다. 순수 함수라 테스트가 전송 없이 모양만 볼 수 있다. */
1201
+ declare function buildLassoEnvelope(report: FeedbackReport): LassoEnvelope;
1202
+ /**
1203
+ * 라쏘런 어댑터를 만든다.
1204
+ *
1205
+ * 실패 분류(코어가 이 값만 보고 재시도 여부를 정한다):
1206
+ * - 429 → 재시도 가능 + rateLimited. 코어가 최소 10분을 띄운다.
1207
+ * - 408/425/5xx/네트워크 오류 → 재시도 가능(일시적).
1208
+ * - 그 밖의 4xx → **재시도 불가.** 400/401/413 은 다시 보내도 같은 답이라 큐만 막는다.
1209
+ * - 2xx 인데 본문의 `ok` 가 true 가 아님 → 재시도 불가. 스키마 거절은 저절로 낫지 않는다.
1210
+ * - 2xx 인데 본문을 못 읽음 → **재시도 가능.** 프록시·CDN 이 HTML 을 끼워 넣는 일이 실제로 있고,
1211
+ * 멱등 키가 있어 다시 보내도 중복이 생기지 않는다.
1212
+ */
1213
+ declare function createLassoAdapter(opts?: LassoAdapterOpts): FeedbackAdapter;
1214
+
1215
+ /**
1216
+ * 저장소에서 게스트 id를 찾아 반환하고, 없으면 새로 만들어 저장한다.
1217
+ * 반환값은 사용자가 "이미 알고 있던" 식별자이므로 멱등(같은 저장소 → 같은 id).
1218
+ */
1219
+ declare function getOrCreateGuestId(storage: FeedbackStorage): Promise<string>;
1220
+ /**
1221
+ * 게스트 id로 FeedbackUser 객체를 만든다.
1222
+ *
1223
+ * `isGuest: true` 를 반드시 붙인다. 게스트를 `user: null` 로 보내면 받는 쪽에서
1224
+ * "신원 수집이 안 된 제보"와 "게스트가 남긴 제보"를 구분할 수 없다.
1225
+ */
1226
+ declare function guestUser(id: string): FeedbackUser;
1227
+
1228
+ /**
1229
+ * RFC 4122 v4 UUID 문자열을 만든다(8-4-4-4-16).
1230
+ * crypto.getRandomValues가 있으면 그것을, 없으면 Math.random을 쓴다.
1231
+ */
1232
+ declare function uuidv4(): string;
1233
+
1234
+ export { type AdapterName, type AppInfo, BACKOFF_BASE_MS, BACKOFF_MAX_MS, type BuildContextOpts, type BuildReportOpts, COMMENT_MAX_CHARS, COMMENT_REQUIRED_MESSAGE, COMMENT_TOO_LONG_MESSAGE, type CaptureWithinLimitOpts, type ConfigWarning, type ConfigWarningCode, type ContextProviders, type ContextUser, DEFAULT_ADAPTER, DEFAULT_INTERNAL_ROLES, DEFAULT_POSITION, type DeviceInfo, type DiagLogEntry, type DiagNetworkEntry, type DiagRequestRef, DiagnosticsCollector, type DiagnosticsExcludeMatcher, type DiagnosticsInstallOpts, type DiagnosticsPayload, type DiagnosticsSource, type DisplayInfo, ENDPOINT_FROM_ADAPTER, type ElementInfo, FLOATING_BUTTON_ID, type FeedbackAdapter, type FeedbackConfig, type FeedbackContext, type FeedbackKind, type FeedbackPin, type FeedbackPriority, FeedbackQueue, type FeedbackQueueOpts, type FeedbackReport, type FeedbackScreenshot, type FeedbackStorage, type FeedbackUser, FocusRing, type GetCurrentScreenFn, type GetUserFn, KEEPALIVE_BODY_LIMIT_BYTES, LASSO_DEFAULT_ENDPOINT, LOG_BUFFER_LIMIT, type LassoAdapterOpts, type LassoEnvelope, type LassoFetch, type LassoRequestInit, type LassoResponseLike, type LinearIssueInput, MAX_LOG_MESSAGE_CHARS, MAX_QUEUE_AGE_MS, MAX_QUEUE_ITEMS, MODAL_ACTION_ATTACH, MODAL_ACTION_CANCEL, MODAL_ACTION_PICK, MODAL_ACTION_PIN, MODAL_ACTION_REMOVE_SCREENSHOT, MODAL_ACTION_RETRY, MODAL_ACTION_SEND, MODAL_FIELD_COMMENT, MODAL_FIELD_PRIORITY, type ModalQueueLike, type ModalSubmitStatus, NETWORK_BUFFER_LIMIT, type NativeContext, type NotionPageInput, PinController, type PixelPoint, type PixelSize, type QueueStatus, type QueueStatusListener, RATE_LIMIT_MIN_DELAY_MS, REDACTED, ReportModalController, type ReportModalListener, type ReportModalOpts, type ReportModalState, type ReportParts, type ResolvedConfig, RingBuffer, SCREENSHOT_FAILED_MESSAGE, SCREENSHOT_MAX_BASE64_BYTES, SCREENSHOT_QUALITY_STEPS, SOURCE_ATTR, SUBMIT_DONE_MESSAGE, SUBMIT_FAILED_MESSAGE, SUBMIT_PENDING_MESSAGE, type ScreenshotCapture, type ScreenshotOutcome, type ScreenshotReencode, type ScreenshotStatus, type SourceLocation, type SourceMapping, type SubmitOutcome, type SubmitResult, TITLE_MAX_CHARS, type Visibility, type VisibilityEnv, type VisibilityFn, type WebContext, type WebContextInput, WidgetController, type WidgetControllerOpts, type WidgetCorner, type WidgetListener, type WidgetPlatform, type WidgetPosition, type WidgetScreen, type WidgetState, buildContext, buildLassoEnvelope, buildLinearIssue, buildNotionPage, buildReport, buildTitle, byteLengthOf, canUseKeepalive, captureWithinLimit, createLassoAdapter, defaultBackoff, denormalizePin, detectDevBuild, diagnosticsProviderFor, getOrCreateGuestId, guestUser, isInternalUser, normalizePin, normalizeUser, parseSourceAttr, renderReportBody, resolveConfig, resolveContextUser, sanitizeUrl, screenshotBytes, sharedDiagnostics, shouldShowWidget, sourceFromElement, uuidv4 };