@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.
- package/README.md +241 -0
- package/dist/budget.d.ts +22 -0
- package/dist/bundle.d.ts +20 -0
- package/dist/capture/actionTrail.d.ts +7 -0
- package/dist/capture/cause.d.ts +3 -0
- package/dist/capture/consoleBuffer.d.ts +4 -0
- package/dist/capture/crashWatcher.d.ts +1 -0
- package/dist/capture/networkBuffer.d.ts +4 -0
- package/dist/capture/redact.d.ts +9 -0
- package/dist/capture/stepCorrelation.d.ts +41 -0
- package/dist/config.d.ts +217 -0
- package/dist/context.d.ts +2 -0
- package/dist/debug-capture.js +116 -0
- package/dist/embed.d.ts +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.mjs +129 -0
- package/dist/install.d.ts +5 -0
- package/dist/mytickets/api.d.ts +115 -0
- package/dist/mytickets/format.d.ts +84 -0
- package/dist/mytickets/sanitize.d.ts +66 -0
- package/dist/mytickets/strings.d.ts +74 -0
- package/dist/mytickets/toolbar.d.ts +17 -0
- package/dist/reporter.d.ts +27 -0
- package/dist/screenshot.d.ts +7 -0
- package/dist/signature.d.ts +18 -0
- package/dist/submit.d.ts +8 -0
- package/dist/types.d.ts +175 -0
- package/dist/ui/annotator.d.ts +34 -0
- package/dist/ui/arrow.d.ts +25 -0
- package/dist/ui/element.d.ts +9 -0
- package/dist/ui/strings.d.ts +42 -0
- package/dist/ui/styles.d.ts +13 -0
- package/dist/ui/toast.d.ts +11 -0
- package/package.json +53 -0
- package/src/budget.ts +62 -0
- package/src/bundle.ts +274 -0
- package/src/capture/actionTrail.ts +693 -0
- package/src/capture/cause.ts +49 -0
- package/src/capture/consoleBuffer.ts +80 -0
- package/src/capture/crashWatcher.ts +61 -0
- package/src/capture/networkBuffer.ts +315 -0
- package/src/capture/redact.ts +117 -0
- package/src/capture/stepCorrelation.ts +160 -0
- package/src/config.ts +299 -0
- package/src/context.ts +81 -0
- package/src/embed.ts +35 -0
- package/src/index.ts +109 -0
- package/src/install.ts +59 -0
- package/src/mytickets/api.ts +226 -0
- package/src/mytickets/format.ts +191 -0
- package/src/mytickets/sanitize.ts +221 -0
- package/src/mytickets/strings.ts +217 -0
- package/src/mytickets/toolbar.ts +48 -0
- package/src/reporter.ts +53 -0
- package/src/screenshot.ts +238 -0
- package/src/signature.ts +40 -0
- package/src/styles.css +400 -0
- package/src/submit.ts +143 -0
- package/src/types.ts +169 -0
- package/src/ui/annotator.ts +698 -0
- package/src/ui/arrow.ts +62 -0
- package/src/ui/element.ts +362 -0
- package/src/ui/strings.ts +113 -0
- package/src/ui/styles.ts +96 -0
- 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.
|
package/dist/budget.d.ts
ADDED
|
@@ -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;
|
package/dist/bundle.d.ts
ADDED
|
@@ -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 @@
|
|
|
1
|
+
export declare function installCrashWatcher(): 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;
|
package/dist/config.d.ts
ADDED
|
@@ -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 {};
|