@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
@@ -0,0 +1,115 @@
1
+ /** A board column as the realm configured it. Only public lanes are returned. */
2
+ export interface StatusColumn {
3
+ key: string;
4
+ label: string;
5
+ description: string;
6
+ color: string;
7
+ bucket: "active" | "closed";
8
+ awaitingReply: boolean;
9
+ }
10
+ export interface RealmConfig {
11
+ realm: {
12
+ id: string;
13
+ slug: string;
14
+ name: string;
15
+ };
16
+ timezone: string;
17
+ statusColumns: StatusColumn[];
18
+ featureFlags: {
19
+ selfServiceReopen?: boolean;
20
+ attachments?: boolean;
21
+ conversation?: boolean;
22
+ };
23
+ locale: {
24
+ default: string;
25
+ labels: Record<string, string>;
26
+ };
27
+ }
28
+ export interface TicketMessage {
29
+ id: string;
30
+ authorId: string | null;
31
+ authorName: string | null;
32
+ authorRole: "REPORTER" | "AGENT";
33
+ body: string;
34
+ createdAt: string;
35
+ editedAt: string | null;
36
+ }
37
+ export interface TicketAttachment {
38
+ id: string;
39
+ objectKey: string;
40
+ filename: string | null;
41
+ contentType: string | null;
42
+ sizeBytes: number | null;
43
+ }
44
+ export interface MyTicket {
45
+ id: string;
46
+ number: number;
47
+ title: string;
48
+ description: string | null;
49
+ category: string | null;
50
+ routeUrl: string | null;
51
+ status: string;
52
+ priority: string;
53
+ resolutionNote: string | null;
54
+ reopened: boolean;
55
+ createdAt: string;
56
+ updatedAt: string;
57
+ }
58
+ export interface MyTicketListItem extends MyTicket {
59
+ messageCount: number;
60
+ /** How many replies came from the team — "has anyone answered me yet". */
61
+ agentReplyCount: number;
62
+ lastMessage: {
63
+ at: string;
64
+ authorRole: "REPORTER" | "AGENT";
65
+ preview: string;
66
+ } | null;
67
+ }
68
+ export interface MyTicketDetail extends MyTicket {
69
+ messages: TicketMessage[];
70
+ attachments: TicketAttachment[];
71
+ }
72
+ /** One recorded status change. Internal lanes are already resolved server-side. */
73
+ export interface StatusEvent {
74
+ id: string;
75
+ fromStatus: string | null;
76
+ toStatus: string;
77
+ via: string;
78
+ createdAt: string;
79
+ }
80
+ /** Thrown with the server's own words, so a panel can say what went wrong. */
81
+ export declare class TicketApiError extends Error {
82
+ readonly status: number;
83
+ constructor(message: string, status: number);
84
+ }
85
+ /** Raised when there is no signed-in reporter to ask about. */
86
+ export declare class NoIdentityError extends Error {
87
+ constructor();
88
+ }
89
+ export declare function listMyTickets(): Promise<MyTicketListItem[]>;
90
+ export declare function getMyTicket(id: string): Promise<MyTicketDetail>;
91
+ export declare function getMyTicketByNumber(num: number): Promise<MyTicketDetail>;
92
+ /**
93
+ * A short-lived URL for a file on one's own ticket.
94
+ *
95
+ * Fetched rather than pointed at: the endpoint needs the realm key and the
96
+ * identity token, and an `<img>` or an `<a download>` cannot send a header.
97
+ */
98
+ export declare function getAttachmentUrl(attachmentId: string): Promise<{
99
+ url: string;
100
+ filename: string | null;
101
+ }>;
102
+ export declare function getMyTicketHistory(id: string): Promise<StatusEvent[]>;
103
+ export declare function replyToTicket(id: string, body: string): Promise<TicketMessage>;
104
+ /**
105
+ * Rewrite one's own reply.
106
+ *
107
+ * The server refuses any message the caller did not write — authorship, not
108
+ * role — so this cannot touch the team's side of the thread.
109
+ */
110
+ export declare function editMyMessage(ticketId: string, messageId: string, body: string): Promise<TicketMessage>;
111
+ export declare function reopenMyTicket(id: string, note?: string): Promise<MyTicket>;
112
+ export declare function uploadInlineImage(id: string, dataUri: string, filename?: string): Promise<{
113
+ url: string;
114
+ }>;
115
+ export declare function fetchRealmConfig(): Promise<RealmConfig>;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * The panel's decisions that are not DOM: how a ticket is named, dated, sorted,
3
+ * searched and described. Pure, so they can be tested without a browser — which
4
+ * matters here, because the DOM around them cannot be.
5
+ */
6
+ import type { MyTicketListItem, StatusColumn } from "./api";
7
+ /**
8
+ * `ROOT-1099`, from the realm's slug — the same key the board and the admin app
9
+ * print, so a reporter quoting a number is quoting the one support will search
10
+ * for. A realm with no usable slug falls back to `#1099` rather than inventing
11
+ * a prefix.
12
+ */
13
+ export declare function ticketKey(slug: string | undefined, number: number): string;
14
+ /**
15
+ * `24 พ.ค. 2568 13:00` — the original's `DD MMM BBBB HH:mm`, from the platform's
16
+ * own Intl rather than a date library this package will not depend on.
17
+ *
18
+ * Formatted in the REALM's timezone, not the reader's. A support desk in
19
+ * Bangkok and a reporter travelling in Berlin must not disagree about which day
20
+ * a ticket was filed, and the realm is the side that also runs the "today"
21
+ * filters.
22
+ */
23
+ export declare function formatDateTime(iso: string | null | undefined, timezone: string, locale: "th" | "en" | undefined): string;
24
+ /** Lookups over the realm's configured lanes, with sane answers for a key that isn't there. */
25
+ export interface StatusMaps {
26
+ labelOf: (key: string) => string;
27
+ columnOf: (key: string) => StatusColumn | undefined;
28
+ isAwaitingReply: (key: string) => boolean;
29
+ isClosed: (key: string) => boolean;
30
+ /** Board order, for sorting a column of statuses the way the board reads. */
31
+ rankOf: (key: string) => number;
32
+ options: {
33
+ value: string;
34
+ label: string;
35
+ }[];
36
+ }
37
+ export declare function statusMaps(columns: StatusColumn[], allLabel: string): StatusMaps;
38
+ /** Has the team written a solution note? */
39
+ export declare function hasSolution(t: MyTicketListItem): boolean;
40
+ /** Has anyone from the team replied at all? */
41
+ export declare function hasTeamReply(t: MyTicketListItem): boolean;
42
+ /** Solution beats a reply beats silence — the order the column sorts in. */
43
+ export declare function responseRank(t: MyTicketListItem): number;
44
+ export type SortKey = "number" | "title" | "module" | "status" | "response" | "createdAt" | "updatedAt";
45
+ export interface Sort {
46
+ key: SortKey;
47
+ direction: "asc" | "desc";
48
+ }
49
+ /**
50
+ * Filter by what the reporter SEES.
51
+ *
52
+ * The haystack is the rendered row — key, title, module, status label — because
53
+ * that is what someone searching has in front of them. Searching the raw status
54
+ * key would match `IN_PROGRESS` for a lane the reader only ever saw called
55
+ * กำลังดำเนินการ.
56
+ */
57
+ export declare function matchesKeyword(t: MyTicketListItem, keyword: string, slug: string | undefined, labelOf: (key: string) => string): boolean;
58
+ /**
59
+ * Comparators for the orders a raw value compare gets wrong: status follows the
60
+ * board rather than the alphabet, dates are chronological rather than
61
+ * lexicographic, and the response column ranks by a signal that is not one field.
62
+ */
63
+ export declare function compareBy(sort: Sort, maps: StatusMaps, slug: string | undefined): (a: MyTicketListItem, b: MyTicketListItem) => number;
64
+ /** Rows per page — the original's `defaultPageSize`. */
65
+ export declare const PAGE_SIZE = 10;
66
+ export declare function pageOf<T>(rows: T[], page: number, size?: number): T[];
67
+ export declare function pageCount(total: number, size?: number): number;
68
+ /**
69
+ * The same ten colour tokens the board uses, as concrete pairs.
70
+ *
71
+ * A status arrives as DATA — a realm adds lanes whenever it likes — so a pill is
72
+ * coloured with an inline style from this map rather than a class a stylesheet
73
+ * could never know the name of. Keeping the values identical
74
+ * to the app's own STATUS_TONES is the point: a reporter and an agent looking at
75
+ * the same ticket should see the same colour for it.
76
+ */
77
+ export declare const STATUS_TONES: Record<string, {
78
+ fg: string;
79
+ bg: string;
80
+ }>;
81
+ export declare function statusTone(token: string | undefined): {
82
+ fg: string;
83
+ bg: string;
84
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Rendering a ticket body inside somebody else's app.
3
+ *
4
+ * Bodies are HTML — the composer's formatting and its inline screenshots are the
5
+ * point of the thread, so stripping to text would lose what a reporter actually
6
+ * came to read. But that HTML was written by other people, and this package
7
+ * renders it inside a HOST application's page. Markup that ran there would run
8
+ * with the host's origin, against the host's session. A support widget must not
9
+ * be the way an app gets XSS.
10
+ *
11
+ * The support app solves this with DOMPurify. A package that every consumer
12
+ * installs cannot take that dependency for one screen, so this is the same
13
+ * POLICY over the platform's own parser: parse the string with DOMParser (never
14
+ * a regex), walk the result, and delete every node and attribute not on the
15
+ * list. Anything unrecognised is removed rather than escaped, and the walk is
16
+ * over an inert document, so nothing loads or executes while it happens.
17
+ */
18
+ /** Tags an ordinary note actually uses. Everything else goes. */
19
+ export declare const ALLOWED_TAGS: Set<string>;
20
+ export declare const ALLOWED_ATTR: Set<string>;
21
+ /** Links: ordinary web destinations only. No javascript:, no data:. */
22
+ export declare function isAllowedHref(url: string): boolean;
23
+ /**
24
+ * Image sources: OUR inline images and nothing else.
25
+ *
26
+ * Not a tightening for its own sake. Any other src turns every ticket into a
27
+ * tracking pixel aimed at whoever opens it — a body can be written by a stranger
28
+ * who filed a report, and the reader here is the reporter, on the host's page.
29
+ * Accepts the server-relative form and the same path already made absolute
30
+ * against the configured host.
31
+ */
32
+ export declare function isAllowedImageSrc(url: string, host: string): boolean;
33
+ export declare function looksLikeHtml(body: string): boolean;
34
+ /** Tags out, line structure kept — for previews and anywhere text is all that fits. */
35
+ export declare function toPlainText(body: string): string;
36
+ /**
37
+ * Fill `target` with a safe rendering of `body`.
38
+ *
39
+ * A node rather than a string, deliberately: handing back HTML invites a caller
40
+ * to concatenate it with something else and re-parse the result, which is where
41
+ * sanitisers stop holding.
42
+ */
43
+ export declare function renderBody(target: HTMLElement, body: string, host: string): void;
44
+ /**
45
+ * The same walk, but returning a string — for content going OUT.
46
+ *
47
+ * `renderBody` deliberately hands back a node, because handing back HTML invites
48
+ * a caller to concatenate and re-parse it. This one exists for the single case
49
+ * where a string is the only possible answer: the composer has to put what
50
+ * somebody typed into a JSON body.
51
+ *
52
+ * It matters for the same reason the display side does, from the other
53
+ * direction. A contenteditable takes whatever the clipboard had in it — a
54
+ * pasted block of a web page arrives with its scripts, its styles and its
55
+ * tracking pixels intact — and posting that unfiltered would store it for
56
+ * every future reader of the thread.
57
+ */
58
+ export declare function cleanHtml(html: string, host: string): string;
59
+ /**
60
+ * True when there is nothing worth sending.
61
+ *
62
+ * An image alone counts as content — a screenshot with no words is the most
63
+ * common reply a reporter makes — but a contenteditable that has been typed in
64
+ * and cleared is full of empty tags and `&nbsp;`, and those are not.
65
+ */
66
+ export declare function htmlIsEmpty(html: string): boolean;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The tickets panel's own words, kept apart from the reporter dialog's so
3
+ * neither grows the other's vocabulary. Thai wording is the original's, verbatim
4
+ * where the original had one.
5
+ */
6
+ export interface TicketStrings {
7
+ title: string;
8
+ intro: string;
9
+ searchPlaceholder: string;
10
+ searchLabel: string;
11
+ allStatuses: string;
12
+ count: (n: number) => string;
13
+ colNumber: string;
14
+ colTitle: string;
15
+ colModule: string;
16
+ colStatus: string;
17
+ colResponse: string;
18
+ colCreated: string;
19
+ colUpdated: string;
20
+ awaitingReply: string;
21
+ hasSolution: string;
22
+ hasReply: string;
23
+ noReply: string;
24
+ empty: string;
25
+ emptyFiltered: string;
26
+ loading: string;
27
+ listFailed: string;
28
+ detailFailed: string;
29
+ signedOut: string;
30
+ back: string;
31
+ reportedBy: string;
32
+ conversation: string;
33
+ resolution: string;
34
+ noMessages: string;
35
+ you: string;
36
+ team: string;
37
+ /** The role chip beside a reporter's name, as the original prints it. */
38
+ reporterRole: string;
39
+ edited: string;
40
+ edit: string;
41
+ saveEdit: string;
42
+ cancel: string;
43
+ replyPlaceholder: string;
44
+ bold: string;
45
+ italic: string;
46
+ underline: string;
47
+ strike: string;
48
+ code: string;
49
+ blockquote: string;
50
+ bulletList: string;
51
+ orderedList: string;
52
+ link: string;
53
+ linkPrompt: string;
54
+ linkNeedsSelection: string;
55
+ linkInvalid: string;
56
+ attachImage: string;
57
+ uploading: string;
58
+ send: string;
59
+ sending: string;
60
+ sent: string;
61
+ sendFailed: string;
62
+ reopen: string;
63
+ reopenHint: string;
64
+ reopening: string;
65
+ reopened: string;
66
+ reopenFailed: string;
67
+ close: string;
68
+ retry: string;
69
+ page: string;
70
+ of: (page: number, pages: number) => string;
71
+ prev: string;
72
+ next: string;
73
+ }
74
+ export declare function ticketStrings(locale: "th" | "en" | undefined): TicketStrings;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Which controls a rich composer offers.
3
+ *
4
+ * A list rather than a fixed set, because the original itself has two —
5
+ * `NOTE_TOOLBAR` for replies and a longer one for articles — and the support app's
6
+ * admin composer wants a few more than a reporter's does. The alternative was a
7
+ * second editor component, which is the thing this package exists to stop.
8
+ */
9
+ export type EditorTool = "bold" | "italic" | "underline" | "strike" | "code" | "bulletList" | "orderedList" | "blockquote" | "link" | "image";
10
+ /** The original's own NOTE_TOOLBAR — what a reporter replying to a ticket gets. */
11
+ export declare const NOTE_TOOLBAR: EditorTool[];
12
+ /**
13
+ * Everything, for a staff composer. Adds the three the admin side had grown on
14
+ * its own — strike, code and blockquote — so adopting this component costs the
15
+ * app nothing it already had.
16
+ */
17
+ export declare const FULL_TOOLBAR: EditorTool[];
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The bus between "something happened" and "the reporter opens".
3
+ *
4
+ * Replaces the original's zustand store. Not because zustand is bad, but
5
+ * because it is React's: this package is consumed by Vue and Angular apps too,
6
+ * and a state library in the core would be a framework choice imposed on all of
7
+ * them for the sake of two callbacks and a Set.
8
+ *
9
+ * `launch` must be called INSIDE the user's gesture. Screen capture asks the
10
+ * browser for permission, and a browser only grants that to a real click — an
11
+ * await before it and the request is refused with no useful error.
12
+ */
13
+ import type { DebugReason } from "./types";
14
+ type Listener = (reason: DebugReason) => void;
15
+ /** The reporter UI subscribes here. */
16
+ export declare function onLaunch(fn: Listener): () => void;
17
+ /** Open the reporter. Call from inside a click handler — see the note above. */
18
+ export declare function launch(reason?: DebugReason): void;
19
+ /**
20
+ * A crash was caught. Deliberately NOT the same channel as `launch`: opening a
21
+ * screen-capture prompt on its own, with no gesture behind it, is both refused
22
+ * by the browser and rude. A listener turns this into an invitation — a toast
23
+ * with a button — and that button calls `launch`.
24
+ */
25
+ export declare function onCrash(fn: Listener): () => void;
26
+ export declare function emitCrash(reason: DebugReason): void;
27
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { ScreenshotMethod } from './types';
2
+ export interface ScreenshotResult {
3
+ dataUrl: string | null;
4
+ method: ScreenshotMethod;
5
+ }
6
+ export declare function captureScreen(): Promise<ScreenshotResult>;
7
+ export declare function compressScreenshot(dataUrl: string): Promise<string>;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The dedup key: the failed API call that most likely caused the crash.
3
+ *
4
+ * `METHOD /path/:id STATUS`, e.g. `PUT /documents/:id 500`. Two users hitting
5
+ * the same broken endpoint produce the same key, so the backend merges their
6
+ * reports into one parent with duplicates under it — which is what stops an
7
+ * outage from arriving as forty separate tickets.
8
+ *
9
+ * The only normalisation is templating record ids out of the URL PATH, which is
10
+ * bounded, like a route pattern. No fuzzy text matching: a signature that merges
11
+ * two unrelated failures is worse than no signature, because the second report
12
+ * disappears under the first and nobody reads it.
13
+ *
14
+ * Returns null for a manual report or when there is no clear failure. We never
15
+ * guess — an unmerged ticket is a small cost, a wrongly merged one is a lost bug.
16
+ */
17
+ import type { DebugBundle } from "./types";
18
+ export declare function errorSignature(bundle: DebugBundle): string | null;
@@ -0,0 +1,8 @@
1
+ import type { DebugBundle } from "./types";
2
+ export interface FiledTicket {
3
+ id: string;
4
+ number: number;
5
+ status: string;
6
+ }
7
+ export declare function toBase64(bytes: Uint8Array): string;
8
+ export declare function submitBundle(title: string, bundle: DebugBundle): Promise<FiledTicket>;
@@ -0,0 +1,175 @@
1
+ /**
2
+ * The shape of a debug bundle.
3
+ *
4
+ * A bundle is a redacted, client-side snapshot of what the user was doing when
5
+ * something went wrong — recent network requests, console output, the trail of
6
+ * actions that led there, page/session context, a cause summary, and a
7
+ * screenshot. It is assembled entirely in the browser so support can reproduce
8
+ * an issue without a round of "what were you clicking".
9
+ *
10
+ * What changed on the way out of the original in-app reporter: anything that
11
+ * described ONE app — a Thai unit hierarchy, a permission-bypass dev flag, a
12
+ * fixed module vocabulary — is gone. Those arrive through config now, because a
13
+ * library that ships another company's org chart is not a library.
14
+ */
15
+ export type CaptureVia = "fetch" | "xhr";
16
+ export interface CapturedRequest {
17
+ id: string;
18
+ /** ISO timestamp when the request started. */
19
+ startedAt: string;
20
+ method: string;
21
+ /** Redacted URL (sensitive query params masked). */
22
+ url: string;
23
+ /** Redacted request headers. */
24
+ requestHeaders: Record<string, string>;
25
+ /** Redacted + truncated request body (JSON string / marker / null). */
26
+ requestBody: string | null;
27
+ status: number | null;
28
+ statusText: string | null;
29
+ /** Redacted response headers. */
30
+ responseHeaders?: Record<string, string>;
31
+ /**
32
+ * Redacted response body, in full up to a per-response cap — not a snippet.
33
+ * Only captured for origins the consumer allowed: see `capture.bodyOrigins`.
34
+ */
35
+ responseSnippet?: string | null;
36
+ durationMs: number | null;
37
+ /** Populated when the request rejected (network error / abort). */
38
+ error?: string;
39
+ via: CaptureVia;
40
+ }
41
+ export type ConsoleLevel = "log" | "info" | "warn" | "error" | "debug";
42
+ export type ConsoleEntryKind = ConsoleLevel | "exception" | "unhandledrejection";
43
+ export interface CapturedConsoleEntry {
44
+ id: string;
45
+ at: string;
46
+ level: ConsoleEntryKind;
47
+ /** Human-readable, truncated message. */
48
+ message: string;
49
+ }
50
+ export type ActionKind = "click" | "input" | "navigate" | "submit" | "ui";
51
+ export interface ActionTrailEntry {
52
+ id: string;
53
+ at: string;
54
+ kind: ActionKind;
55
+ /** Human-readable description, e.g. `click ปุ่ม "บันทึกร่าง"`. */
56
+ detail: string;
57
+ /** Consecutive identical actions are collapsed; this is how many. */
58
+ count?: number;
59
+ /** Where a click leads — from the link's href or the nearest click target. */
60
+ source?: string;
61
+ /**
62
+ * A small, sanitized+redacted `outerHTML` of the clicked element, so a reader
63
+ * can see the ACTUAL control rather than a text label. Omitted when too large.
64
+ *
65
+ * MUST be re-sanitized before rendering. It is attacker-influenced markup that
66
+ * has travelled between origins.
67
+ */
68
+ html?: string;
69
+ /**
70
+ * The same control a moment later, once it visibly changed — disabled, busy,
71
+ * relabelled, or gone. A button's resting state says what was pressed; this
72
+ * says what pressing it DID, which is the half that was missing.
73
+ */
74
+ afterHtml?: string;
75
+ /** One-line summary of that change, present even if `afterHtml` was dropped. */
76
+ afterNote?: string;
77
+ }
78
+ export type DebugReasonType = "manual" | "runtime-error" | "unhandledrejection" | "framework-error";
79
+ export interface DebugReason {
80
+ type: DebugReasonType;
81
+ message?: string;
82
+ stack?: string;
83
+ }
84
+ /** A failed data-layer query the host chose to report (react-query, Apollo, …). */
85
+ export interface ErroredQuery {
86
+ key: string;
87
+ error: string;
88
+ }
89
+ export interface FailedRequestSummary {
90
+ method: string;
91
+ url: string;
92
+ status: number | null;
93
+ error?: string;
94
+ at: string;
95
+ }
96
+ /** The "smoking gun" — surfaced at the top of the report. */
97
+ export interface CauseSummary {
98
+ failedRequests: FailedRequestSummary[];
99
+ errors: string[];
100
+ erroredQueries: ErroredQuery[];
101
+ }
102
+ /**
103
+ * Who was using the app. Every field optional: the library cannot know what a
104
+ * host knows about its users, and a half-filled identity is worth more than a
105
+ * shape the host has to lie to satisfy.
106
+ */
107
+ export interface DebugUser {
108
+ id?: string | null;
109
+ email?: string | null;
110
+ fullName?: string | null;
111
+ /** Department / unit / team — whatever the host calls the thing. */
112
+ unit?: string | null;
113
+ roles?: string[];
114
+ }
115
+ export interface DebugContext {
116
+ capturedAt: string;
117
+ route: {
118
+ href: string;
119
+ pathname: string;
120
+ search: string;
121
+ };
122
+ /** Which part of the app this was, when the host declares one. */
123
+ module: {
124
+ key: string;
125
+ label: string;
126
+ } | null;
127
+ user: DebugUser;
128
+ /** Host-supplied request headers, redacted. Acting-as context and the like. */
129
+ apiHeaders: Record<string, string>;
130
+ app: {
131
+ name: string;
132
+ version: string;
133
+ environment: string;
134
+ };
135
+ client: {
136
+ userAgent: string;
137
+ language: string;
138
+ platform: string;
139
+ online: boolean;
140
+ viewport: {
141
+ width: number;
142
+ height: number;
143
+ dpr: number;
144
+ };
145
+ screen: {
146
+ width: number;
147
+ height: number;
148
+ };
149
+ };
150
+ performance: {
151
+ memoryUsedMb: number | null;
152
+ memoryLimitMb: number | null;
153
+ domContentLoadedMs: number | null;
154
+ loadEventMs: number | null;
155
+ };
156
+ /** Anything else the host wants on the record — feature flags, build id, … */
157
+ extra: Record<string, unknown>;
158
+ }
159
+ export type ScreenshotMethod = "display-media" | "html-to-image" | "none" | "cancelled";
160
+ export interface DebugBundle {
161
+ context: DebugContext;
162
+ reason: DebugReason;
163
+ note: string;
164
+ cause: CauseSummary;
165
+ actionTrail: ActionTrailEntry[];
166
+ network: CapturedRequest[];
167
+ console: CapturedConsoleEntry[];
168
+ /** PNG data URL, or null if capture failed / was skipped. */
169
+ screenshotDataUrl: string | null;
170
+ screenshotMethod: ScreenshotMethod;
171
+ meta: {
172
+ generatedBy: string;
173
+ schemaVersion: number;
174
+ };
175
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Draw on the screenshot before filing it.
3
+ *
4
+ * "The button on the right" is not a bug report; a red box around it is. This is
5
+ * the highest-value thing in the widget, because it turns a picture of a whole
6
+ * screen into a picture of the problem.
7
+ *
8
+ * The original does this with fabric.js and gets selection, handles and a delete
9
+ * control for free. This is plain canvas — fabric is ~290 kB, and a scene graph
10
+ * to draw four shapes is not a cost every host app should carry. What it must
11
+ * NOT be is a downgrade, so everything a person actually does to an annotation
12
+ * is here: draw, select, move, resize from any handle, delete from the ✕ on the
13
+ * selection, retype a label, clear.
14
+ *
15
+ * Rotation is the one thing fabric gives that this does not. Nobody rotates a
16
+ * box drawn around a broken button.
17
+ */
18
+ export type AnnotatorTool = "rect" | "arrow" | "highlight" | "text";
19
+ export interface Annotator {
20
+ setTool(tool: AnnotatorTool | null): void;
21
+ deleteSelected(): void;
22
+ clear(): void;
23
+ /** The annotated image at FULL resolution — never the on-screen scale. */
24
+ toDataUrl(): string;
25
+ hasShapes(): boolean;
26
+ hasSelection(): boolean;
27
+ destroy(): void;
28
+ }
29
+ export declare function createAnnotator(canvas: HTMLCanvasElement, image: HTMLImageElement,
30
+ /**
31
+ * The drawing or the active tool changed — the toolbar mirrors both, so it
32
+ * has to hear about a tool being consumed as well as a shape being added.
33
+ */
34
+ notify: () => void): Annotator;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Where an arrow's shaft stops and its head begins.
3
+ *
4
+ * The shaft used to run all the way to the tip with the head painted over it,
5
+ * which leaves a spur: the shaft has width, the head's base edge is angled, and
6
+ * the corners of a thick line poke out past that edge on one side. It reads as a
7
+ * notch in the arrowhead, and once seen it cannot be unseen.
8
+ *
9
+ * So the shaft stops at the head's base instead. Pure geometry, kept out of the
10
+ * canvas so it can be checked without one.
11
+ */
12
+ export interface Point {
13
+ x: number;
14
+ y: number;
15
+ }
16
+ export interface ArrowPath {
17
+ /** Null when the arrow is shorter than its own head — then it is head only. */
18
+ shaft: {
19
+ from: Point;
20
+ to: Point;
21
+ } | null;
22
+ /** Tip first, then the two base corners. */
23
+ head: [Point, Point, Point];
24
+ }
25
+ export declare function arrowPath(from: Point, to: Point, headLength: number): ArrowPath;
@@ -0,0 +1,9 @@
1
+ import type { ErroredQuery } from "../types";
2
+ export declare const REPORTER_TAG = "lw-debug-reporter";
3
+ export declare function setErroredQueriesSource(read: () => ErroredQuery[]): void;
4
+ /**
5
+ * Idempotent, and the ONLY thing that touches HTMLElement. A second app-wide
6
+ * mount must not throw on a taken tag name, and a server must be able to import
7
+ * this module without evaluating a browser class.
8
+ */
9
+ export declare function defineReporterElement(): void;