@tracelane/core 0.1.0-alpha.1
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/NOTICE +11 -0
- package/README.md +18 -0
- package/dist/browser-executor.d.ts +39 -0
- package/dist/browser-executor.d.ts.map +1 -0
- package/dist/browser-executor.js +2 -0
- package/dist/browser-executor.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/mode.d.ts +18 -0
- package/dist/mode.d.ts.map +1 -0
- package/dist/mode.js +26 -0
- package/dist/mode.js.map +1 -0
- package/dist/page-script.d.ts +44 -0
- package/dist/page-script.d.ts.map +1 -0
- package/dist/page-script.js +78 -0
- package/dist/page-script.js.map +1 -0
- package/dist/recorder.d.ts +68 -0
- package/dist/recorder.d.ts.map +1 -0
- package/dist/recorder.js +88 -0
- package/dist/recorder.js.map +1 -0
- package/dist/size-guard.d.ts +39 -0
- package/dist/size-guard.d.ts.map +1 -0
- package/dist/size-guard.js +70 -0
- package/dist/size-guard.js.map +1 -0
- package/package.json +37 -0
package/NOTICE
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
rrweb-stack
|
|
2
|
+
Copyright 2026 Cubenest
|
|
3
|
+
|
|
4
|
+
This product includes software developed by:
|
|
5
|
+
- PostHog (https://posthog.com) — vendored rrweb fork:
|
|
6
|
+
@posthog/rrweb 0.0.34 (MIT License)
|
|
7
|
+
@posthog/rrweb-types 0.0.24 (MIT License)
|
|
8
|
+
Forked from upstream rrweb-io/rrweb 2.0.0-alpha.17.
|
|
9
|
+
|
|
10
|
+
This product is licensed under the Apache License, Version 2.0.
|
|
11
|
+
See the LICENSE file for details.
|
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# @tracelane/core
|
|
2
|
+
|
|
3
|
+
Framework-agnostic recording engine for [`tracelane`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-core). Wraps a per-framework `browser` object behind a common `BrowserExecutor` interface, injects the rrweb capture bundle, drains in-page events to Node, and builds the buffer that the report packages render.
|
|
4
|
+
|
|
5
|
+
Not intended for direct consumption — depend on a product package (`@tracelane/wdio`, `@tracelane/playwright`, `@tracelane/cypress`) instead.
|
|
6
|
+
|
|
7
|
+
## What's in here
|
|
8
|
+
|
|
9
|
+
- `BrowserExecutor` — the framework-agnostic surface that adapters implement (see [ADR-0004](https://github.com/Cubenest/rrweb-stack/blob/main/prds/adrs/0004-p1-wdio-service-not-reporter.md)).
|
|
10
|
+
- Recorder controller — in-page buffer install + Node-polled drain (see [ADR-0006](https://github.com/Cubenest/rrweb-stack/blob/main/prds/adrs/0006-p1-in-page-buffer-node-polled.md)).
|
|
11
|
+
- Navigation re-injection with a 250ms cooldown + `tracelane.nav` boundary events.
|
|
12
|
+
- Mode switch (`'failed' | 'all'`) and a 25 MB FullSnapshot-preserving report-size guard (see [ADR-0005](https://github.com/Cubenest/rrweb-stack/blob/main/prds/adrs/0005-p1-failed-only-self-contained-html.md)).
|
|
13
|
+
|
|
14
|
+
Built on [`@cubenest/rrweb-core`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/rrweb-core).
|
|
15
|
+
|
|
16
|
+
## License
|
|
17
|
+
|
|
18
|
+
Apache-2.0. Contributions require a [DCO](https://developercertificate.org/) sign-off (`git commit -s`).
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework-agnostic surface that wraps a per-framework `browser`/`page`
|
|
3
|
+
* object. The wdio / playwright / cypress adapters implement this in their own
|
|
4
|
+
* packages (ADR-0004); `@tracelane/core` only ever talks to a `BrowserExecutor`,
|
|
5
|
+
* never to a concrete framework driver.
|
|
6
|
+
*
|
|
7
|
+
* Modeled on WebdriverIO's `browser` API (P1 PRD §A.4 / §A.5), which is the
|
|
8
|
+
* lowest-common-denominator across the three target frameworks.
|
|
9
|
+
*/
|
|
10
|
+
export interface BrowserExecutor {
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` in the page (browser) context and resolve with its return value.
|
|
13
|
+
*
|
|
14
|
+
* The function body is `.toString()`-serialized and evaluated in the page, so
|
|
15
|
+
* (per PRD §A.4) it MUST be self-contained: closures over Node-side variables
|
|
16
|
+
* are silently dropped — always pass values explicitly via `...args`. The
|
|
17
|
+
* return value must be JSON-serializable (no functions, DOM nodes, or
|
|
18
|
+
* circular references).
|
|
19
|
+
*/
|
|
20
|
+
execute<T>(fn: (...args: unknown[]) => T, ...args: unknown[]): Promise<T>;
|
|
21
|
+
/**
|
|
22
|
+
* Run an async `fn` in the page context. The injected function receives a
|
|
23
|
+
* trailing `done` callback as its last argument (WebDriver async-script
|
|
24
|
+
* semantics); it resolves the returned promise when `done(value)` is called.
|
|
25
|
+
*/
|
|
26
|
+
executeAsync<T>(fn: (...args: unknown[]) => void, ...args: unknown[]): Promise<T>;
|
|
27
|
+
/**
|
|
28
|
+
* Send a Chrome DevTools Protocol command on the active connection
|
|
29
|
+
* (PRD §A.5). Requires a CDP-capable transport (e.g. `@wdio/devtools-service`
|
|
30
|
+
* for WebdriverIO). Resolves with the raw CDP result.
|
|
31
|
+
*/
|
|
32
|
+
cdp(domain: string, command: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to a CDP event (e.g. `'Network.responseReceived'`) on the same
|
|
35
|
+
* connection used by {@link BrowserExecutor.cdp}.
|
|
36
|
+
*/
|
|
37
|
+
on(event: string, handler: (params: unknown) => void): void;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=browser-executor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-executor.d.ts","sourceRoot":"","sources":["../src/browser-executor.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAE1E;;;;OAIG;IACH,YAAY,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAElF;;;;OAIG;IACH,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEzF;;;OAGG;IACH,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,GAAG,IAAI,CAAC;CAC7D"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser-executor.js","sourceRoot":"","sources":["../src/browser-executor.ts"],"names":[],"mappings":""}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { BrowserExecutor } from './browser-executor.js';
|
|
2
|
+
export { createRecorder, DEFAULT_COOLDOWN_MS, DEFAULT_DRAIN_INTERVAL_MS } from './recorder.js';
|
|
3
|
+
export type { FinalizeResult, Recorder, RecorderOptions, TestOutcome } from './recorder.js';
|
|
4
|
+
export type { ConsolePluginOptions } from './page-script.js';
|
|
5
|
+
export { DEFAULT_MODE, resolveMode } from './mode.js';
|
|
6
|
+
export type { Mode } from './mode.js';
|
|
7
|
+
export { MAX_REPORT_BYTES, PRUNE_EVENT_TAG, pruneToSizeBudget, serializedSize, } from './size-guard.js';
|
|
8
|
+
export type { PruneEventPayload, PruneResult } from './size-guard.js';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,YAAY,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAG7D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAC/F,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5F,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAG7D,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AACtD,YAAY,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGtC,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Public API surface for @tracelane/core.
|
|
2
|
+
// Recorder controller — in-page buffer install + Node-polled drain (ADR-0006).
|
|
3
|
+
export { createRecorder, DEFAULT_COOLDOWN_MS, DEFAULT_DRAIN_INTERVAL_MS } from './recorder.js';
|
|
4
|
+
// Capture mode switch (ADR-0005).
|
|
5
|
+
export { DEFAULT_MODE, resolveMode } from './mode.js';
|
|
6
|
+
// 25 MB report-size guard with FullSnapshot-preserving prune (ADR-0005).
|
|
7
|
+
export { MAX_REPORT_BYTES, PRUNE_EVENT_TAG, pruneToSizeBudget, serializedSize, } from './size-guard.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAK1C,+EAA+E;AAC/E,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAI/F,kCAAkC;AAClC,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAGtD,yEAAyE;AACzE,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,iBAAiB,EACjB,cAAc,GACf,MAAM,iBAAiB,CAAC"}
|
package/dist/mode.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture mode (ADR-0005).
|
|
3
|
+
*
|
|
4
|
+
* - `'failed'` (default): events buffer in memory during the test; on pass the
|
|
5
|
+
* buffer is discarded, on failure a report is built.
|
|
6
|
+
* - `'all'`: a report is built regardless of outcome (visual-regression
|
|
7
|
+
* workflows), available behind `TRACELANE_MODE=all`.
|
|
8
|
+
*/
|
|
9
|
+
export type Mode = 'failed' | 'all';
|
|
10
|
+
/** The default capture mode (ADR-0005). */
|
|
11
|
+
export declare const DEFAULT_MODE: Mode;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve the effective mode. The `TRACELANE_MODE` env var, when set to a valid
|
|
14
|
+
* value, overrides the config; an invalid env value is ignored. Falls back to
|
|
15
|
+
* the config mode, then {@link DEFAULT_MODE}.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveMode(configMode?: Mode): Mode;
|
|
18
|
+
//# sourceMappingURL=mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../src/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,MAAM,IAAI,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEpC,2CAA2C;AAC3C,eAAO,MAAM,YAAY,EAAE,IAAe,CAAC;AAgB3C;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,UAAU,CAAC,EAAE,IAAI,GAAG,IAAI,CAInD"}
|
package/dist/mode.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** The default capture mode (ADR-0005). */
|
|
2
|
+
export const DEFAULT_MODE = 'failed';
|
|
3
|
+
function isMode(value) {
|
|
4
|
+
return value === 'failed' || value === 'all';
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Read `TRACELANE_MODE` from the environment without a hard dependency on
|
|
8
|
+
* `@types/node` (this package stays framework- and platform-light). Resolves
|
|
9
|
+
* `process.env` defensively so it's a no-op in a browser-like context.
|
|
10
|
+
*/
|
|
11
|
+
function readModeEnv() {
|
|
12
|
+
const proc = globalThis.process;
|
|
13
|
+
return proc?.env?.TRACELANE_MODE;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the effective mode. The `TRACELANE_MODE` env var, when set to a valid
|
|
17
|
+
* value, overrides the config; an invalid env value is ignored. Falls back to
|
|
18
|
+
* the config mode, then {@link DEFAULT_MODE}.
|
|
19
|
+
*/
|
|
20
|
+
export function resolveMode(configMode) {
|
|
21
|
+
const fromEnv = readModeEnv();
|
|
22
|
+
if (isMode(fromEnv))
|
|
23
|
+
return fromEnv;
|
|
24
|
+
return configMode ?? DEFAULT_MODE;
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=mode.js.map
|
package/dist/mode.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mode.js","sourceRoot":"","sources":["../src/mode.ts"],"names":[],"mappings":"AAUA,2CAA2C;AAC3C,MAAM,CAAC,MAAM,YAAY,GAAS,QAAQ,CAAC;AAE3C,SAAS,MAAM,CAAC,KAAyB;IACvC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,KAAK,CAAC;AAC/C,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW;IAClB,MAAM,IAAI,GAAI,UAAyE,CAAC,OAAO,CAAC;IAChG,OAAO,IAAI,EAAE,GAAG,EAAE,cAAc,CAAC;AACnC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CAAC,UAAiB;IAC3C,MAAM,OAAO,GAAG,WAAW,EAAE,CAAC;IAC9B,IAAI,MAAM,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACpC,OAAO,UAAU,IAAI,YAAY,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-context scripts, written as self-contained functions so a
|
|
3
|
+
* {@link BrowserExecutor} can `.toString()`-serialize and run them in the
|
|
4
|
+
* browser (PRD §A.4). They MUST NOT close over any Node-side variable — every
|
|
5
|
+
* input arrives via an explicit argument, every output is JSON-serializable.
|
|
6
|
+
*
|
|
7
|
+
* The recorder controller (Node side) is the only caller; these are kept in a
|
|
8
|
+
* separate module so the serialized source stays small and reviewable.
|
|
9
|
+
*/
|
|
10
|
+
/** Options forwarded into the in-page rrweb console plugin (PRD §D.3). */
|
|
11
|
+
export interface ConsolePluginOptions {
|
|
12
|
+
level?: string[];
|
|
13
|
+
lengthThreshold?: number;
|
|
14
|
+
stringifyOptions?: {
|
|
15
|
+
stringLengthLimit?: number;
|
|
16
|
+
numOfKeysLimit?: number;
|
|
17
|
+
depthOfLimit?: number;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/** Console-plugin defaults from PRD §D.3. */
|
|
21
|
+
export declare const DEFAULT_CONSOLE_PLUGIN_OPTIONS: ConsolePluginOptions;
|
|
22
|
+
/**
|
|
23
|
+
* The in-page init routine (PRD §D.3 + ADR-0006). Idempotent across calls via a
|
|
24
|
+
* monotonic `__tracelane__sessionId` stamp and a cooldown guard so hash-only /
|
|
25
|
+
* HMR navigations don't double-init the recorder (the cooldown / re-injection
|
|
26
|
+
* semantics are exercised in Task 2.4).
|
|
27
|
+
*
|
|
28
|
+
* Assumes `window.rrweb` is already defined (the recorder injects the rrweb
|
|
29
|
+
* bundle string first). Returns the active session id so the Node side can
|
|
30
|
+
* confirm whether a (re-)init actually took effect.
|
|
31
|
+
*/
|
|
32
|
+
export declare function tracelaneInitScript(cooldownMs: number, consoleOptions: ConsolePluginOptions): number;
|
|
33
|
+
/**
|
|
34
|
+
* Read-and-clear drain (PRD §A.4 / §D.3). Returns the buffered events and resets
|
|
35
|
+
* the page buffer so the next drain doesn't double-count.
|
|
36
|
+
*/
|
|
37
|
+
export declare function tracelaneDrainScript(): unknown[];
|
|
38
|
+
/**
|
|
39
|
+
* Append a `tracelane.nav` boundary marker (ADR-0006 / PRD §D.5) via rrweb's
|
|
40
|
+
* canonical custom-event API so the merged stream still has a navigation marker
|
|
41
|
+
* the player can render. No-op if rrweb isn't present.
|
|
42
|
+
*/
|
|
43
|
+
export declare function tracelaneNavScript(url: string, ts: number): void;
|
|
44
|
+
//# sourceMappingURL=page-script.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-script.d.ts","sourceRoot":"","sources":["../src/page-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,0EAA0E;AAC1E,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE;QACjB,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,YAAY,CAAC,EAAE,MAAM,CAAC;KACvB,CAAC;CACH;AAED,6CAA6C;AAC7C,eAAO,MAAM,8BAA8B,EAAE,oBAI5C,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,oBAAoB,GACnC,MAAM,CA8CR;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,OAAO,EAAE,CAKhD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAKhE"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page-context scripts, written as self-contained functions so a
|
|
3
|
+
* {@link BrowserExecutor} can `.toString()`-serialize and run them in the
|
|
4
|
+
* browser (PRD §A.4). They MUST NOT close over any Node-side variable — every
|
|
5
|
+
* input arrives via an explicit argument, every output is JSON-serializable.
|
|
6
|
+
*
|
|
7
|
+
* The recorder controller (Node side) is the only caller; these are kept in a
|
|
8
|
+
* separate module so the serialized source stays small and reviewable.
|
|
9
|
+
*/
|
|
10
|
+
/** Console-plugin defaults from PRD §D.3. */
|
|
11
|
+
export const DEFAULT_CONSOLE_PLUGIN_OPTIONS = {
|
|
12
|
+
level: ['info', 'log', 'warn', 'error'],
|
|
13
|
+
lengthThreshold: 10000,
|
|
14
|
+
stringifyOptions: { stringLengthLimit: 1000, numOfKeysLimit: 100, depthOfLimit: 1 },
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* The in-page init routine (PRD §D.3 + ADR-0006). Idempotent across calls via a
|
|
18
|
+
* monotonic `__tracelane__sessionId` stamp and a cooldown guard so hash-only /
|
|
19
|
+
* HMR navigations don't double-init the recorder (the cooldown / re-injection
|
|
20
|
+
* semantics are exercised in Task 2.4).
|
|
21
|
+
*
|
|
22
|
+
* Assumes `window.rrweb` is already defined (the recorder injects the rrweb
|
|
23
|
+
* bundle string first). Returns the active session id so the Node side can
|
|
24
|
+
* confirm whether a (re-)init actually took effect.
|
|
25
|
+
*/
|
|
26
|
+
export function tracelaneInitScript(cooldownMs, consoleOptions) {
|
|
27
|
+
const w = window;
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
// Cooldown: a very recent init means this is a hash/HMR re-render, not a real
|
|
30
|
+
// navigation — skip to avoid double-recording (ADR-0006).
|
|
31
|
+
if (w.__tracelane__inited !== undefined && now - w.__tracelane__inited < cooldownMs) {
|
|
32
|
+
return w.__tracelane__sessionId ?? 0;
|
|
33
|
+
}
|
|
34
|
+
w.__tracelane__inited = now;
|
|
35
|
+
w.__tracelane__sessionId = (w.__tracelane__sessionId ?? 0) + 1;
|
|
36
|
+
w.__tracelane__events = w.__tracelane__events ?? [];
|
|
37
|
+
if (w.rrweb !== undefined) {
|
|
38
|
+
// Tear down any prior recorder before starting a fresh one (re-injection).
|
|
39
|
+
if (typeof w.__tracelane__stop === 'function') {
|
|
40
|
+
try {
|
|
41
|
+
w.__tracelane__stop();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore teardown errors from a destroyed page context
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const stop = w.rrweb.record({
|
|
48
|
+
emit(event) {
|
|
49
|
+
// Never call console.* here — the console plugin patches console and
|
|
50
|
+
// guards recursion (PRD §D.4).
|
|
51
|
+
w.__tracelane__events.push(event);
|
|
52
|
+
},
|
|
53
|
+
plugins: [w.rrweb.getRecordConsolePlugin(consoleOptions)],
|
|
54
|
+
});
|
|
55
|
+
w.__tracelane__stop = typeof stop === 'function' ? stop : undefined;
|
|
56
|
+
}
|
|
57
|
+
return w.__tracelane__sessionId;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Read-and-clear drain (PRD §A.4 / §D.3). Returns the buffered events and resets
|
|
61
|
+
* the page buffer so the next drain doesn't double-count.
|
|
62
|
+
*/
|
|
63
|
+
export function tracelaneDrainScript() {
|
|
64
|
+
const w = window;
|
|
65
|
+
const out = w.__tracelane__events ?? [];
|
|
66
|
+
w.__tracelane__events = [];
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Append a `tracelane.nav` boundary marker (ADR-0006 / PRD §D.5) via rrweb's
|
|
71
|
+
* canonical custom-event API so the merged stream still has a navigation marker
|
|
72
|
+
* the player can render. No-op if rrweb isn't present.
|
|
73
|
+
*/
|
|
74
|
+
export function tracelaneNavScript(url, ts) {
|
|
75
|
+
const w = window;
|
|
76
|
+
w.rrweb?.record?.addCustomEvent?.('tracelane.nav', { url, ts });
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=page-script.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"page-script.js","sourceRoot":"","sources":["../src/page-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,6CAA6C;AAC7C,MAAM,CAAC,MAAM,8BAA8B,GAAyB;IAClE,KAAK,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC;IACvC,eAAe,EAAE,KAAK;IACtB,gBAAgB,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,EAAE;CACpF,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,UAAU,mBAAmB,CACjC,UAAkB,EAClB,cAAoC;IAEpC,MAAM,CAAC,GAAG,MAWT,CAAC;IAEF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,8EAA8E;IAC9E,0DAA0D;IAC1D,IAAI,CAAC,CAAC,mBAAmB,KAAK,SAAS,IAAI,GAAG,GAAG,CAAC,CAAC,mBAAmB,GAAG,UAAU,EAAE,CAAC;QACpF,OAAO,CAAC,CAAC,sBAAsB,IAAI,CAAC,CAAC;IACvC,CAAC;IAED,CAAC,CAAC,mBAAmB,GAAG,GAAG,CAAC;IAC5B,CAAC,CAAC,sBAAsB,GAAG,CAAC,CAAC,CAAC,sBAAsB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;IAC/D,CAAC,CAAC,mBAAmB,GAAG,CAAC,CAAC,mBAAmB,IAAI,EAAE,CAAC;IAEpD,IAAI,CAAC,CAAC,KAAK,KAAK,SAAS,EAAE,CAAC;QAC1B,2EAA2E;QAC3E,IAAI,OAAO,CAAC,CAAC,iBAAiB,KAAK,UAAU,EAAE,CAAC;YAC9C,IAAI,CAAC;gBACH,CAAC,CAAC,iBAAiB,EAAE,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,uDAAuD;YACzD,CAAC;QACH,CAAC;QACD,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC;YAC1B,IAAI,CAAC,KAAc;gBACjB,qEAAqE;gBACrE,+BAA+B;gBAC9B,CAAC,CAAC,mBAAiC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;YACD,OAAO,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,sBAAsB,CAAC,cAAc,CAAC,CAAC;SAC1D,CAAC,CAAC;QACH,CAAC,CAAC,iBAAiB,GAAG,OAAO,IAAI,KAAK,UAAU,CAAC,CAAC,CAAE,IAAmB,CAAC,CAAC,CAAC,SAAS,CAAC;IACtF,CAAC;IAED,OAAO,CAAC,CAAC,sBAAsB,CAAC;AAClC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,GAAG,MAAwD,CAAC;IACnE,MAAM,GAAG,GAAG,CAAC,CAAC,mBAAmB,IAAI,EAAE,CAAC;IACxC,CAAC,CAAC,mBAAmB,GAAG,EAAE,CAAC;IAC3B,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,EAAU;IACxD,MAAM,CAAC,GAAG,MAET,CAAC;IACF,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC,eAAe,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,CAAC,CAAC;AAClE,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
import type { BrowserExecutor } from './browser-executor.js';
|
|
3
|
+
import { type Mode } from './mode.js';
|
|
4
|
+
import { type ConsolePluginOptions } from './page-script.js';
|
|
5
|
+
/** Default re-injection cooldown in ms (ADR-0006). */
|
|
6
|
+
export declare const DEFAULT_COOLDOWN_MS = 250;
|
|
7
|
+
/** Default Node-side poll interval in ms (ADR-0006). */
|
|
8
|
+
export declare const DEFAULT_DRAIN_INTERVAL_MS = 5000;
|
|
9
|
+
export interface RecorderOptions {
|
|
10
|
+
/** The framework-agnostic driver (ADR-0004). */
|
|
11
|
+
executor: BrowserExecutor;
|
|
12
|
+
/**
|
|
13
|
+
* The rrweb UMD bundle source that defines `window.rrweb` (with `record` and
|
|
14
|
+
* `getRecordConsolePlugin`). Supplied by the consuming adapter — `@tracelane/core`
|
|
15
|
+
* is bundle-source-agnostic and never imports rrweb for in-page injection.
|
|
16
|
+
*/
|
|
17
|
+
rrwebBundle: string;
|
|
18
|
+
/** Node-side drain poll interval (default 5000). */
|
|
19
|
+
drainIntervalMs?: number;
|
|
20
|
+
/** Re-injection cooldown guard (default 250). */
|
|
21
|
+
cooldownMs?: number;
|
|
22
|
+
/** Options forwarded to the in-page console plugin. */
|
|
23
|
+
consolePluginOptions?: ConsolePluginOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Capture mode (ADR-0005). Default `'failed'`. The `TRACELANE_MODE` env var
|
|
26
|
+
* overrides this at {@link Recorder.finalize} time.
|
|
27
|
+
*/
|
|
28
|
+
mode?: Mode;
|
|
29
|
+
}
|
|
30
|
+
/** Outcome handed to {@link Recorder.finalize}. */
|
|
31
|
+
export interface TestOutcome {
|
|
32
|
+
/** Whether the test passed. */
|
|
33
|
+
passed: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** Decision returned by {@link Recorder.finalize}. */
|
|
36
|
+
export interface FinalizeResult {
|
|
37
|
+
/** Whether a report should be built for this test (ADR-0005). */
|
|
38
|
+
shouldBuildReport: boolean;
|
|
39
|
+
/** The events to build the report from (empty when discarded). */
|
|
40
|
+
events: eventWithTime[];
|
|
41
|
+
}
|
|
42
|
+
export interface Recorder {
|
|
43
|
+
/** Inject the rrweb bundle, install the in-page buffer, and start polling. */
|
|
44
|
+
start(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Re-inject after a navigation (ADR-0006). The in-page cooldown guard
|
|
47
|
+
* suppresses double-init on hash-only / HMR navigations; when a real re-init
|
|
48
|
+
* takes effect (the monotonic session id advances) a `tracelane.nav` boundary
|
|
49
|
+
* event is appended. Returns `true` if a re-init actually happened.
|
|
50
|
+
*/
|
|
51
|
+
reinject(url: string): Promise<boolean>;
|
|
52
|
+
/** Read+clear the page buffer, merge into the Node buffer, return the batch. */
|
|
53
|
+
drain(): Promise<eventWithTime[]>;
|
|
54
|
+
/** Stop polling and perform a final drain. */
|
|
55
|
+
stop(): Promise<void>;
|
|
56
|
+
/**
|
|
57
|
+
* End the capture (ADR-0005): stop polling, drain any pending in-page events,
|
|
58
|
+
* then apply the mode policy. In `'failed'` mode a passing test discards the
|
|
59
|
+
* buffer and reports nothing; a failing test (or `'all'` mode) keeps the
|
|
60
|
+
* buffer and signals that a report should be built. `TRACELANE_MODE` overrides
|
|
61
|
+
* the configured mode here.
|
|
62
|
+
*/
|
|
63
|
+
finalize(outcome: TestOutcome): Promise<FinalizeResult>;
|
|
64
|
+
/** The merged Node-side event buffer (live reference). */
|
|
65
|
+
getBuffer(): eventWithTime[];
|
|
66
|
+
}
|
|
67
|
+
export declare function createRecorder(options: RecorderOptions): Recorder;
|
|
68
|
+
//# sourceMappingURL=recorder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.d.ts","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,KAAK,IAAI,EAAe,MAAM,WAAW,CAAC;AACnD,OAAO,EACL,KAAK,oBAAoB,EAK1B,MAAM,kBAAkB,CAAC;AAE1B,sDAAsD;AACtD,eAAO,MAAM,mBAAmB,MAAM,CAAC;AACvC,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;;OAIG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,oDAAoD;IACpD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,iDAAiD;IACjD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,oBAAoB,CAAC,EAAE,oBAAoB,CAAC;IAC5C;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAED,mDAAmD;AACnD,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,sDAAsD;AACtD,MAAM,WAAW,cAAc;IAC7B,iEAAiE;IACjE,iBAAiB,EAAE,OAAO,CAAC;IAC3B,kEAAkE;IAClE,MAAM,EAAE,aAAa,EAAE,CAAC;CACzB;AAED,MAAM,WAAW,QAAQ;IACvB,8EAA8E;IAC9E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,gFAAgF;IAChF,KAAK,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;IAClC,8CAA8C;IAC9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACtB;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IACxD,0DAA0D;IAC1D,SAAS,IAAI,aAAa,EAAE,CAAC;CAC9B;AASD,wBAAgB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,QAAQ,CA+FjE"}
|
package/dist/recorder.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { resolveMode } from './mode.js';
|
|
2
|
+
import { DEFAULT_CONSOLE_PLUGIN_OPTIONS, tracelaneDrainScript, tracelaneInitScript, tracelaneNavScript, } from './page-script.js';
|
|
3
|
+
/** Default re-injection cooldown in ms (ADR-0006). */
|
|
4
|
+
export const DEFAULT_COOLDOWN_MS = 250;
|
|
5
|
+
/** Default Node-side poll interval in ms (ADR-0006). */
|
|
6
|
+
export const DEFAULT_DRAIN_INTERVAL_MS = 5000;
|
|
7
|
+
/** Inject + eval the rrweb bundle string in the page (defines `window.rrweb`). */
|
|
8
|
+
function injectBundleScript(bundle) {
|
|
9
|
+
// window.eval runs the bundle in global page scope (so `window.rrweb` becomes
|
|
10
|
+
// a real global), which is the intended injection behavior in the page context.
|
|
11
|
+
window.eval(bundle);
|
|
12
|
+
}
|
|
13
|
+
export function createRecorder(options) {
|
|
14
|
+
const { executor, rrwebBundle, drainIntervalMs = DEFAULT_DRAIN_INTERVAL_MS, cooldownMs = DEFAULT_COOLDOWN_MS, consolePluginOptions = DEFAULT_CONSOLE_PLUGIN_OPTIONS, mode: configMode, } = options;
|
|
15
|
+
const buffer = [];
|
|
16
|
+
let pollTimer;
|
|
17
|
+
let started = false;
|
|
18
|
+
// Last session id we've observed from the page; advances only when an init
|
|
19
|
+
// actually takes effect (i.e. wasn't suppressed by the cooldown guard).
|
|
20
|
+
let lastSessionId = 0;
|
|
21
|
+
/** Run the init script in-page and return the active session id. */
|
|
22
|
+
async function runInit() {
|
|
23
|
+
return executor.execute(tracelaneInitScript, cooldownMs, consolePluginOptions);
|
|
24
|
+
}
|
|
25
|
+
async function inject() {
|
|
26
|
+
await executor.execute(injectBundleScript, rrwebBundle);
|
|
27
|
+
lastSessionId = await runInit();
|
|
28
|
+
}
|
|
29
|
+
async function reinject(url) {
|
|
30
|
+
// Re-eval the bundle (the page may have been torn down by navigation), then
|
|
31
|
+
// re-run init. The cooldown guard inside the init script decides whether a
|
|
32
|
+
// fresh recorder actually starts.
|
|
33
|
+
await executor.execute(injectBundleScript, rrwebBundle);
|
|
34
|
+
const sessionId = await runInit();
|
|
35
|
+
if (sessionId <= lastSessionId) {
|
|
36
|
+
// Suppressed by cooldown — no navigation boundary to record.
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
lastSessionId = sessionId;
|
|
40
|
+
await executor.execute(tracelaneNavScript, url, Date.now());
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
async function drain() {
|
|
44
|
+
const batch = (await executor.execute(tracelaneDrainScript));
|
|
45
|
+
if (batch && batch.length > 0) {
|
|
46
|
+
buffer.push(...batch);
|
|
47
|
+
return batch;
|
|
48
|
+
}
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
async function start() {
|
|
52
|
+
if (started)
|
|
53
|
+
return;
|
|
54
|
+
started = true;
|
|
55
|
+
await inject();
|
|
56
|
+
pollTimer = setInterval(() => {
|
|
57
|
+
void drain();
|
|
58
|
+
}, drainIntervalMs);
|
|
59
|
+
}
|
|
60
|
+
async function stop() {
|
|
61
|
+
if (pollTimer !== undefined) {
|
|
62
|
+
clearInterval(pollTimer);
|
|
63
|
+
pollTimer = undefined;
|
|
64
|
+
}
|
|
65
|
+
started = false;
|
|
66
|
+
await drain();
|
|
67
|
+
}
|
|
68
|
+
async function finalize(outcome) {
|
|
69
|
+
await stop();
|
|
70
|
+
const mode = resolveMode(configMode);
|
|
71
|
+
const shouldBuildReport = mode === 'all' || !outcome.passed;
|
|
72
|
+
if (!shouldBuildReport) {
|
|
73
|
+
// Discard: passing test in 'failed' mode keeps near-zero artifact cost.
|
|
74
|
+
buffer.length = 0;
|
|
75
|
+
return { shouldBuildReport: false, events: [] };
|
|
76
|
+
}
|
|
77
|
+
return { shouldBuildReport: true, events: buffer };
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
start,
|
|
81
|
+
reinject,
|
|
82
|
+
drain,
|
|
83
|
+
stop,
|
|
84
|
+
finalize,
|
|
85
|
+
getBuffer: () => buffer,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=recorder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recorder.js","sourceRoot":"","sources":["../src/recorder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { eventWithTime } from '@cubenest/rrweb-core';
|
|
2
|
+
/**
|
|
3
|
+
* Hard cap on the serialized events size (ADR-0005): 25 MB. Calibrated against
|
|
4
|
+
* GitHub Actions artifact limits and rrweb's typical compressed size for
|
|
5
|
+
* ~5-minute interactive tests; anything larger is slow to open in a browser.
|
|
6
|
+
*/
|
|
7
|
+
export declare const MAX_REPORT_BYTES: number;
|
|
8
|
+
/** Tag of the custom rrweb event emitted when a prune fires (ADR-0005). */
|
|
9
|
+
export declare const PRUNE_EVENT_TAG = "tracelane.events-pruned";
|
|
10
|
+
/** UTF-8 byte length of the JSON serialization of `events`. */
|
|
11
|
+
export declare function serializedSize(events: readonly eventWithTime[]): number;
|
|
12
|
+
/** Payload of the {@link PRUNE_EVENT_TAG} marker. */
|
|
13
|
+
export interface PruneEventPayload {
|
|
14
|
+
/** Number of IncrementalSnapshot events dropped to fit the budget. */
|
|
15
|
+
droppedCount: number;
|
|
16
|
+
/** The byte budget the prune targeted. */
|
|
17
|
+
maxBytes: number;
|
|
18
|
+
}
|
|
19
|
+
export interface PruneResult {
|
|
20
|
+
/** The (possibly pruned) events, including the prune marker when fired. */
|
|
21
|
+
events: eventWithTime[];
|
|
22
|
+
/** Whether any events were dropped. */
|
|
23
|
+
pruned: boolean;
|
|
24
|
+
/** How many IncrementalSnapshot events were dropped. */
|
|
25
|
+
droppedCount: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Prune `events` to fit `maxBytes` (ADR-0005), dropping the OLDEST
|
|
29
|
+
* IncrementalSnapshot (type 3) events first while preserving FullSnapshot /
|
|
30
|
+
* Meta / Custom / Plugin. Surviving events keep their relative order. When any
|
|
31
|
+
* event is dropped, a single {@link PRUNE_EVENT_TAG} custom event is appended
|
|
32
|
+
* so the report can surface a "events pruned to fit budget" banner.
|
|
33
|
+
*
|
|
34
|
+
* If dropping every IncrementalSnapshot still doesn't fit (preserved events
|
|
35
|
+
* alone exceed the budget), it keeps the preserved events — replay correctness
|
|
36
|
+
* wins over the byte cap, and the prune marker still records what happened.
|
|
37
|
+
*/
|
|
38
|
+
export declare function pruneToSizeBudget(events: readonly eventWithTime[], maxBytes?: number): PruneResult;
|
|
39
|
+
//# sourceMappingURL=size-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-guard.d.ts","sourceRoot":"","sources":["../src/size-guard.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,QAAmB,CAAC;AAEjD,2EAA2E;AAC3E,eAAO,MAAM,eAAe,4BAA4B,CAAC;AAiBzD,+DAA+D;AAC/D,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,MAAM,CAEvE;AAED,qDAAqD;AACrD,MAAM,WAAW,iBAAiB;IAChC,sEAAsE;IACtE,YAAY,EAAE,MAAM,CAAC;IACrB,0CAA0C;IAC1C,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,uCAAuC;IACvC,MAAM,EAAE,OAAO,CAAC;IAChB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;CACtB;AAUD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,SAAS,aAAa,EAAE,EAChC,QAAQ,GAAE,MAAyB,GAClC,WAAW,CA2Bb"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { EventType } from '@cubenest/rrweb-core';
|
|
2
|
+
/**
|
|
3
|
+
* Hard cap on the serialized events size (ADR-0005): 25 MB. Calibrated against
|
|
4
|
+
* GitHub Actions artifact limits and rrweb's typical compressed size for
|
|
5
|
+
* ~5-minute interactive tests; anything larger is slow to open in a browser.
|
|
6
|
+
*/
|
|
7
|
+
export const MAX_REPORT_BYTES = 25 * 1024 * 1024;
|
|
8
|
+
/** Tag of the custom rrweb event emitted when a prune fires (ADR-0005). */
|
|
9
|
+
export const PRUNE_EVENT_TAG = 'tracelane.events-pruned';
|
|
10
|
+
/**
|
|
11
|
+
* Event types that must survive a prune (ADR-0005): FullSnapshot checkpoints
|
|
12
|
+
* are mandatory for replay, and Meta / Custom / Plugin carry structural and
|
|
13
|
+
* panel context. Only IncrementalSnapshot (the largest, lowest-value-per-byte
|
|
14
|
+
* category) is droppable.
|
|
15
|
+
*/
|
|
16
|
+
const PRESERVED_TYPES = new Set([
|
|
17
|
+
EventType.FullSnapshot, // 2
|
|
18
|
+
EventType.Meta, // 4
|
|
19
|
+
EventType.Custom, // 5
|
|
20
|
+
EventType.Plugin, // 6
|
|
21
|
+
]);
|
|
22
|
+
const encoder = new TextEncoder();
|
|
23
|
+
/** UTF-8 byte length of the JSON serialization of `events`. */
|
|
24
|
+
export function serializedSize(events) {
|
|
25
|
+
return encoder.encode(JSON.stringify(events)).length;
|
|
26
|
+
}
|
|
27
|
+
function makePruneEvent(payload) {
|
|
28
|
+
return {
|
|
29
|
+
type: EventType.Custom,
|
|
30
|
+
data: { tag: PRUNE_EVENT_TAG, payload },
|
|
31
|
+
timestamp: Date.now(),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Prune `events` to fit `maxBytes` (ADR-0005), dropping the OLDEST
|
|
36
|
+
* IncrementalSnapshot (type 3) events first while preserving FullSnapshot /
|
|
37
|
+
* Meta / Custom / Plugin. Surviving events keep their relative order. When any
|
|
38
|
+
* event is dropped, a single {@link PRUNE_EVENT_TAG} custom event is appended
|
|
39
|
+
* so the report can surface a "events pruned to fit budget" banner.
|
|
40
|
+
*
|
|
41
|
+
* If dropping every IncrementalSnapshot still doesn't fit (preserved events
|
|
42
|
+
* alone exceed the budget), it keeps the preserved events — replay correctness
|
|
43
|
+
* wins over the byte cap, and the prune marker still records what happened.
|
|
44
|
+
*/
|
|
45
|
+
export function pruneToSizeBudget(events, maxBytes = MAX_REPORT_BYTES) {
|
|
46
|
+
if (serializedSize(events) <= maxBytes) {
|
|
47
|
+
return { events: [...events], pruned: false, droppedCount: 0 };
|
|
48
|
+
}
|
|
49
|
+
// Indices of droppable (IncrementalSnapshot) events, oldest first. The input
|
|
50
|
+
// is already in chronological order, so array order == chronological order.
|
|
51
|
+
const droppableIndices = [];
|
|
52
|
+
events.forEach((e, i) => {
|
|
53
|
+
if (!PRESERVED_TYPES.has(e.type))
|
|
54
|
+
droppableIndices.push(i);
|
|
55
|
+
});
|
|
56
|
+
const dropped = new Set();
|
|
57
|
+
for (const index of droppableIndices) {
|
|
58
|
+
if (serializedSize(events.filter((_, i) => !dropped.has(i))) <= maxBytes)
|
|
59
|
+
break;
|
|
60
|
+
dropped.add(index);
|
|
61
|
+
}
|
|
62
|
+
const droppedCount = dropped.size;
|
|
63
|
+
const survivors = events.filter((_, i) => !dropped.has(i));
|
|
64
|
+
if (droppedCount === 0) {
|
|
65
|
+
return { events: survivors, pruned: false, droppedCount: 0 };
|
|
66
|
+
}
|
|
67
|
+
survivors.push(makePruneEvent({ droppedCount, maxBytes }));
|
|
68
|
+
return { events: survivors, pruned: true, droppedCount };
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=size-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"size-guard.js","sourceRoot":"","sources":["../src/size-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAGjD;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,eAAe,GAAG,yBAAyB,CAAC;AAEzD;;;;;GAKG;AACH,MAAM,eAAe,GAA2B,IAAI,GAAG,CAAC;IACtD,SAAS,CAAC,YAAY,EAAE,IAAI;IAC5B,SAAS,CAAC,IAAI,EAAE,IAAI;IACpB,SAAS,CAAC,MAAM,EAAE,IAAI;IACtB,SAAS,CAAC,MAAM,EAAE,IAAI;CACvB,CAAC,CAAC;AAEH,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;AAElC,+DAA+D;AAC/D,MAAM,UAAU,cAAc,CAAC,MAAgC;IAC7D,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AACvD,CAAC;AAmBD,SAAS,cAAc,CAAC,OAA0B;IAChD,OAAO;QACL,IAAI,EAAE,SAAS,CAAC,MAAM;QACtB,IAAI,EAAE,EAAE,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE;QACvC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;KACM,CAAC;AAChC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAgC,EAChC,WAAmB,gBAAgB;IAEnC,IAAI,cAAc,CAAC,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QACvC,OAAO,EAAE,MAAM,EAAE,CAAC,GAAG,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;IACjE,CAAC;IAED,6EAA6E;IAC7E,4EAA4E;IAC5E,MAAM,gBAAgB,GAAa,EAAE,CAAC;IACtC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC;YAAE,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,KAAK,IAAI,gBAAgB,EAAE,CAAC;QACrC,IAAI,cAAc,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ;YAAE,MAAM;QAChF,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACrB,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAClC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IAE3D,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;QACvB,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,EAAE,CAAC;IAC/D,CAAC;IAED,SAAS,CAAC,IAAI,CAAC,cAAc,CAAC,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;IAC3D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC;AAC3D,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tracelane/core",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Framework-agnostic rrweb recording engine for tracelane. Wraps a per-framework browser object behind a common BrowserExecutor interface.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": ["dist", "NOTICE", "README.md"],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsc -p tsconfig.json",
|
|
18
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
19
|
+
"test": "vitest run --passWithNoTests"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@cubenest/rrweb-core": "workspace:*"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"jsdom": "^25.0.1"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/Cubenest/rrweb-stack.git",
|
|
34
|
+
"directory": "packages/tracelane-core"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-core#readme"
|
|
37
|
+
}
|