@koderlabs/tasks-sdk-web-reporter 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,362 @@
1
+ import { Integration, ClientInterface } from '@koderlabs/tasks-sdk';
2
+ import { Serializable } from '@koderlabs/tasks-sdk-types';
3
+
4
+ interface DrawCommand {
5
+ /** 'highlight' = red cutout rect with dim overlay; 'hide' = opaque black box */
6
+ type: 'highlight' | 'hide';
7
+ /** Normalized 0..1 against captured-image width */
8
+ x: number;
9
+ /** Normalized 0..1 against captured-image height */
10
+ y: number;
11
+ /** Normalized 0..1 width */
12
+ w: number;
13
+ /** Normalized 0..1 height */
14
+ h: number;
15
+ }
16
+ type WidgetApi = {
17
+ show(): void;
18
+ hide(): void;
19
+ isVisible(): boolean;
20
+ capture(mode?: 'fullscreen' | 'advanced'): Promise<void>;
21
+ cancelCapture(): void;
22
+ setReporter(info: {
23
+ email: string;
24
+ fullName?: string;
25
+ }): void;
26
+ clearReporter(): void;
27
+ setCustomData(data?: Record<string, Serializable>): void;
28
+ setNetworkRecordingSettings(s: {
29
+ excludedKeys?: string[];
30
+ excludedDomains?: string[];
31
+ }): void;
32
+ on(event: WidgetEventName, listener: (e: WidgetEvent) => void): void;
33
+ off(event: WidgetEventName, listener: (e: WidgetEvent) => void): void;
34
+ unload(): void;
35
+ };
36
+ type WidgetEventName = 'load' | 'loaderror' | 'show' | 'hide' | 'capture' | 'feedbackbeforesend' | 'feedbacksent' | 'feedbackerror' | 'feedbackdiscarded';
37
+ /** Mutable fields in feedbackbeforesend — cannot mutate description/screenshot */
38
+ type MutableFeedbackField = 'assignee' | 'labels' | 'customFields' | 'priority' | 'issueType';
39
+ interface BeforeSendPayload {
40
+ values: {
41
+ description: string;
42
+ title?: string;
43
+ email?: string;
44
+ assignee?: string;
45
+ labels?: string[];
46
+ customFields?: Record<string, unknown>;
47
+ priority?: string;
48
+ issueType?: string;
49
+ };
50
+ setValue(field: MutableFeedbackField, value: unknown): void;
51
+ cancel(): void;
52
+ }
53
+ type WidgetEvent = {
54
+ name: 'load';
55
+ } | {
56
+ name: 'loaderror';
57
+ error: Error;
58
+ } | {
59
+ name: 'show';
60
+ } | {
61
+ name: 'hide';
62
+ } | {
63
+ name: 'capture';
64
+ canvas: HTMLCanvasElement;
65
+ } | (BeforeSendPayload & {
66
+ name: 'feedbackbeforesend';
67
+ }) | {
68
+ name: 'feedbacksent';
69
+ ticketKey: string;
70
+ ticketUrl: string;
71
+ } | {
72
+ name: 'feedbackerror';
73
+ error: Error;
74
+ } | {
75
+ name: 'feedbackdiscarded';
76
+ };
77
+ interface NetworkRecordingSettings {
78
+ enabled?: boolean;
79
+ excludedKeys?: string[];
80
+ excludedDomains?: string[];
81
+ }
82
+ interface WidgetOptions {
83
+ /**
84
+ * Hotkey string, e.g. 'ctrl+shift+i'. Set to false to disable.
85
+ * Mac users should use 'meta+shift+i' or keep ctrl for cross-platform.
86
+ */
87
+ hotkey?: string | false;
88
+ /** Auto-inject a floating trigger button into the host page. Default: true. */
89
+ autoInject?: boolean;
90
+ /** Prefer getDisplayMedia (asks browser permission, pixel-perfect) over
91
+ * html2canvas (no prompt, DOM-rendered). Default: false — seamless UX.
92
+ * Flip to true when the page has WebGL / cross-origin iframes / canvas
93
+ * content html2canvas can't render. */
94
+ useNativeScreenshot?: boolean;
95
+ /** Capture screenshot automatically when the widget opens, instead of
96
+ * requiring an explicit click. Default: true. Set false to keep the
97
+ * "Add Screenshot" button as the trigger. */
98
+ autoCapture?: boolean;
99
+ /** Show "Powered by InstantTasks" branding. Default: true. */
100
+ showBranding?: boolean;
101
+ /** Default reporter info. Can be overridden via setReporter(). */
102
+ reporter?: {
103
+ email: string;
104
+ fullName?: string;
105
+ };
106
+ /** Arbitrary key/value appended to every submission. */
107
+ customData?: Record<string, Serializable>;
108
+ /** Whether to ask the user for their email. Default: false. */
109
+ collectEmail?: boolean;
110
+ /** Suppress console diagnostics. Default: false. */
111
+ silent?: boolean;
112
+ /**
113
+ * Expose `window.InstantTasks.show()` for manual triggering from DevTools.
114
+ * Default: true in non-production, false in production. In production this
115
+ * defaults off because any third-party script (analytics, A/B testing,
116
+ * vendor injects) can read `window.InstantTasks` and trigger the widget.
117
+ */
118
+ exposeGlobal?: boolean;
119
+ /** Network recording settings. */
120
+ networkRecording?: NetworkRecordingSettings;
121
+ }
122
+
123
+ /**
124
+ * Capability detection for native screenshot capture.
125
+ * Ported from: sentry-javascript/packages/feedback/src/util/isScreenshotSupported.ts
126
+ *
127
+ * Returns false on:
128
+ * - Mobile UA (Android, iPhone, iPad via UA, etc.)
129
+ * - iPad-as-Mac (Macintosh UA with maxTouchPoints > 1)
130
+ * - Insecure context (non-HTTPS except localhost)
131
+ * - Missing getDisplayMedia API
132
+ */
133
+ declare function isNativeCaptureSupported(): boolean;
134
+
135
+ /**
136
+ * Hotkey parser and listener factory.
137
+ *
138
+ * Parses strings like 'ctrl+shift+i', 'meta+shift+i'.
139
+ * Modifiers: ctrl, meta, alt, shift (order-insensitive).
140
+ * Ignores keypresses when the focused element is an input, textarea,
141
+ * or any contenteditable element (to avoid interfering with user typing).
142
+ */
143
+ interface ParsedHotkey {
144
+ ctrl: boolean;
145
+ meta: boolean;
146
+ alt: boolean;
147
+ shift: boolean;
148
+ key: string;
149
+ }
150
+ /**
151
+ * Parse a hotkey string into a structured predicate.
152
+ * Returns null for invalid or falsy input.
153
+ */
154
+ declare function parseHotkey(raw: string | false | undefined): ParsedHotkey | null;
155
+ /**
156
+ * Register a keydown listener for the given hotkey string.
157
+ * Returns a cleanup function to remove the listener.
158
+ *
159
+ * @param hotkeyStr - e.g. 'ctrl+shift+i' or false to disable.
160
+ * @param handler - Called when the hotkey fires outside typing targets.
161
+ * @param target - Element to attach to (defaults to window).
162
+ */
163
+ declare function registerHotkey(hotkeyStr: string | false | undefined, handler: () => void, target?: EventTarget): () => void;
164
+
165
+ /**
166
+ * Metadata collector.
167
+ *
168
+ * Collects:
169
+ * - url: current window.location.href
170
+ * - userAgent: navigator.userAgent
171
+ * - viewport: { width, height }
172
+ * - appVersion: from <meta name="app-version" content="…"> if present
173
+ * - consoleTail: last N console messages (ring buffer, opt-in via captureConsole)
174
+ * - customData: lazy callback result (Jam-style pattern)
175
+ */
176
+
177
+ interface ConsoleEntry {
178
+ level: 'log' | 'warn' | 'error' | 'info' | 'debug';
179
+ args: string[];
180
+ ts: number;
181
+ }
182
+ interface BrowserInfo {
183
+ name?: string;
184
+ version?: string;
185
+ major?: string;
186
+ mobile?: boolean;
187
+ }
188
+ interface OsInfo {
189
+ name?: string;
190
+ version?: string;
191
+ }
192
+ interface DeviceInfo {
193
+ deviceMemoryGB?: number;
194
+ hardwareConcurrency?: number;
195
+ jsHeapUsedMB?: number;
196
+ jsHeapTotalMB?: number;
197
+ }
198
+ interface NetworkInfo {
199
+ online?: boolean;
200
+ effectiveType?: string;
201
+ downlinkMbps?: number;
202
+ rttMs?: number;
203
+ saveData?: boolean;
204
+ }
205
+ interface CollectedMetadata {
206
+ url: string;
207
+ userAgent: string;
208
+ viewport: {
209
+ width: number;
210
+ height: number;
211
+ };
212
+ appVersion?: string;
213
+ consoleTail?: ConsoleEntry[];
214
+ customData?: Record<string, Serializable>;
215
+ /** Physical screen resolution (independent of window size). */
216
+ screen?: {
217
+ width: number;
218
+ height: number;
219
+ dpr: number;
220
+ };
221
+ /** Parsed from userAgentData (Chromium) or UA regex. */
222
+ browser?: BrowserInfo;
223
+ /** Parsed OS name + version. */
224
+ os?: OsInfo;
225
+ /** Hardware capabilities. */
226
+ device?: DeviceInfo;
227
+ /** Network conditions at submit time. */
228
+ network?: NetworkInfo;
229
+ /** BCP-47 language tags from `navigator.languages`. */
230
+ languages?: string[];
231
+ /** IANA timezone name (e.g. `America/New_York`). */
232
+ timezone?: string;
233
+ /** UTC offset in minutes at submit time. */
234
+ timezoneOffsetMin?: number;
235
+ /** User-agent preference signals. */
236
+ preferences?: {
237
+ colorScheme?: 'light' | 'dark';
238
+ reducedMotion?: boolean;
239
+ contrast?: 'no-preference' | 'more' | 'less';
240
+ };
241
+ /** Document referrer at the moment of the report. */
242
+ referrer?: string;
243
+ /** Navigation timing snapshot — useful for "was the page slow?" reports. */
244
+ pageLoad?: {
245
+ domContentLoadedMs?: number;
246
+ loadCompleteMs?: number;
247
+ ttfbMs?: number;
248
+ };
249
+ /** Wall-clock at submit time (server already has receive time; this is the user's). */
250
+ clientTime?: string;
251
+ }
252
+ /** Patch global console to capture a ring buffer of log calls. */
253
+ declare function patchConsole(): () => void;
254
+ /**
255
+ * Collect current-page metadata snapshot.
256
+ *
257
+ * @param captureConsole - Include console ring buffer. Default: false.
258
+ * @param customDataFn - Lazy callback that returns extra key/value data.
259
+ */
260
+ declare function collectMetadata(captureConsole?: boolean, customDataFn?: () => Record<string, Serializable> | Promise<Record<string, Serializable>>): Promise<CollectedMetadata>;
261
+ /**
262
+ * Format collected metadata as a collapsible markdown block.
263
+ * Appended to the ticket description body.
264
+ */
265
+ /**
266
+ * Format collected metadata as an HTML block.
267
+ *
268
+ * Output is HTML — not markdown — because ticket descriptions in
269
+ * InstantTasks are stored as TipTap-compatible HTML and the renderer
270
+ * does NOT post-process markdown. Returning `**bold**` and bare `\n`
271
+ * would render as literal asterisks on one collapsed line.
272
+ */
273
+ declare function formatMetadataBlock(meta: CollectedMetadata): string;
274
+
275
+ /**
276
+ * @koderlabs/tasks-sdk-web-reporter
277
+ *
278
+ * Browser in-app bug-reporter integration for the InstantTasks SDK.
279
+ * `reporterIntegration(opts)` (alias: `widgetIntegration` for back-compat).
280
+ *
281
+ * Usage:
282
+ * import { init } from '@koderlabs/tasks-sdk';
283
+ * import { reporterIntegration } from '@koderlabs/tasks-sdk-web-reporter';
284
+ *
285
+ * const client = init({
286
+ * projectId: 'FE',
287
+ * accessKey: 'sk_live_…',
288
+ * integrations: [reporterIntegration({ hotkey: 'ctrl+shift+i' })],
289
+ * });
290
+ */
291
+
292
+ type Listener = (e: WidgetEvent) => void;
293
+ declare class WidgetIntegration implements Integration {
294
+ readonly name = "widget";
295
+ private opts;
296
+ /** Caller-supplied options, before defaults were applied. Used to decide
297
+ * which fields can be overwritten by server-side project config. */
298
+ private _initialOpts;
299
+ private client;
300
+ private shell;
301
+ private fabBtn;
302
+ /** Outer shadow-host element for the FAB. Tracked separately from the
303
+ * button so capture flows can hide the *entire* widget chrome (not just
304
+ * the modal) from screenshots. */
305
+ private fabHost;
306
+ private _visible;
307
+ private _reporter;
308
+ private _customData;
309
+ /** Reserved for upcoming network capture filters (Phase I).
310
+ * Public so the field stays type-checked and isn't tree-shaken; safe to
311
+ * read but currently has no effect. */
312
+ networkSettings: {
313
+ excludedKeys?: string[];
314
+ excludedDomains?: string[];
315
+ };
316
+ private listeners;
317
+ private cleanupHotkey;
318
+ private cleanupConsole;
319
+ constructor(opts: WidgetOptions);
320
+ setup(client: ClientInterface): void;
321
+ /**
322
+ * GET /sdk/v1/config — uses the SDK's own configured endpoint + access
323
+ * key (the client already has both). Merges into this.opts only for
324
+ * fields the caller didn't explicitly set.
325
+ */
326
+ private _applyRemoteConfig;
327
+ /** Wires hotkey + FAB + debug surface. Called after _applyRemoteConfig. */
328
+ private _wireRuntime;
329
+ teardown(): void;
330
+ show(): void;
331
+ hide(): void;
332
+ isVisible(): boolean;
333
+ capture(mode?: 'fullscreen' | 'advanced'): Promise<void>;
334
+ cancelCapture(): void;
335
+ setReporter(info: {
336
+ email: string;
337
+ fullName?: string;
338
+ }): void;
339
+ clearReporter(): void;
340
+ setCustomData(data?: Record<string, unknown>): void;
341
+ setNetworkRecordingSettings(s: {
342
+ excludedKeys?: string[];
343
+ excludedDomains?: string[];
344
+ }): void;
345
+ on(event: WidgetEventName, listener: Listener): void;
346
+ off(event: WidgetEventName, listener: Listener): void;
347
+ unload(): void;
348
+ private _openModal;
349
+ private _injectFab;
350
+ private _emit;
351
+ }
352
+ /**
353
+ * Create an in-app reporter integration for the InstantTasks SDK client.
354
+ *
355
+ * @param opts - Reporter configuration options.
356
+ * @returns An Integration that can be passed to `init({ integrations: [...] })`.
357
+ */
358
+ declare function reporterIntegration(opts?: WidgetOptions): Integration & WidgetApi;
359
+ /** @deprecated Renamed to `reporterIntegration`. Will be removed in v1. */
360
+ declare const widgetIntegration: typeof reporterIntegration;
361
+
362
+ export { type DrawCommand, WidgetIntegration as ReporterIntegration, type WidgetApi, type WidgetEvent, type WidgetEventName, WidgetIntegration, type WidgetOptions, collectMetadata, reporterIntegration as default, formatMetadataBlock, isNativeCaptureSupported, parseHotkey, patchConsole, registerHotkey, reporterIntegration, widgetIntegration };