@lightworkai.official/debug-capture 0.6.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.
Files changed (65) hide show
  1. package/README.md +241 -0
  2. package/dist/budget.d.ts +22 -0
  3. package/dist/bundle.d.ts +20 -0
  4. package/dist/capture/actionTrail.d.ts +7 -0
  5. package/dist/capture/cause.d.ts +3 -0
  6. package/dist/capture/consoleBuffer.d.ts +4 -0
  7. package/dist/capture/crashWatcher.d.ts +1 -0
  8. package/dist/capture/networkBuffer.d.ts +4 -0
  9. package/dist/capture/redact.d.ts +9 -0
  10. package/dist/capture/stepCorrelation.d.ts +41 -0
  11. package/dist/config.d.ts +217 -0
  12. package/dist/context.d.ts +2 -0
  13. package/dist/debug-capture.js +116 -0
  14. package/dist/embed.d.ts +1 -0
  15. package/dist/index.d.ts +45 -0
  16. package/dist/index.mjs +129 -0
  17. package/dist/install.d.ts +5 -0
  18. package/dist/mytickets/api.d.ts +115 -0
  19. package/dist/mytickets/format.d.ts +84 -0
  20. package/dist/mytickets/sanitize.d.ts +66 -0
  21. package/dist/mytickets/strings.d.ts +74 -0
  22. package/dist/mytickets/toolbar.d.ts +17 -0
  23. package/dist/reporter.d.ts +27 -0
  24. package/dist/screenshot.d.ts +7 -0
  25. package/dist/signature.d.ts +18 -0
  26. package/dist/submit.d.ts +8 -0
  27. package/dist/types.d.ts +175 -0
  28. package/dist/ui/annotator.d.ts +34 -0
  29. package/dist/ui/arrow.d.ts +25 -0
  30. package/dist/ui/element.d.ts +9 -0
  31. package/dist/ui/strings.d.ts +42 -0
  32. package/dist/ui/styles.d.ts +13 -0
  33. package/dist/ui/toast.d.ts +11 -0
  34. package/package.json +53 -0
  35. package/src/budget.ts +62 -0
  36. package/src/bundle.ts +274 -0
  37. package/src/capture/actionTrail.ts +693 -0
  38. package/src/capture/cause.ts +49 -0
  39. package/src/capture/consoleBuffer.ts +80 -0
  40. package/src/capture/crashWatcher.ts +61 -0
  41. package/src/capture/networkBuffer.ts +315 -0
  42. package/src/capture/redact.ts +117 -0
  43. package/src/capture/stepCorrelation.ts +160 -0
  44. package/src/config.ts +299 -0
  45. package/src/context.ts +81 -0
  46. package/src/embed.ts +35 -0
  47. package/src/index.ts +109 -0
  48. package/src/install.ts +59 -0
  49. package/src/mytickets/api.ts +226 -0
  50. package/src/mytickets/format.ts +191 -0
  51. package/src/mytickets/sanitize.ts +221 -0
  52. package/src/mytickets/strings.ts +217 -0
  53. package/src/mytickets/toolbar.ts +48 -0
  54. package/src/reporter.ts +53 -0
  55. package/src/screenshot.ts +238 -0
  56. package/src/signature.ts +40 -0
  57. package/src/styles.css +400 -0
  58. package/src/submit.ts +143 -0
  59. package/src/types.ts +169 -0
  60. package/src/ui/annotator.ts +698 -0
  61. package/src/ui/arrow.ts +62 -0
  62. package/src/ui/element.ts +362 -0
  63. package/src/ui/strings.ts +113 -0
  64. package/src/ui/styles.ts +96 -0
  65. package/src/ui/toast.ts +138 -0
package/README.md ADDED
@@ -0,0 +1,241 @@
1
+ # @lightworkai.official/debug-capture
2
+
3
+ A "report a problem" widget: rolling capture of network, console and user
4
+ actions, plus a screenshot the user can draw on, filed as a ticket on a
5
+ Lightwork Support host.
6
+
7
+ Framework-agnostic core with React, Vue and Angular wrappers.
8
+
9
+ ```ts
10
+ // once, at app start
11
+ import { configureDebugCapture } from "@lightworkai.official/debug-capture";
12
+
13
+ configureDebugCapture({
14
+ host: "https://support.example.com",
15
+ realmKey: "pk_live_…", // public by design
16
+ app: { version: "1.4.2", environment: "production" },
17
+ user: () => ({ id, email, fullName, unit, roles }),
18
+ });
19
+ ```
20
+
21
+ ```tsx
22
+ <DebugCaptureInit /> {/* arms the buffers; renders nothing */}
23
+ <ReportProblemButton /> {/* opens the reporter */}
24
+ ```
25
+
26
+ ## What it captures
27
+
28
+ | | |
29
+ | --- | --- |
30
+ | Network | method, URL, status, duration for everything; bodies only for allowed origins |
31
+ | Console | log/info/warn/error/debug, plus uncaught errors and rejections |
32
+ | Actions | clicks, inputs, navigations, and overlays that appeared — with the control's own HTML before and after |
33
+ | Context | route, module, user, viewport, memory, timing |
34
+ | Cause | the failed requests and errors most likely to be the reason |
35
+ | Screenshot | `getDisplayMedia` (captures cross-origin iframes), annotated by the user |
36
+
37
+ ## Configuration
38
+
39
+ Everything app-specific is a callback, called at capture time. The package
40
+ contains no hostnames, no keys and no organisation's vocabulary.
41
+
42
+ See `DebugCaptureConfig` in [`src/config.ts`](src/config.ts). The parts worth
43
+ knowing before you ship:
44
+
45
+ - **`capture.bodyOrigins`** — response bodies are stored for same-origin only
46
+ unless you name more. Bodies from a third party your app merely talks to are
47
+ not yours to collect.
48
+ - **`redact`** — extends the built-in header/query/body masking. Every app has a
49
+ secret the defaults do not know about.
50
+ - **`module(pathname)`** — becomes the ticket's category. Defaults to the first
51
+ path segment.
52
+
53
+ ## The other half: letting people track what they reported
54
+
55
+ Filing a report is one side. `<lw-my-tickets>` is the other — the list of what
56
+ this person has reported and where each one stands, with the conversation, the
57
+ team's answer, and a "still a problem" button — the reporter-facing
58
+ counterpart to the agent's ticket list, as a component you drop on a route of
59
+ your own.
60
+
61
+ ```tsx
62
+ import { MyTicketsPanel } from "@lightworkai.official/debug-capture-react";
63
+
64
+ export default function SupportPage() {
65
+ return <MyTicketsPanel />; // or openMyTickets() for a modal
66
+ }
67
+ ```
68
+
69
+ ### It needs to know who is asking
70
+
71
+ This is the one part that is not plug-and-play, and it cannot be.
72
+
73
+ `realmKey` is public by design — it ships in your client source, and that is
74
+ fine for filing, because the worst a stolen key buys is noise the rate limits
75
+ already absorb. **Reading is different.** "List the tickets for this email"
76
+ behind a public key is an endpoint that reads *everyone's* reports.
77
+
78
+ So your server vouches for your user. Ask the support team for the realm's reporter
79
+ secret (an admin sets `authConfig.reporterSecret` on the realm), keep it on your
80
+ **server**, and mint a short-lived token:
81
+
82
+ ```ts
83
+ // your backend — e.g. app/api/support-token/route.ts
84
+ import jwt from "jsonwebtoken";
85
+
86
+ export async function GET() {
87
+ const user = await currentUser(); // your session, your rules
88
+ const token = jwt.sign(
89
+ { sub: user.id, email: user.email, name: user.fullName },
90
+ process.env.SUPPORT_REPORTER_SECRET!, // never reaches the browser
91
+ { algorithm: "HS256", expiresIn: "1h" },
92
+ );
93
+ return Response.json({ token });
94
+ }
95
+ ```
96
+
97
+ ```ts
98
+ // your config — the callback just fetches that
99
+ configureDebugCapture({
100
+ host, realmKey, app: { name: "ERP" },
101
+ identity: () => fetch("/api/support-token").then((r) => r.json()).then((d) => d.token),
102
+ });
103
+ ```
104
+
105
+ That is the whole contract. Tokens must carry `exp`, live at most 12 hours, and
106
+ be signed HS256; anything else is refused. The callback runs per request, so
107
+ cache the token on your side until it is close to expiring.
108
+
109
+ `identity` is optional. Without it, filing still works exactly as before — the
110
+ report is simply anonymous, and the panel says "sign in to see your reports"
111
+ rather than showing an empty list. With it, submissions are linked to the person
112
+ too, and the identity in the token wins over any `requesterEmail` in the body.
113
+
114
+ ### What the panel shows
115
+
116
+ The realm's own status vocabulary, read from `/ingest/config` — so a lane an
117
+ admin adds appears with its label and colour, and an **internal** lane is never
118
+ named: the server resolves it to its public stand-in before it leaves. Internal
119
+ notes are not in the payload either. Beyond that:
120
+
121
+ - search over number, title, module and status; a status filter; sortable
122
+ columns; ten rows a page
123
+ - **รอคุณตอบกลับ** in amber when the team is waiting on this person
124
+ - **คำตอบจากทีม** — มีวิธีแก้ไข > ตอบกลับแล้ว > ยังไม่มีการตอบกลับ
125
+ - a status hero, a progress timeline including reopens, the conversation, and
126
+ **ยังมีปัญหา** when the realm enables `selfServiceReopen`
127
+
128
+ The reply box is TipTap, with the original's own toolbar — bold, italic,
129
+ underline, bullets, numbers, link, image. TipTap is a PEER dependency, so an app
130
+ that already has it pays nothing for it. Images upload first and are referenced
131
+ by the token the server mints; pasting a screenshot straight in works, which is
132
+ how one usually arrives.
133
+
134
+ ### Where the code lives
135
+
136
+ This package is the **core**: the capture buffers, the bundle, the ingest client,
137
+ the sanitiser, the status maps, the comparators, the dates, the strings — and one
138
+ stylesheet. It renders no panel UI.
139
+
140
+ The panel is real components in the framework packages:
141
+ `@lightworkai.official/debug-capture-vue` ships `.vue` single-file components,
142
+ `@lightworkai.official/debug-capture-react` ships `.tsx`, and
143
+ `@lightworkai.official/debug-capture-angular` ships standalone components. Each is markup
144
+ over the shared functions — the filtering, sorting and formatting exist once,
145
+ here.
146
+
147
+ Import the stylesheet once, from anywhere:
148
+
149
+ ```ts
150
+ import "@lightworkai.official/debug-capture/style.css";
151
+ ```
152
+
153
+ Every colour in it is a custom property, so a host restyles by setting variables
154
+ rather than by fighting specificity:
155
+
156
+ ```css
157
+ .my-app { --lw-primary: #7c3aed; --lw-accent: #6d28d9; }
158
+ ```
159
+
160
+ There is no attachments list. The original's reporter view has none either, and
161
+ what a widget report actually carries makes the reason plain: `debug-bundle.json`
162
+ is the capture payload the *team* reads, and offering it as a download put the
163
+ one artefact the reporter has no use for on their page. The screenshot they drew
164
+ on is theirs, and it is shown as a picture.
165
+
166
+ Message bodies are HTML written by other people and rendered inside *your* page,
167
+ so they go through an allowlist sanitiser: a small tag set, no event handlers,
168
+ links forced to `rel="noopener noreferrer nofollow"`, and images restricted to
169
+ the host's own inline URLs — a remote `<img>` would make every ticket a tracking
170
+ pixel aimed at whoever opens it.
171
+
172
+ Both outcomes dispatch a cancelable event (`lw-ticket-replied`,
173
+ `lw-ticket-reopened`, and their `-failed` pairs) before the built-in toast, so an
174
+ app with its own notification system calls `preventDefault()` and shows its own.
175
+
176
+ ## Installing
177
+
178
+ ```bash
179
+ bun add @lightworkai.official/debug-capture @lightworkai.official/debug-capture-react
180
+ # or: npm i @lightworkai.official/debug-capture @lightworkai.official/debug-capture-vue
181
+ ```
182
+
183
+ Install **both**: the framework package peer-depends on the core rather than
184
+ carrying it as a transitive dependency. That is deliberate. The core keeps the
185
+ capture buffers and the config as module-level singletons, so two copies in one
186
+ app means the `fetch` you patched is not the `fetch` the reporter reads back —
187
+ a failure that looks like "capture is empty" and nothing like a duplicate
188
+ install. A peer dependency is how you say *there must be exactly one of these*.
189
+
190
+ One stylesheet, imported once, from the core:
191
+
192
+ ```ts
193
+ import "@lightworkai.official/debug-capture/style.css";
194
+ ```
195
+
196
+ The wrappers deliberately do **not** re-export it. One file in one place is what
197
+ keeps a consumer who installs two wrappers from shipping two copies of it.
198
+
199
+ ### Peers you may already have
200
+
201
+ The wrappers compile the reply editor against TipTap and declare it as a peer
202
+ instead of bundling it: an app that already uses TipTap must end up on a single
203
+ copy, and the panel's editor should follow the app's version, not fight it.
204
+
205
+ | package | peers, besides the core |
206
+ | --- | --- |
207
+ | `-react` | `react` · `@tiptap/react` + `starter-kit`, `extension-image`, `extension-placeholder` |
208
+ | `-vue` | `vue` · `@tiptap/vue-3` + the same three |
209
+ | `-angular` | `@angular/{core,common,forms,platform-browser}` · `@tiptap/core` + the same three |
210
+
211
+ `html-to-image` is an *optional* peer of the core — see the maintainer notes.
212
+
213
+ ## Releasing
214
+
215
+ ```bash
216
+ bun run release:packages 0.4.1 # bump all four, keep peer ranges, rebuild
217
+ bun run check:packages # typecheck, test, and verify dist is not stale
218
+ ```
219
+
220
+ `dist` is **committed** as well as published. That makes it possible to change
221
+ source and leave the built output behind — shipping the previous release's code
222
+ under a new version number, with nothing downstream able to tell. `check:dist`
223
+ rebuilds and fails if git sees a difference, which is the only thing standing
224
+ between that mistake and a published tarball.
225
+
226
+ ## Notes for maintainers
227
+
228
+ **This package has side effects and must never be marked `sideEffects: false`.**
229
+ It patches `window.fetch` and `XMLHttpRequest` and defines a custom element. The
230
+ claim was there once, and because `index.ts` is only re-exports, bundlers
231
+ tree-shook the entire implementation away and emitted an entry that exported
232
+ names bound to nothing — a build that succeeds and ships nothing.
233
+
234
+ **`html-to-image` is an optional peer.** It is only the fallback for browsers
235
+ without the Screen Capture API, and it is loaded by a runtime `import()` so an
236
+ app that never installs it still builds.
237
+
238
+ **The reporter UI is one custom element in a shadow root.** Not three framework
239
+ components: the annotator alone would be three ports of the same canvas code.
240
+ The shadow root also means the host's CSS cannot reach in, which is what makes
241
+ this safe to drop into an app we do not control.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Keeping a bundle inside what the server will accept.
3
+ *
4
+ * The capture buffers hold full response bodies, which is the point — a 500's
5
+ * body is usually the answer. But a long session can hold megabytes of them, and
6
+ * the ingest endpoint caps an attachment at 5 MB and the request at 10 MB total.
7
+ * Arriving over the limit means the report is refused at the very moment
8
+ * somebody finally bothered to file one.
9
+ *
10
+ * So trim, in the order that loses the least: response bodies are the biggest
11
+ * and least often needed, oldest first, because the failure that prompted the
12
+ * report is at the END of the buffer. Only if that is not enough do whole
13
+ * entries go, and the trail of actions is never touched — it is small, and it is
14
+ * the part a human actually reads.
15
+ */
16
+ import type { DebugBundle } from "./types";
17
+ /**
18
+ * Well under the 5 MB attachment cap. The gap is deliberate: base64 adds a
19
+ * third, and the screenshot is in the same request.
20
+ */
21
+ export declare const BUNDLE_BUDGET_BYTES = 2000000;
22
+ export declare function trimToBudget(bundle: DebugBundle, budget?: number): DebugBundle;
@@ -0,0 +1,20 @@
1
+ import type { CapturedRequest, DebugBundle, DebugReason, ErroredQuery, ScreenshotMethod } from './types';
2
+ export interface BuildBundleInput {
3
+ note: string;
4
+ reason: DebugReason;
5
+ screenshotDataUrl: string | null;
6
+ screenshotMethod: ScreenshotMethod;
7
+ erroredQueries: ErroredQuery[];
8
+ }
9
+ /** Snapshot the live buffers + context into an immutable bundle. */
10
+ export declare function buildBundle(input: BuildBundleInput): DebugBundle;
11
+ export declare function toCurl(req: CapturedRequest): string;
12
+ /** Ticket-friendly Markdown report (excludes the screenshot binary). */
13
+ export declare function bundleToMarkdown(bundle: DebugBundle): string;
14
+ export declare function bundleFileName(bundle: DebugBundle, ext: string): string;
15
+ export declare function hasCause(bundle: DebugBundle): boolean;
16
+ export declare function downloadBlob(blob: Blob, filename: string): void;
17
+ /** Copy a PNG data URL to the clipboard as an actual image (pasteable into
18
+ * chat apps), not as base64 text. */
19
+ export declare function copyImageToClipboard(dataUrl: string): Promise<boolean>;
20
+ export declare function copyTextToClipboard(text: string): Promise<boolean>;
@@ -0,0 +1,7 @@
1
+ import type { ActionTrailEntry } from '../types';
2
+ /** Display suffix for a collapsed repeated action, e.g. " (×4)". Empty for a
3
+ * single occurrence — shared by the report text + the in-dialog preview. */
4
+ export declare function actionCountSuffix(count?: number): string;
5
+ export declare function installActionTrail(): void;
6
+ export declare function getActionTrail(): ActionTrailEntry[];
7
+ export declare function clearActionTrail(): void;
@@ -0,0 +1,3 @@
1
+ import type { CapturedConsoleEntry, CapturedRequest, CauseSummary, ErroredQuery } from '../types';
2
+ export declare function computeCause(network: CapturedRequest[], consoleLog: CapturedConsoleEntry[], erroredQueries: ErroredQuery[]): CauseSummary;
3
+ export declare function causeCount(cause: CauseSummary): number;
@@ -0,0 +1,4 @@
1
+ import type { CapturedConsoleEntry } from '../types';
2
+ export declare function installConsoleCapture(): void;
3
+ export declare function getConsoleLog(): CapturedConsoleEntry[];
4
+ export declare function clearConsoleLog(): void;
@@ -0,0 +1 @@
1
+ export declare function installCrashWatcher(): void;
@@ -0,0 +1,4 @@
1
+ import type { CapturedRequest } from '../types';
2
+ export declare function installNetworkCapture(): void;
3
+ export declare function getNetworkLog(): CapturedRequest[];
4
+ export declare function clearNetworkLog(): void;
@@ -0,0 +1,9 @@
1
+ export declare const REDACTED = "\u2039redacted\u203A";
2
+ /** Strip likely PII (emails, phone/ID number runs) from a captured UI label.
3
+ * Labels come from visible text, so they rarely hold secrets — but a container's
4
+ * text can carry a reporter's name/number, and tickets are shown to admins. */
5
+ export declare function redactLabel(text: string): string;
6
+ export declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
7
+ export declare function redactUrl(rawUrl: string): string;
8
+ /** Redact + truncate a request/response body string. */
9
+ export declare function redactBody(body: string | null, maxLen: number): string | null;
@@ -0,0 +1,41 @@
1
+ import type { ActionTrailEntry, CapturedConsoleEntry, CapturedRequest } from '../types';
2
+ export interface StepActivity {
3
+ /** ms since the previous step. Null on the first — nothing to measure from. */
4
+ elapsedMs: number | null;
5
+ /** Requests that STARTED while this step was the most recent one. */
6
+ requests: CapturedRequest[];
7
+ /** Subset that errored or came back >= 400 — usually the answer. */
8
+ failedRequests: CapturedRequest[];
9
+ /** error / exception / unhandledrejection logged in the same window. */
10
+ consoleErrors: CapturedConsoleEntry[];
11
+ /** Longest request in the window — surfaces the click that appeared to hang. */
12
+ slowestMs: number | null;
13
+ }
14
+ /** A request counts as failed if it rejected outright or returned >= 400. */
15
+ export declare function isFailedRequest(req: CapturedRequest): boolean;
16
+ /** Path (no origin, no query) for compact display — the origin is identical on
17
+ * every row and the query is often a long redacted filter string.
18
+ *
19
+ * Decoded before display: `URL` percent-encodes non-ASCII, and these paths
20
+ * routinely carry Thai, so the raw `pathname` would render as a wall of
21
+ * `%E0%B9%84…` exactly where a human is trying to read which endpoint failed. */
22
+ export declare function requestPath(url: string): string;
23
+ /**
24
+ * Attribute network + console activity to the step that was current at the time.
25
+ *
26
+ * Step `i` owns `[at(i), at(i+1))`; the last step owns everything after it.
27
+ * Returns one entry per trail row, in the same order, so callers can index by
28
+ * position without a lookup.
29
+ *
30
+ * Activity BEFORE the first recorded step belongs to no step and is omitted:
31
+ * the trail is a bounded ring buffer, so the earliest rows are frequently
32
+ * mid-session and anything before them was caused by actions already evicted —
33
+ * attributing it to step 1 would invent a link that isn't there. The Network and
34
+ * Console sections still list every row.
35
+ */
36
+ export declare function correlateActionTrail(trail: readonly ActionTrailEntry[], network: readonly CapturedRequest[], consoleEntries: readonly CapturedConsoleEntry[]): StepActivity[];
37
+ /** True when a step produced nothing worth showing — lets the UI skip the row
38
+ * rather than print a line of zeroes under every step. */
39
+ export declare function isQuietStep(activity: StepActivity, gapThresholdMs: number): boolean;
40
+ /** Compact Thai duration: "1.4 วิ" / "12 วิ" / "2 นาที 5 วิ". */
41
+ export declare function formatDuration(ms: number): string;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Everything this library refuses to guess.
3
+ *
4
+ * The original read the app's env module, its user store and its API-header
5
+ * helper directly — three imports that made it un-shippable. They are callbacks
6
+ * now, and the rule they follow is: if a fact belongs to ONE app, the app
7
+ * supplies it.
8
+ *
9
+ * That includes the module vocabulary. The original hard-coded a map of Thai
10
+ * department names; a package that ships another organisation's org chart is
11
+ * not a package.
12
+ *
13
+ * Nothing here is secret. `realmKey` is public by design — the support ingest
14
+ * endpoint authenticates the widget with it and rate-limits per IP *and* per
15
+ * realm precisely because it travels in client code. Do not put a bearer token
16
+ * in `headers()`; it is captured into every bundle (redacted, but still).
17
+ */
18
+ import type { DebugUser } from "./types";
19
+ /** The shape of html-to-image's `toPng`, without depending on it. */
20
+ export type ToPng = (node: HTMLElement, options?: Record<string, unknown>) => Promise<string>;
21
+ export interface DebugCaptureConfig {
22
+ /** Support host origin, e.g. https://support.example.com */
23
+ host: string;
24
+ /** The realm's public embed key. */
25
+ realmKey: string;
26
+ app: {
27
+ /**
28
+ * What this app is called, in the words a support agent would use.
29
+ *
30
+ * It becomes the ticket's category, so a desk taking reports from several
31
+ * apps can filter by the one that broke. The module was doing that job,
32
+ * derived from the first path segment — which produced badges like "admin"
33
+ * and told a reader nothing about WHICH admin, of which app. A name the app
34
+ * states about itself cannot be wrong; one inferred from a URL usually is.
35
+ */
36
+ name: string;
37
+ /** Defaults to "0.0.0". Worth setting: it ties a report to a build. */
38
+ version?: string;
39
+ /**
40
+ * "production" | "uat" | "development" — free text; it is only reported.
41
+ *
42
+ * Inferred from the hostname when omitted, which gets localhost right and
43
+ * everything else wrong-but-harmless. A staging environment is worth naming
44
+ * yourself; nothing can guess the difference between uat and production
45
+ * from a URL.
46
+ */
47
+ environment?: string;
48
+ };
49
+ /** Who is using the app. Called at capture time, not at config time. */
50
+ user?: () => DebugUser;
51
+ /**
52
+ * Proof of who that is, minted by the HOST'S SERVER. Required only by the
53
+ * "my tickets" surface.
54
+ *
55
+ * `user()` above is a claim; this is evidence. Filing a report needs no
56
+ * evidence — the worst a false name buys is a mislabelled ticket — but reading
57
+ * reports back does, because `realmKey` is public and "show me the tickets for
58
+ * this email" behind a public key is an endpoint that reads everyone's.
59
+ *
60
+ * Your server signs a short-lived HS256 JWT with the realm's reporter secret:
61
+ *
62
+ * jwt.sign({ sub: user.id, email: user.email, name: user.fullName },
63
+ * process.env.SUPPORT_REPORTER_SECRET,
64
+ * { algorithm: 'HS256', expiresIn: '1h' })
65
+ *
66
+ * The SECRET NEVER REACHES THE BROWSER. This callback fetches the finished
67
+ * token from your own backend; it is called per request, so cache it on your
68
+ * side until it is close to expiring.
69
+ *
70
+ * Returning null means "nobody is signed in" — the panel says so rather than
71
+ * failing.
72
+ */
73
+ identity?: () => string | null | Promise<string | null>;
74
+ /**
75
+ * Extra request headers the app sends, for the record — acting-as ids and
76
+ * the like. Redacted before it reaches a bundle.
77
+ */
78
+ headers?: () => Record<string, string>;
79
+ /**
80
+ * Which part of the app the user was in — สารบรรณ, ครุภัณฑ์, งบประมาณ.
81
+ *
82
+ * This becomes the ticket's category, because on a desk that supports web
83
+ * applications "which module broke" is the question worth filtering by. It
84
+ * falls back to `app.name`, so a single-module app still lands somewhere
85
+ * sensible without configuring anything.
86
+ *
87
+ * A DECLARED module is worth having; a guessed one is not. This used to
88
+ * default to the first path segment, which produced categories like "admin"
89
+ * — true of the URL and meaningless to whoever picks the report up. If the app
90
+ * cannot say, it should not guess, so the fallback is the app's own name.
91
+ *
92
+ * Return null for a path you have no name for. Most hosts declare a map of
93
+ * the screens they care about, and the rule this callback exists to enforce
94
+ * applies to THEM too: without a way to say "not one of mine", the only ways
95
+ * out are inventing a label or throwing, and a host that throws here looks
96
+ * like a host with no modules at all.
97
+ */
98
+ module?: (pathname: string) => {
99
+ key: string;
100
+ label: string;
101
+ } | null;
102
+ /** Anything else worth recording — feature flags, build id, tenant. */
103
+ extra?: () => Record<string, unknown>;
104
+ capture?: CaptureOptions;
105
+ redact?: RedactOptions;
106
+ /**
107
+ * A DOM-rendering screenshot fallback, for browsers without the Screen
108
+ * Capture API. Supplied by the host so this package never names a dependency
109
+ * it does not have — a bare `import('html-to-image')` here survives our build
110
+ * and then fails the CONSUMER's, because a minifier folds any runtime
111
+ * specifier back into a literal Rollup tries to resolve.
112
+ *
113
+ * screenshotFallback: () => import('html-to-image').then((m) => m.toPng)
114
+ */
115
+ screenshotFallback?: () => Promise<ToPng | null>;
116
+ /** UI language. Only affects the built-in reporter's own strings. */
117
+ locale?: "th" | "en";
118
+ }
119
+ export interface CaptureOptions {
120
+ /** Rolling buffer sizes. */
121
+ maxRequests?: number;
122
+ maxConsoleEntries?: number;
123
+ maxActions?: number;
124
+ /** Per-response body cap, in characters. */
125
+ maxBodyChars?: number;
126
+ /**
127
+ * Origins whose RESPONSE BODIES may be captured. Same-origin is always
128
+ * included; anything else records method/status/duration only.
129
+ */
130
+ bodyOrigins?: string[];
131
+ /**
132
+ * CSS selectors for overlays worth recording when they appear — dialogs,
133
+ * menus, toasts. Defaults cover ARIA roles, which is most component libraries.
134
+ */
135
+ overlaySelectors?: string[];
136
+ }
137
+ export interface RedactOptions {
138
+ /** Extra header names to mask. Matched case-insensitively, as whole names. */
139
+ headers?: string[];
140
+ /** Extra query-string keys to mask. */
141
+ queryKeys?: string[];
142
+ /** Extra body keys to mask. Matched as a substring, like the built-ins. */
143
+ bodyKeys?: string[];
144
+ }
145
+ declare const DEFAULTS: {
146
+ maxRequests: number;
147
+ maxConsoleEntries: number;
148
+ maxActions: number;
149
+ maxBodyChars: number;
150
+ };
151
+ export declare function configureDebugCapture(config: DebugCaptureConfig): void;
152
+ /**
153
+ * Throws rather than returning a default. A bundle assembled against a
154
+ * half-configured library would be submitted to nowhere and look like it
155
+ * worked — and that failure surfaces as a missing ticket, days later.
156
+ */
157
+ export declare function getConfig(): DebugCaptureConfig;
158
+ export declare function isConfigured(): boolean;
159
+ /**
160
+ * The app's identity, with the parts it did not bother to state filled in.
161
+ *
162
+ * Everything optional in this library is optional because a sensible default
163
+ * exists — the config a host writes should be the things only that host knows.
164
+ */
165
+ export declare function appInfo(): {
166
+ name: string;
167
+ version: string;
168
+ environment: string;
169
+ };
170
+ export declare function captureOption(key: keyof typeof DEFAULTS): number;
171
+ /**
172
+ * The host's module for this path, or nothing.
173
+ *
174
+ * Nothing, deliberately: an undeclared module is not the first path segment. See
175
+ * the note on `module` above.
176
+ */
177
+ export declare function resolveModule(pathname: string): {
178
+ key: string;
179
+ label: string;
180
+ } | null;
181
+ /** Redaction extras, always a usable shape even before configure() runs. */
182
+ export declare function getRedactOptions(): Required<RedactOptions>;
183
+ export declare function overlaySelectors(): string[];
184
+ /**
185
+ * Whether a response body from this URL may be stored.
186
+ *
187
+ * Same-origin always: the app's own API is the whole reason anyone opens a
188
+ * report. Anything else only if the host named it in `bodyOrigins` — a body
189
+ * from a third party the host merely calls is not ours to collect.
190
+ *
191
+ * The one exclusion is the WIDGET'S OWN traffic: `/ingest/*` on the support
192
+ * host. Those calls are this library talking to itself, and a report that
193
+ * contains the request which fetched the report's own config tells a reader
194
+ * nothing.
195
+ *
196
+ * It used to exclude the support host's WHOLE origin, as a guard against a
197
+ * report filed from a ticket screen storing ticket bodies that themselves carry
198
+ * earlier reports' captured data. That was too blunt by half: when the app IS
199
+ * the support product filing its own bugs, the support host is its
200
+ * own origin, so the guard dropped every body it had and reports arrived with
201
+ * nothing but URLs and timings.
202
+ *
203
+ * The compounding it worried about is already bounded, and was before: every
204
+ * body is truncated at `maxBodyChars` (20k), a response over 1 MB is never read
205
+ * at all, and `trimToBudget` caps the finished bundle at 2 MB. A second, much
206
+ * coarser cap on top of those cost far more than it saved.
207
+ */
208
+ export declare function mayCaptureBody(url: string): boolean;
209
+ /**
210
+ * This library talking to the support host — `/ingest/config`, `/ingest/me/…`.
211
+ *
212
+ * Matched by PATH as well as origin, deliberately. Matching the origin alone is
213
+ * what made a self-hosted support app unable to capture anything: its own API
214
+ * lives on that origin too.
215
+ */
216
+ export declare function isWidgetTraffic(target: URL): boolean;
217
+ export {};
@@ -0,0 +1,2 @@
1
+ import type { DebugContext } from "./types";
2
+ export declare function collectContext(): DebugContext;