@lightworkai.official/debug-capture 0.19.7 → 0.19.9

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.
@@ -1,3 +1,4 @@
1
+ import { type PictureEncoder } from "./replayPictures";
1
2
  import type { ReplayCapture, ReplayEvent, ReplaySummary } from "./types";
2
3
  /** The name the desk looks for. Changing it here changes it nowhere else. */
3
4
  export declare const REPLAY_FILENAME = "session-replay.json.gz";
@@ -6,6 +7,12 @@ export interface PackedReplay {
6
7
  filename: string;
7
8
  summary: ReplaySummary;
8
9
  }
10
+ export interface PackOptions {
11
+ /** JPEG quality for a canvas picture the browser could only make as PNG. Default 0.5. */
12
+ quality?: number;
13
+ /** How such a picture is made lighter. Default the browser's own encoder; null skips the step. */
14
+ lighten?: PictureEncoder | null;
15
+ }
9
16
  /** Is there something to attach, before the work of packing it. */
10
17
  export declare function hasReplayEvents(replay: ReplayCapture | null | undefined): replay is ReplayCapture;
11
18
  /**
@@ -14,9 +21,55 @@ export declare function hasReplayEvents(replay: ReplayCapture | null | undefined
14
21
  * The cap is applied to what is SENT — compressed bytes when compression is
15
22
  * available — because that is the number the server's per-file limit is
16
23
  * measured against.
24
+ *
25
+ * Over the cap, a report still carries a recording. Each step below is taken
26
+ * only when the one before it did not fit, and each gives up less than the
27
+ * next:
28
+ *
29
+ * 1. Safari's PNG canvas pictures saved again as JPEG (replayPictures) —
30
+ * nothing visible is lost.
31
+ * 2. Canvas pictures kept from the latest moment that fits, the earlier
32
+ * ones left out: the document as it last looked, over the whole
33
+ * recording. A canvas whose picture went plays blank until it is next
34
+ * drawn.
35
+ * 3. Every canvas picture left out: the canvases stay, blank; every
36
+ * click, page and field is still there.
37
+ * 4. Large pictures embedded in the page itself (a `data:` image, a font
38
+ * inlined in a stylesheet) swapped for an empty one.
39
+ * 5. The oldest part left out: the recording is kept in stretches that
40
+ * each begin with a full picture of the page (capture/replay — a
41
+ * checkout every half of the configured length), so the latest
42
+ * stretches play on their own. As many as fit, newest first.
43
+ * 6. The latest stretch from its start, as far as fits — it then stops
44
+ * before the report, and the ticket says by how much.
45
+ * 7. Long text and styles shortened, when not even the page's own picture
46
+ * fits.
47
+ *
48
+ * Only a page whose single picture is over the cap after all of that goes
49
+ * without — millions of elements. CIB-5880 (2026-11-02) went without at
50
+ * step 3: the strip missed every canvas added after the snapshot, and
51
+ * Safari's pictures are PNG.
17
52
  */
18
- export declare function packReplay(replay: ReplayCapture | null | undefined, maxBytes: number): Promise<PackedReplay | {
53
+ export declare function packReplay(replay: ReplayCapture | null | undefined, maxBytes: number, options?: PackOptions): Promise<PackedReplay | {
19
54
  summary: ReplaySummary;
20
55
  }>;
21
- /** The same replay with every canvas picture removed — the canvases stay, blank. */
22
- export declare function stripCanvas(events: readonly ReplayEvent[]): ReplayEvent[];
56
+ /**
57
+ * Every canvas picture removed, in place — the canvases stay, blank. The
58
+ * snapshot's and those on a canvas added later alike: missing the second
59
+ * kind is how CIB-5749 went out marked `canvasDropped` with 1.05 MB of
60
+ * pictures on fourteen canvases still in it. Canvas frames are dropped by
61
+ * the caller (`isCanvasFrame`). Whether there was anything to remove.
62
+ */
63
+ export declare function stripCanvas(events: readonly ReplayEvent[]): boolean;
64
+ /**
65
+ * Large pictures the page itself holds — an `<img src="data:…">`, a font or
66
+ * background inlined in a stylesheet — swapped for an empty one, in place,
67
+ * wherever the events carry them: attributes, styles, style rules.
68
+ */
69
+ export declare function dropEmbedded(events: readonly ReplayEvent[]): boolean;
70
+ /**
71
+ * The recording in its stretches, each from a Meta and full snapshot to the
72
+ * next — the units it can be shortened by and still play. Anything before
73
+ * the first Meta rides with the first stretch.
74
+ */
75
+ export declare function splitWindows(events: readonly ReplayEvent[]): ReplayEvent[][];
@@ -0,0 +1,33 @@
1
+ import type { ReplayEvent } from "./types";
2
+ export interface Picture {
3
+ base64: string;
4
+ type: string;
5
+ }
6
+ /** A picture as JPEG, or null to keep it as it is. */
7
+ export type PictureEncoder = (picture: Picture) => Promise<Picture | null>;
8
+ /** One place a picture is kept, and how to put a new one there. */
9
+ interface Slot {
10
+ picture: Picture;
11
+ put: (next: Picture) => void;
12
+ }
13
+ /** Every canvas picture in the events, wherever the recorder put it. */
14
+ export declare function pictureSlots(events: readonly ReplayEvent[]): Slot[];
15
+ /** Every picture's base64, as often as it occurs — gzip's 32 KB window never sees a picture's earlier copy. */
16
+ export declare function pictureStrings(events: readonly ReplayEvent[]): string[];
17
+ /** Whether there is anything for `lightenPictures` to do — before the work of decoding. */
18
+ export declare function hasHeavyPngs(events: readonly ReplayEvent[]): boolean;
19
+ /**
20
+ * Each large PNG replaced by what `encode` makes of it, when that is smaller.
21
+ * The same picture is encoded once however often it recurs — a page's
22
+ * picture is in every snapshot the recording holds. Returns how many changed.
23
+ * A picture the encoder fails on stays as it was: this never throws.
24
+ */
25
+ export declare function lightenPictures(events: ReplayEvent[], encode: PictureEncoder): Promise<number>;
26
+ /**
27
+ * The browser's own encoder: the PNG decoded, checked opaque pixel by pixel,
28
+ * and saved as JPEG at `quality`. Null where the browser lacks what it takes
29
+ * (no `createImageBitmap`), for a picture anything shows through, and for a
30
+ * browser that hands back something other than a JPEG.
31
+ */
32
+ export declare function browserJpeg(quality: number): PictureEncoder | null;
33
+ export {};
package/dist/types.d.ts CHANGED
@@ -189,7 +189,7 @@ export interface ReplayCapture {
189
189
  * attachment loads — or say why there is none.
190
190
  */
191
191
  export interface ReplaySummary {
192
- /** Whole seconds covered, when one was attached. */
192
+ /** Whole seconds covered by what was attached (or, with `omitted`, by what was recorded). */
193
193
  seconds?: number;
194
194
  events?: number;
195
195
  startedAt?: string;
@@ -197,8 +197,16 @@ export interface ReplaySummary {
197
197
  /** Size of the attachment as sent, and whether it is gzip. */
198
198
  bytes?: number;
199
199
  compressed?: boolean;
200
- /** The canvas frames were left out to fit under the size cap. */
200
+ /** Canvas pictures left out: the canvases play blank. */
201
201
  canvasDropped?: boolean;
202
+ /** Large pictures embedded in the page (a `data:` image, an inlined font) left out. */
203
+ picturesDropped?: boolean;
204
+ /** Long text and styles shortened — only when not even one picture of the page fitted otherwise. */
205
+ simplified?: boolean;
206
+ /** Only part of the recording went: how long the whole was. `seconds` is what was sent. */
207
+ recordedSeconds?: number;
208
+ /** The part sent stops this many seconds before the report was made. */
209
+ endsEarlySeconds?: number;
202
210
  /** Present when the host records but this report carries no replay. */
203
211
  omitted?: "too-large" | "empty";
204
212
  }
@@ -14,13 +14,14 @@
14
14
  * Content that scrolls under is not: a floating button over content is what a floating button is. A host may name
15
15
  * more (`chat.avoid: "<selector>"`) or turn it off (`false`).
16
16
  *
17
- * THE NEAREST FREE PLACE. Up or down its own side first; the other side when that is nearer than a long climb
18
- * (`SWITCH_COST`) — a column the height of the window sends it to the other corner, a FAB lifts it above. When a
19
- * nearer place frees up it goes back (`settleSpot`): the tray shut, it is above the toggle again. 0.19.6 stayed
20
- * where it had stepped to for the rest of the page, which on the real เอกสาร page left it high on the right over
21
- * the viewer's ยังไม่มีการวางตรา once the tray was shut. A shift of less than STAY_MARGIN does not move it — no
22
- * jitter as the page settles.
23
- *
17
+ * THE NEAREST FREE PLACE (`placeSpot`). What a place costs is the climb plus the move in from its edge, and
18
+ * SWITCH_COST more on the other side. Up or down its own column; or a column further in, just past what stands in
19
+ * the way; or the other corner. A FAB lifts it above. A toolbar column as tall as the window puts it just inside the
20
+ * toolbar, at the same height. 0.19.7 only knew the two corner columns, so on a short window, with the ERP's tray
21
+ * filling the right edge and its sidebar the left, it found no free place and stayed on the tray (the owner's
22
+ * screenshot, 11-02). When a nearer place frees up it goes back: the tray shut, it is home again. A shift of less than
23
+ * STAY_MARGIN does not move it, so it does not jitter as the page settles.
24
+
24
25
  * A PAGE'S OWN PLACE. The desk's rules (ปุ่มแชท › ตำแหน่งปุ่ม): pages whose path starts so — hidden there, or
25
26
  * started in a corner at a height. The most specific rule wins; a person who moved it keeps their place.
26
27
  *
@@ -51,31 +52,28 @@ export declare function blockedRanges(obstacles: readonly Rect[], col: {
51
52
  }, viewH: number, size: number): [number, number][];
52
53
  /** The free height nearest `want` within [min, max], outside every range (open ends) — null when none. Up wins a tie. Pure. */
53
54
  export declare function nearestFreeY(want: number, ranges: readonly [number, number][], min: number, max: number): number | null;
54
- /**
55
- * Where the bubble stands: `want` on its side when free, else the nearest free height there — or the other side's when
56
- * that is cheaper (SWITCH_COST plus its own move). Nothing free anywhere: `want`, as it was. Pure.
57
- */
58
- export declare function chooseSpot(want: {
59
- side: Side;
60
- y: number;
61
- }, free: Record<Side, readonly [number, number][]>, min: number, max: number): {
55
+ export interface Spot {
62
56
  side: Side;
57
+ /** From its side's edge, px. */
58
+ x: number;
59
+ /** Up from the bottom, px. */
63
60
  y: number;
64
- };
61
+ }
62
+ /** The page's boxes across the bubble's column at `x` from `side`'s edge — read off the page, or given in the tests. */
63
+ export type ReadColumn = (side: Side, x: number) => readonly Rect[];
64
+ /** How many columns further in it looks on a side, past what stands in the way. */
65
+ export declare const MAX_SHIFTS = 3;
65
66
  /**
66
- * Where it stands now, given where it stood: the best place (`chooseSpot`), unless where it stands is still free and
67
- * within STAY_MARGIN of it (`prev` is null when the wish, the page or the person's own place changed). Pure.
67
+ * Where the bubble stands: `want` when it is free there; else the cheapest free place — up or down its column, a column
68
+ * further in just past what stands in the way (a toolbar column as tall as the window: just left of it, at the same
69
+ * height), or the other side's (SWITCH_COST more). What a place costs is the climb plus the move in. A place it already
70
+ * stands in (`prev`), still free and within STAY_MARGIN of the best, is kept — no jitter as the page settles. Columns
71
+ * are read only as needed (`read`), and never past the middle of the window. Nothing free anywhere: `want`. Pure.
68
72
  */
69
- export declare function settleSpot(prev: {
70
- side: Side;
71
- y: number;
72
- } | null, want: {
73
- side: Side;
74
- y: number;
75
- }, free: Record<Side, readonly [number, number][]>, min: number, max: number): {
76
- side: Side;
77
- y: number;
78
- };
73
+ export declare function placeSpot(want: Spot, read: ReadColumn, view: {
74
+ w: number;
75
+ h: number;
76
+ }, size: number, min: number, max: number, prev?: Spot | null): Spot;
79
77
  export interface PlaceRule {
80
78
  /** "/saraban/edoc-management" — the page and every page under it; `*` one segment, `**` any. */
81
79
  path: string;
@@ -93,7 +91,9 @@ export declare function placeFor(pathname: string, rules: readonly PlaceRule[] |
93
91
  * - a host's selector (`extra`): what it names, whole, wherever it is;
94
92
  * - in a small floating box (its nearest `fixed`, or `sticky` stuck at its edge): what shows there, by its own box —
95
93
  * the control it is part of, else itself;
96
- * - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller;
94
+ * - in a box as big as the window, or on a page that does not scroll: a control not inside a scroller — a disabled one
95
+ * too, found by its own selector, as no hit test reaches it (the ERP's ผลการจำแนก panel on a phone, 11-02: its
96
+ * greyed มอบหมายผู้ดำเนินการ was under the bubble);
97
97
  * - anything else scrolls with the page, and is not.
98
98
  */
99
99
  export declare function scanColumns(cols: readonly {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightworkai.official/debug-capture",
3
- "version": "0.19.7",
3
+ "version": "0.19.9",
4
4
  "description": "Report-a-problem widget: rolling network/console/action capture, optional rrweb session replay, annotated screenshot, and submission to a Lightwork Support host",
5
5
  "type": "module",
6
6
  "module": "./dist/index.mjs",