@tracelane/report 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 ADDED
@@ -0,0 +1,15 @@
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
+ - rrweb contributors (https://github.com/rrweb-io/rrweb) — rrweb-player:
10
+ rrweb-player 1.0.0-alpha.4 (MIT License) — UMD + CSS inlined into report HTML.
11
+ - fflate (https://github.com/101arrowz/fflate) 0.8.x (MIT License) —
12
+ browser gunzip build inlined into report HTML for offline decompression.
13
+
14
+ This product is licensed under the Apache License, Version 2.0.
15
+ See the LICENSE file for details.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # @tracelane/report
2
+
3
+ The self-contained, offline HTML report builder for [`tracelane`](https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-core). Given a captured rrweb event stream plus test metadata, it produces a **single `.html` file** that:
4
+
5
+ - opens in any browser, fully offline (no network fetch at view time);
6
+ - embeds the [`rrweb-player`](https://www.npmjs.com/package/rrweb-player) UMD + CSS inline;
7
+ - embeds the events as a gzipped, base64-encoded blob that is decompressed in-page with an inlined [`fflate`](https://github.com/101arrowz/fflate) gunzip;
8
+ - renders console + network panels, a metadata header, and a "Copy as Markdown for AI paste" button.
9
+
10
+ Not generally intended for direct consumption — depend on a product package (`@tracelane/wdio`) instead.
11
+
12
+ ## Usage
13
+
14
+ ```ts
15
+ import { buildReport } from '@tracelane/report';
16
+
17
+ const html = buildReport(events, {
18
+ spec: 'login.spec.ts',
19
+ title: 'logs in with valid credentials',
20
+ status: 'failed',
21
+ error: 'expected element to be visible',
22
+ durationMs: 4210,
23
+ browserName: 'chrome',
24
+ browserVersion: '124.0',
25
+ viewport: { width: 1280, height: 720 },
26
+ // commitSha / buildUrl auto-detected from CI env when omitted
27
+ });
28
+ // write `html` to ./tracelane-reports/<spec>--<title>.html
29
+ ```
30
+
31
+ ## Design
32
+
33
+ See [P1 PRD §F](https://github.com/Cubenest/rrweb-stack/blob/main/prds/compass_artifact_wf-d53d32da-17e9-41b5-bb70-21dd1bf648c6_text_markdown.md) and [ADR-0005](https://github.com/Cubenest/rrweb-stack/blob/main/prds/adrs/0005-p1-failed-only-self-contained-html.md).
34
+
35
+ - **Player:** `rrweb-player@1.0.0-alpha.4` (upstream). `@posthog/rrweb-player` was the natural lineage match for the `@cubenest/rrweb-core` substrate fork, but every published version pins an unpublished dependency (`@posthog/rrweb-packer@0.0.0`, 404) and is therefore uninstallable. Upstream `rrweb-player` descends from the same rrweb 2.x line (`2.0.0-alpha.x`) that the substrate's `@posthog/rrweb@0.0.34` was forked from (`2.0.0-alpha.17`), so the recorded event shape replays correctly.
36
+ - **Decompression:** the build side uses `@cubenest/rrweb-core`'s `compress()` (fflate gzip); the view side inlines fflate's browser `gunzipSync` for a small (~8 KB) offline decompressor.
37
+ - **Asset inlining:** the player UMD/CSS and the fflate gunzip source are read from `node_modules` at build time (`fs.readFileSync`) — never hand-pasted into source.
38
+
39
+ ## License
40
+
41
+ Apache 2.0. The inlined rrweb player and fflate remain MIT-licensed; see NOTICE.
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The rrweb-player UMD bundle (~115 KB). Inlined verbatim into a top-level
3
+ * `<script>` in the report; its `var rrwebPlayer = (function(){…})()` IIFE then
4
+ * exposes `window.rrwebPlayer` for the bootstrap script to instantiate.
5
+ */
6
+ export declare function loadPlayerUmd(): string;
7
+ /**
8
+ * The rrweb-player stylesheet (~5 KB). Self-contained (cursor SVGs are inline
9
+ * data URIs), so it inlines into a `<style>` with no external fetches.
10
+ */
11
+ export declare function loadPlayerCss(): string;
12
+ /**
13
+ * The fflate UMD (~33 KB). Inlined into a top-level `<script>`; its UMD wrapper
14
+ * assigns `window.fflate` (with `gunzipSync` + `strFromU8`) so the bootstrap
15
+ * script can decompress the embedded event blob in-page (Task 2.9). Chosen over
16
+ * pako for consistency with `@cubenest/rrweb-core`'s fflate-based `compress()`.
17
+ */
18
+ export declare function loadFflateGunzipSource(): string;
19
+ //# sourceMappingURL=assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.d.ts","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAqDA;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,IAAI,MAAM,CAE/C"}
package/dist/assets.js ADDED
@@ -0,0 +1,73 @@
1
+ // Build-time asset loaders (Task 2.8 + 2.9).
2
+ //
3
+ // The self-contained report inlines three vendored assets so it opens fully
4
+ // offline with nothing fetched at view time:
5
+ // 1. the rrweb-player UMD (defines `window.rrwebPlayer`) — Task 2.8
6
+ // 2. the rrweb-player CSS — Task 2.8
7
+ // 3. the fflate UMD gunzip (defines `window.fflate`) — Task 2.9
8
+ //
9
+ // Each is read from the installed package via `require.resolve`, NOT hand-pasted
10
+ // into source (the assets are large and would bloat/obscure the diff, and they
11
+ // must track the pinned dependency versions). The reads happen at report-build
12
+ // time in Node, so they cost nothing at view time.
13
+ import { readFileSync } from 'node:fs';
14
+ import { createRequire } from 'node:module';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ // A CJS-style require rooted at this module's location, so `require.resolve`
17
+ // finds the dependencies through the normal node_modules resolution that pnpm
18
+ // set up — robust to where the compiled `dist/` ends up on disk.
19
+ const localRequire = createRequire(import.meta.url);
20
+ function readAsset(specifier) {
21
+ return readFileSync(localRequire.resolve(specifier), 'utf8');
22
+ }
23
+ /**
24
+ * Resolve a UMD entry that the package's `exports` map hides behind a deep path
25
+ * (fflate exports only `.` / `./browser` / `./node`, so
26
+ * `require.resolve('fflate/umd/index.js')` is blocked). We resolve the always-
27
+ * exported `package.json`, read its `unpkg` (the declared CDN/UMD entry), and
28
+ * resolve it against the package directory's `file:` URL — `new URL` normalizes
29
+ * the relative path (no manual string join), and a containment check rejects a
30
+ * `unpkg` value that would escape the package directory.
31
+ */
32
+ function readUmdViaUnpkg(packageName) {
33
+ const pkgJsonPath = localRequire.resolve(`${packageName}/package.json`);
34
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
35
+ const unpkg = pkg.unpkg;
36
+ if (typeof unpkg !== 'string') {
37
+ throw new Error(`${packageName}: package.json has no "unpkg" UMD entry to inline`);
38
+ }
39
+ // Resolve `unpkg` relative to the package.json file URL (its last segment is
40
+ // replaced), then re-derive a normalized path.
41
+ const pkgDirUrl = pathToFileURL(pkgJsonPath.slice(0, pkgJsonPath.lastIndexOf('/') + 1));
42
+ const assetPath = fileURLToPath(new URL(unpkg, pkgDirUrl));
43
+ const pkgDirPath = fileURLToPath(pkgDirUrl);
44
+ if (!assetPath.startsWith(pkgDirPath)) {
45
+ throw new Error(`${packageName}: "unpkg" entry escapes the package directory`);
46
+ }
47
+ return readFileSync(assetPath, 'utf8');
48
+ }
49
+ /**
50
+ * The rrweb-player UMD bundle (~115 KB). Inlined verbatim into a top-level
51
+ * `<script>` in the report; its `var rrwebPlayer = (function(){…})()` IIFE then
52
+ * exposes `window.rrwebPlayer` for the bootstrap script to instantiate.
53
+ */
54
+ export function loadPlayerUmd() {
55
+ return readAsset('rrweb-player/dist/index.js');
56
+ }
57
+ /**
58
+ * The rrweb-player stylesheet (~5 KB). Self-contained (cursor SVGs are inline
59
+ * data URIs), so it inlines into a `<style>` with no external fetches.
60
+ */
61
+ export function loadPlayerCss() {
62
+ return readAsset('rrweb-player/dist/style.css');
63
+ }
64
+ /**
65
+ * The fflate UMD (~33 KB). Inlined into a top-level `<script>`; its UMD wrapper
66
+ * assigns `window.fflate` (with `gunzipSync` + `strFromU8`) so the bootstrap
67
+ * script can decompress the embedded event blob in-page (Task 2.9). Chosen over
68
+ * pako for consistency with `@cubenest/rrweb-core`'s fflate-based `compress()`.
69
+ */
70
+ export function loadFflateGunzipSource() {
71
+ return readUmdViaUnpkg('fflate');
72
+ }
73
+ //# sourceMappingURL=assets.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assets.js","sourceRoot":"","sources":["../src/assets.ts"],"names":[],"mappings":"AAAA,6CAA6C;AAC7C,EAAE;AACF,4EAA4E;AAC5E,6CAA6C;AAC7C,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,EAAE;AACF,iFAAiF;AACjF,+EAA+E;AAC/E,+EAA+E;AAC/E,mDAAmD;AAEnD,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAExD,6EAA6E;AAC7E,8EAA8E;AAC9E,iEAAiE;AACjE,MAAM,YAAY,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAEpD,SAAS,SAAS,CAAC,SAAiB;IAClC,OAAO,YAAY,CAAC,YAAY,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC;AAC/D,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,WAAmB;IAC1C,MAAM,WAAW,GAAG,YAAY,CAAC,OAAO,CAAC,GAAG,WAAW,eAAe,CAAC,CAAC;IACxE,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAuB,CAAC;IAChF,MAAM,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC;IACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,mDAAmD,CAAC,CAAC;IACrF,CAAC;IACD,6EAA6E;IAC7E,+CAA+C;IAC/C,MAAM,SAAS,GAAG,aAAa,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IACxF,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3D,MAAM,UAAU,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;IAC5C,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QACtC,MAAM,IAAI,KAAK,CAAC,GAAG,WAAW,+CAA+C,CAAC,CAAC;IACjF,CAAC;IACD,OAAO,YAAY,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,SAAS,CAAC,4BAA4B,CAAC,CAAC;AACjD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,SAAS,CAAC,6BAA6B,CAAC,CAAC;AAClD,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,eAAe,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,19 @@
1
+ import type { eventWithTime } from '@cubenest/rrweb-core';
2
+ import type { ReportMeta } from './types.js';
3
+ /** Options for {@link buildReport}. */
4
+ export interface BuildReportOptions {
5
+ /**
6
+ * Prune events to the 25 MB budget before embedding (ADR-0005). Default true.
7
+ * Pass `false` only when the caller has already applied the size guard.
8
+ */
9
+ enforceSizeBudget?: boolean;
10
+ }
11
+ /**
12
+ * Build a self-contained HTML report for one test run.
13
+ *
14
+ * @param events the captured rrweb event stream (chronological)
15
+ * @param meta test metadata; CI provenance is auto-filled from the env
16
+ * @returns a complete `.html` document string — write it to disk as-is
17
+ */
18
+ export declare function buildReport(events: eventWithTime[], meta: ReportMeta, options?: BuildReportOptions): string;
19
+ //# sourceMappingURL=build-report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-report.d.ts","sourceRoot":"","sources":["../src/build-report.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAO1D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,aAAa,EAAE,EACvB,IAAI,EAAE,UAAU,EAChB,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CAsBR"}
@@ -0,0 +1,49 @@
1
+ // buildReport — the package entry point.
2
+ //
3
+ // Given a captured rrweb event stream + test metadata, produce a single,
4
+ // self-contained, offline HTML report string (P1 PRD §F). Orchestration only;
5
+ // each concern lives in its own module:
6
+ // • size-budget prune ............ @tracelane/core (pruneToSizeBudget)
7
+ // • events → gzip → base64 ....... embed.ts (Task 2.9)
8
+ // • console / network extraction . panels.ts (Task 2.10)
9
+ // • CI metadata + header ......... metadata.ts (Task 2.11)
10
+ // • copy-as-markdown payload ..... markdown.ts (Task 2.12)
11
+ // • HTML composition ............. template.ts
12
+ //
13
+ // The size guard reuses @tracelane/core's pruneToSizeBudget so the report never
14
+ // exceeds the 25 MB budget (ADR-0005); the report renders a banner when a prune
15
+ // fired so the truncation is visible to the user.
16
+ import { pruneToSizeBudget } from '@tracelane/core';
17
+ import { encodeEventsBlob } from './embed.js';
18
+ import { buildMarkdown, extractActionLog } from './markdown.js';
19
+ import { resolveCiMetadata } from './metadata.js';
20
+ import { extractConsole, extractNetwork } from './panels.js';
21
+ import { renderReportHtml } from './template.js';
22
+ /**
23
+ * Build a self-contained HTML report for one test run.
24
+ *
25
+ * @param events the captured rrweb event stream (chronological)
26
+ * @param meta test metadata; CI provenance is auto-filled from the env
27
+ * @returns a complete `.html` document string — write it to disk as-is
28
+ */
29
+ export function buildReport(events, meta, options = {}) {
30
+ const { enforceSizeBudget = true } = options;
31
+ // Keep the report within budget; surface a banner if anything was dropped.
32
+ const { events: sized, pruned } = enforceSizeBudget
33
+ ? pruneToSizeBudget(events)
34
+ : { events, pruned: false };
35
+ const resolvedMeta = resolveCiMetadata(meta);
36
+ const consoleRows = extractConsole(sized);
37
+ const networkRows = extractNetwork(sized);
38
+ const actions = extractActionLog(sized);
39
+ const markdown = buildMarkdown(resolvedMeta, consoleRows, networkRows, actions);
40
+ return renderReportHtml({
41
+ meta: resolvedMeta,
42
+ eventsGzB64: encodeEventsBlob(sized),
43
+ console: consoleRows,
44
+ network: networkRows,
45
+ markdown,
46
+ pruned,
47
+ });
48
+ }
49
+ //# sourceMappingURL=build-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"build-report.js","sourceRoot":"","sources":["../src/build-report.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,yEAAyE;AACzE,8EAA8E;AAC9E,wCAAwC;AACxC,yEAAyE;AACzE,gEAAgE;AAChE,iEAAiE;AACjE,iEAAiE;AACjE,iEAAiE;AACjE,iDAAiD;AACjD,EAAE;AACF,gFAAgF;AAChF,gFAAgF;AAChF,kDAAkD;AAGlD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAC9C,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAYjD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CACzB,MAAuB,EACvB,IAAgB,EAChB,UAA8B,EAAE;IAEhC,MAAM,EAAE,iBAAiB,GAAG,IAAI,EAAE,GAAG,OAAO,CAAC;IAE7C,2EAA2E;IAC3E,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,iBAAiB;QACjD,CAAC,CAAC,iBAAiB,CAAC,MAAM,CAAC;QAC3B,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAE9B,MAAM,YAAY,GAAG,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAC1C,MAAM,OAAO,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;IACxC,MAAM,QAAQ,GAAG,aAAa,CAAC,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,CAAC,CAAC;IAEhF,OAAO,gBAAgB,CAAC;QACtB,IAAI,EAAE,YAAY;QAClB,WAAW,EAAE,gBAAgB,CAAC,KAAK,CAAC;QACpC,OAAO,EAAE,WAAW;QACpB,OAAO,EAAE,WAAW;QACpB,QAAQ;QACR,MAAM;KACP,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,14 @@
1
+ import type { eventWithTime } from '@cubenest/rrweb-core';
2
+ /**
3
+ * Encode an rrweb event array to the base64-of-gzip string embedded in the
4
+ * report as `EVENTS_GZ_B64`. The string uses only the standard base64 alphabet,
5
+ * so it is safe inside a double-quoted JS string literal in the HTML.
6
+ */
7
+ export declare function encodeEventsBlob(events: eventWithTime[]): string;
8
+ /**
9
+ * Decode the embedded blob back to the event array. Node-side mirror of the
10
+ * in-page decompress path; used for the build→embed→decode round-trip test and
11
+ * available to consumers that want to re-read a report's events.
12
+ */
13
+ export declare function decodeEventsBlob(b64: string): eventWithTime[];
14
+ //# sourceMappingURL=embed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA2B1D;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAEhE;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,EAAE,CAE7D"}
package/dist/embed.js ADDED
@@ -0,0 +1,51 @@
1
+ // Events blob embedding (Task 2.9).
2
+ //
3
+ // The pipeline: events --JSON--> gzip (@cubenest/rrweb-core.compress)
4
+ // --base64--> EVENTS_GZ_B64 string in the report.
5
+ //
6
+ // At view time the inlined fflate gunzips the decoded bytes and JSON.parses
7
+ // them — so the report decompresses fully offline (Task 2.9). The wire format
8
+ // is deliberately interoperable with the substrate's compress/decompress pair:
9
+ // base64(compress(events)) on the way in, decompress(base64-decode) on the way
10
+ // out. `decodeEventsBlob` here mirrors what the in-page bootstrap does, and the
11
+ // round-trip is asserted in tests.
12
+ import { compress, decompress } from '@cubenest/rrweb-core';
13
+ // Base64 in 8 KB chunks: `btoa(String.fromCharCode(...all))` overflows the call
14
+ // stack on multi-MB reports (events can approach the 25 MB cap). `btoa` is a
15
+ // web standard available in both Node (16+) and the browser/jsdom.
16
+ const CHUNK = 0x8000;
17
+ /** base64-encode raw bytes without a Buffer dependency or a stack-blowing spread. */
18
+ function bytesToBase64(bytes) {
19
+ let binary = '';
20
+ for (let i = 0; i < bytes.length; i += CHUNK) {
21
+ const slice = bytes.subarray(i, i + CHUNK);
22
+ binary += String.fromCharCode(...slice);
23
+ }
24
+ return btoa(binary);
25
+ }
26
+ /** Inverse of {@link bytesToBase64}. */
27
+ function base64ToBytes(b64) {
28
+ const binary = atob(b64);
29
+ const bytes = new Uint8Array(binary.length);
30
+ for (let i = 0; i < binary.length; i += 1) {
31
+ bytes[i] = binary.charCodeAt(i);
32
+ }
33
+ return bytes;
34
+ }
35
+ /**
36
+ * Encode an rrweb event array to the base64-of-gzip string embedded in the
37
+ * report as `EVENTS_GZ_B64`. The string uses only the standard base64 alphabet,
38
+ * so it is safe inside a double-quoted JS string literal in the HTML.
39
+ */
40
+ export function encodeEventsBlob(events) {
41
+ return bytesToBase64(compress(events));
42
+ }
43
+ /**
44
+ * Decode the embedded blob back to the event array. Node-side mirror of the
45
+ * in-page decompress path; used for the build→embed→decode round-trip test and
46
+ * available to consumers that want to re-read a report's events.
47
+ */
48
+ export function decodeEventsBlob(b64) {
49
+ return decompress(base64ToBytes(b64));
50
+ }
51
+ //# sourceMappingURL=embed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"embed.js","sourceRoot":"","sources":["../src/embed.ts"],"names":[],"mappings":"AAAA,oCAAoC;AACpC,EAAE;AACF,wEAAwE;AACxE,yEAAyE;AACzE,EAAE;AACF,4EAA4E;AAC5E,8EAA8E;AAC9E,+EAA+E;AAC/E,+EAA+E;AAC/E,gFAAgF;AAChF,mCAAmC;AAEnC,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAG5D,gFAAgF;AAChF,6EAA6E;AAC7E,mEAAmE;AACnE,MAAM,KAAK,GAAG,MAAM,CAAC;AAErB,qFAAqF;AACrF,SAAS,aAAa,CAAC,KAAiB;IACtC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;QAC3C,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,GAAG,KAAK,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,wCAAwC;AACxC,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1C,KAAK,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAClC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAuB;IACtD,OAAO,aAAa,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,GAAW;IAC1C,OAAO,UAAU,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC;AACxC,CAAC"}
package/dist/html.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /** HTML-escape a string for safe interpolation into element text / attributes. */
2
+ export declare function escapeHtml(value: string): string;
3
+ /**
4
+ * Serialize a value to JSON safe to embed inside an inline `<script>`. Escapes
5
+ * `<` (covers `</script>` and `<!--`) and the U+2028/U+2029 line terminators
6
+ * that are illegal in JS string literals. The result is valid JSON that direct
7
+ * JS evaluation (or `JSON.parse`) reads back intact.
8
+ */
9
+ export declare function serializeForScript(value: unknown): string;
10
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAWA,kFAAkF;AAClF,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOhD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAKzD"}
package/dist/html.js ADDED
@@ -0,0 +1,32 @@
1
+ // Small HTML/JSON escaping helpers for safe report composition.
2
+ //
3
+ // Two distinct hazards are handled:
4
+ // 1. User/test metadata rendered as HTML text (titles, errors, console
5
+ // messages) must be HTML-escaped so a `<` in an error message can't inject
6
+ // markup — see {@link escapeHtml}.
7
+ // 2. JSON embedded inside an inline `<script>` must have its `</` sequences
8
+ // neutralised so a `</script>` inside string data can't terminate the
9
+ // script element early (the classic inline-JSON XSS) — see
10
+ // {@link serializeForScript}.
11
+ /** HTML-escape a string for safe interpolation into element text / attributes. */
12
+ export function escapeHtml(value) {
13
+ return value
14
+ .replace(/&/g, '&amp;')
15
+ .replace(/</g, '&lt;')
16
+ .replace(/>/g, '&gt;')
17
+ .replace(/"/g, '&quot;')
18
+ .replace(/'/g, '&#39;');
19
+ }
20
+ /**
21
+ * Serialize a value to JSON safe to embed inside an inline `<script>`. Escapes
22
+ * `<` (covers `</script>` and `<!--`) and the U+2028/U+2029 line terminators
23
+ * that are illegal in JS string literals. The result is valid JSON that direct
24
+ * JS evaluation (or `JSON.parse`) reads back intact.
25
+ */
26
+ export function serializeForScript(value) {
27
+ return JSON.stringify(value)
28
+ .replace(/</g, '\\u003c')
29
+ .replace(/\u2028/g, '\\u2028')
30
+ .replace(/\u2029/g, '\\u2029');
31
+ }
32
+ //# sourceMappingURL=html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.js","sourceRoot":"","sources":["../src/html.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,oCAAoC;AACpC,yEAAyE;AACzE,gFAAgF;AAChF,wCAAwC;AACxC,8EAA8E;AAC9E,2EAA2E;AAC3E,gEAAgE;AAChE,mCAAmC;AAEnC,kFAAkF;AAClF,MAAM,UAAU,UAAU,CAAC,KAAa;IACtC,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAc;IAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC;SACzB,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC;SACxB,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC;SAC7B,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;AACnC,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { buildReport } from './build-report.js';
2
+ export type { BuildReportOptions } from './build-report.js';
3
+ export type { ReportMeta, ReportStatus, Viewport } from './types.js';
4
+ export { decodeEventsBlob, encodeEventsBlob } from './embed.js';
5
+ export { extractConsole, extractNetwork, CONSOLE_PLUGIN, NETWORK_EVENT_TAG, NETWORK_CONSOLE_PREFIX, } from './panels.js';
6
+ export type { ConsoleEntry, NetworkEntry } from './panels.js';
7
+ export { resolveCiMetadata } from './metadata.js';
8
+ export { buildMarkdown, extractActionLog, MAX_CONSOLE_MESSAGES, MAX_ACTIONS } from './markdown.js';
9
+ export type { ActionEntry } from './markdown.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAG5D,YAAY,EAAE,UAAU,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGrE,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAGhE,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAG9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACnG,YAAY,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ // Public API surface for @tracelane/report.
2
+ // The report builder: events + metadata -> self-contained offline HTML string.
3
+ export { buildReport } from './build-report.js';
4
+ // Events blob round-trip (Task 2.9) — useful for re-reading a report's events.
5
+ export { decodeEventsBlob, encodeEventsBlob } from './embed.js';
6
+ // Console + network panel extraction (Task 2.10).
7
+ export { extractConsole, extractNetwork, CONSOLE_PLUGIN, NETWORK_EVENT_TAG, NETWORK_CONSOLE_PREFIX, } from './panels.js';
8
+ // CI metadata resolution (Task 2.11).
9
+ export { resolveCiMetadata } from './metadata.js';
10
+ // Copy-as-Markdown payload (Task 2.12).
11
+ export { buildMarkdown, extractActionLog, MAX_CONSOLE_MESSAGES, MAX_ACTIONS } from './markdown.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,4CAA4C;AAE5C,+EAA+E;AAC/E,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAMhD,+EAA+E;AAC/E,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEhE,kDAAkD;AAClD,OAAO,EACL,cAAc,EACd,cAAc,EACd,cAAc,EACd,iBAAiB,EACjB,sBAAsB,GACvB,MAAM,aAAa,CAAC;AAGrB,sCAAsC;AACtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAElD,wCAAwC;AACxC,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { eventWithTime } from '@cubenest/rrweb-core';
2
+ import type { ConsoleEntry, NetworkEntry } from './panels.js';
3
+ import type { ReportMeta } from './types.js';
4
+ /** Max console messages included in the prompt (P1 PRD §F.3: "last 30"). */
5
+ export declare const MAX_CONSOLE_MESSAGES = 30;
6
+ /** Max user actions included in the "steps before failure" section. */
7
+ export declare const MAX_ACTIONS = 20;
8
+ /** One human-readable user action derived from the rrweb stream. */
9
+ export interface ActionEntry {
10
+ description: string;
11
+ timestamp: number;
12
+ }
13
+ /**
14
+ * Walk the event stream for discrete user actions: meaningful mouse
15
+ * interactions (clicks/taps/focus, not raw moves), text input, and
16
+ * `tracelane.nav` navigation boundaries. Mouse-move / scroll noise is excluded —
17
+ * the prompt wants the semantic steps a human or AI would narrate.
18
+ */
19
+ export declare function extractActionLog(events: readonly eventWithTime[]): ActionEntry[];
20
+ /** Build the Markdown prompt (P1 PRD §F.3). */
21
+ export declare function buildMarkdown(meta: ReportMeta, consoleRows: readonly ConsoleEntry[], networkRows: readonly NetworkEntry[], actions: readonly ActionEntry[]): string;
22
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,4EAA4E;AAC5E,eAAO,MAAM,oBAAoB,KAAK,CAAC;AACvC,uEAAuE;AACvE,eAAO,MAAM,WAAW,KAAK,CAAC;AAE9B,oEAAoE;AACpE,MAAM,WAAW,WAAW;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;CACnB;AAqBD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,WAAW,EAAE,CAsBhF;AAMD,+CAA+C;AAC/C,wBAAgB,aAAa,CAC3B,IAAI,EAAE,UAAU,EAChB,WAAW,EAAE,SAAS,YAAY,EAAE,EACpC,WAAW,EAAE,SAAS,YAAY,EAAE,EACpC,OAAO,EAAE,SAAS,WAAW,EAAE,GAC9B,MAAM,CA4CR"}
@@ -0,0 +1,101 @@
1
+ // "Copy as Markdown for AI paste" payload (Task 2.12 / P1 PRD §F.3).
2
+ //
3
+ // Emits the structured prompt that is the product differentiator: failing-test
4
+ // metadata, the last 30 console messages, failed network requests, and the
5
+ // user-action log just before the failure. Built at report-build time and
6
+ // embedded as the `MARKDOWN` const the copy button writes to the clipboard.
7
+ import { EventType, IncrementalSource, MouseInteractions } from '@cubenest/rrweb-core';
8
+ /** Max console messages included in the prompt (P1 PRD §F.3: "last 30"). */
9
+ export const MAX_CONSOLE_MESSAGES = 30;
10
+ /** Max user actions included in the "steps before failure" section. */
11
+ export const MAX_ACTIONS = 20;
12
+ const MOUSE_INTERACTION_LABEL = {
13
+ [MouseInteractions.Click]: 'Click',
14
+ [MouseInteractions.DblClick]: 'Double-click',
15
+ [MouseInteractions.ContextMenu]: 'Right-click',
16
+ [MouseInteractions.Focus]: 'Focus',
17
+ [MouseInteractions.Blur]: 'Blur',
18
+ [MouseInteractions.TouchStart]: 'Touch',
19
+ [MouseInteractions.TouchEnd]: 'Touch end',
20
+ };
21
+ /**
22
+ * Walk the event stream for discrete user actions: meaningful mouse
23
+ * interactions (clicks/taps/focus, not raw moves), text input, and
24
+ * `tracelane.nav` navigation boundaries. Mouse-move / scroll noise is excluded —
25
+ * the prompt wants the semantic steps a human or AI would narrate.
26
+ */
27
+ export function extractActionLog(events) {
28
+ const actions = [];
29
+ for (const e of events) {
30
+ if (e.type === EventType.Custom) {
31
+ const data = e.data;
32
+ if (data.tag === 'tracelane.nav') {
33
+ const url = typeof data.payload?.url === 'string' ? data.payload.url : '(unknown)';
34
+ actions.push({ description: `Navigate to ${url}`, timestamp: e.timestamp });
35
+ }
36
+ continue;
37
+ }
38
+ if (e.type !== EventType.IncrementalSnapshot)
39
+ continue;
40
+ const data = e.data;
41
+ if (data.source === IncrementalSource.MouseInteraction) {
42
+ const kind = typeof data.type === 'number' ? data.type : -1;
43
+ const label = MOUSE_INTERACTION_LABEL[kind];
44
+ if (label)
45
+ actions.push({ description: label, timestamp: e.timestamp });
46
+ }
47
+ else if (data.source === IncrementalSource.Input) {
48
+ actions.push({ description: 'Input text', timestamp: e.timestamp });
49
+ }
50
+ }
51
+ return actions;
52
+ }
53
+ function bullet(line) {
54
+ return `- ${line}`;
55
+ }
56
+ /** Build the Markdown prompt (P1 PRD §F.3). */
57
+ export function buildMarkdown(meta, consoleRows, networkRows, actions) {
58
+ const out = [];
59
+ out.push('## Failing test');
60
+ out.push(bullet(`Spec: ${meta.spec ?? '(unknown)'}`));
61
+ out.push(bullet(`Title: ${meta.title}`));
62
+ out.push(bullet(`Status: ${meta.status}`));
63
+ if (meta.browserName) {
64
+ out.push(bullet(`Browser: ${[meta.browserName, meta.browserVersion].filter(Boolean).join(' ')}`));
65
+ }
66
+ if (meta.commitSha)
67
+ out.push(bullet(`Commit: ${meta.commitSha}`));
68
+ if (meta.error)
69
+ out.push(bullet(`Error: ${meta.error}`));
70
+ out.push('');
71
+ const lastConsole = consoleRows.slice(-MAX_CONSOLE_MESSAGES);
72
+ out.push(`## Last ${lastConsole.length} console messages`);
73
+ if (lastConsole.length === 0) {
74
+ out.push('_None captured._');
75
+ }
76
+ else {
77
+ for (const c of lastConsole)
78
+ out.push(bullet(`[${c.level}] ${c.message}`));
79
+ }
80
+ out.push('');
81
+ out.push('## Failed network requests');
82
+ if (networkRows.length === 0) {
83
+ out.push('_None captured._');
84
+ }
85
+ else {
86
+ for (const n of networkRows) {
87
+ out.push(bullet(`${n.status} ${n.method ? `${n.method} ` : ''}${n.url}`));
88
+ }
89
+ }
90
+ out.push('');
91
+ const lastActions = actions.slice(-MAX_ACTIONS);
92
+ out.push('## Steps just before failure (rrweb action log)');
93
+ if (lastActions.length === 0) {
94
+ out.push('_No user actions captured._');
95
+ }
96
+ else {
97
+ lastActions.forEach((a, i) => out.push(`${i + 1}. ${a.description}`));
98
+ }
99
+ return `${out.join('\n')}\n`;
100
+ }
101
+ //# sourceMappingURL=markdown.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.js","sourceRoot":"","sources":["../src/markdown.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ import type { ReportMeta } from './types.js';
2
+ /**
3
+ * Return `meta` with `commitSha` / `buildUrl` filled from the environment when
4
+ * absent. Explicit values always win; auto-detection only fills gaps.
5
+ */
6
+ export declare function resolveCiMetadata(meta: ReportMeta): ReportMeta;
7
+ /** Render the metadata header markup (P1 PRD §F.1). */
8
+ export declare function renderMetaHeader(meta: ReportMeta): string;
9
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AA4B7C;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAQ9D;AA8BD,uDAAuD;AACvD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA2BzD"}
@@ -0,0 +1,105 @@
1
+ // Metadata header (Task 2.11).
2
+ //
3
+ // Two responsibilities:
4
+ // • resolveCiMetadata — fill commit SHA + build URL from CI env vars when the
5
+ // caller didn't supply them (GITHUB_SHA / CI_COMMIT_SHA, and common build
6
+ // URL conventions for GitHub Actions + GitLab CI).
7
+ // • renderMetaHeader — the <header class="meta"> markup (spec, title, status,
8
+ // duration, browser, viewport, commit, build URL), HTML-escaped.
9
+ import { escapeHtml } from './html.js';
10
+ /** Read an env var without a hard `@types/node` dependency (platform-light). */
11
+ function readEnv(name) {
12
+ const proc = globalThis.process;
13
+ return proc?.env?.[name];
14
+ }
15
+ /** First defined, non-empty env value among `names`. */
16
+ function firstEnv(names) {
17
+ for (const name of names) {
18
+ const v = readEnv(name);
19
+ if (v !== undefined && v !== '')
20
+ return v;
21
+ }
22
+ return undefined;
23
+ }
24
+ /** Derive a CI build/run URL from common provider env vars. */
25
+ function detectBuildUrl() {
26
+ // GitHub Actions: assemble the run URL from the documented pieces.
27
+ const server = readEnv('GITHUB_SERVER_URL');
28
+ const repo = readEnv('GITHUB_REPOSITORY');
29
+ const runId = readEnv('GITHUB_RUN_ID');
30
+ if (server && repo && runId)
31
+ return `${server}/${repo}/actions/runs/${runId}`;
32
+ // GitLab CI exposes the job URL directly.
33
+ return firstEnv(['CI_JOB_URL', 'CI_PIPELINE_URL', 'BUILD_URL']);
34
+ }
35
+ /**
36
+ * Return `meta` with `commitSha` / `buildUrl` filled from the environment when
37
+ * absent. Explicit values always win; auto-detection only fills gaps.
38
+ */
39
+ export function resolveCiMetadata(meta) {
40
+ const commitSha = meta.commitSha ?? firstEnv(['GITHUB_SHA', 'CI_COMMIT_SHA']);
41
+ const buildUrl = meta.buildUrl ?? detectBuildUrl();
42
+ return {
43
+ ...meta,
44
+ ...(commitSha !== undefined ? { commitSha } : {}),
45
+ ...(buildUrl !== undefined ? { buildUrl } : {}),
46
+ };
47
+ }
48
+ function formatDuration(ms) {
49
+ if (ms < 1000)
50
+ return `${ms} ms`;
51
+ const s = ms / 1000;
52
+ if (s < 60)
53
+ return `${s.toFixed(s < 10 ? 2 : 1)} s`;
54
+ const m = Math.floor(s / 60);
55
+ return `${m}m ${Math.round(s - m * 60)}s`;
56
+ }
57
+ function row(term, value) {
58
+ return `<dt>${escapeHtml(term)}</dt><dd>${value}</dd>`;
59
+ }
60
+ /**
61
+ * Whether `url` is safe to use as an `href`. Only http(s) — blocks `javascript:`,
62
+ * `data:`, `vbscript:` etc. The build URL is sourced from CI env vars
63
+ * (CI_JOB_URL / BUILD_URL), which a hostile or misconfigured CI could set, so a
64
+ * crafted `javascript:` value must never become a clickable link in the saved
65
+ * report's origin. (HTML-escaping alone doesn't help — `javascript:alert(1)`
66
+ * has none of the escaped characters.)
67
+ */
68
+ function isSafeUrl(url) {
69
+ try {
70
+ return /^https?:$/i.test(new URL(url).protocol);
71
+ }
72
+ catch {
73
+ return false;
74
+ }
75
+ }
76
+ /** Render the metadata header markup (P1 PRD §F.1). */
77
+ export function renderMetaHeader(meta) {
78
+ const rows = [];
79
+ if (meta.spec)
80
+ rows.push(row('Spec', escapeHtml(meta.spec)));
81
+ if (meta.durationMs !== undefined)
82
+ rows.push(row('Duration', escapeHtml(formatDuration(meta.durationMs))));
83
+ const browser = [meta.browserName, meta.browserVersion].filter(Boolean).join(' ');
84
+ if (browser)
85
+ rows.push(row('Browser', escapeHtml(browser)));
86
+ if (meta.viewport) {
87
+ rows.push(row('Viewport', escapeHtml(`${meta.viewport.width} × ${meta.viewport.height}`)));
88
+ }
89
+ if (meta.commitSha)
90
+ rows.push(row('Commit', `<code>${escapeHtml(meta.commitSha)}</code>`));
91
+ if (meta.buildUrl) {
92
+ const escaped = escapeHtml(meta.buildUrl);
93
+ // Only render a clickable link for http(s); a non-http(s) URL (e.g. a
94
+ // crafted `javascript:`) is shown as escaped text with no href.
95
+ const value = isSafeUrl(meta.buildUrl) ? `<a href="${escaped}">${escaped}</a>` : escaped;
96
+ rows.push(row('Build', value));
97
+ }
98
+ const error = meta.error ? `<div class="error">${escapeHtml(meta.error)}</div>` : '';
99
+ return `<header class="meta">
100
+ <h1>${escapeHtml(meta.title)} <span class="status ${escapeHtml(meta.status)}">${escapeHtml(meta.status)}</span></h1>
101
+ ${rows.length ? `<dl>${rows.join('')}</dl>` : ''}
102
+ ${error}
103
+ </header>`;
104
+ }
105
+ //# sourceMappingURL=metadata.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import type { eventWithTime } from '@cubenest/rrweb-core';
2
+ /** A console panel row. */
3
+ export interface ConsoleEntry {
4
+ level: string;
5
+ message: string;
6
+ timestamp: number;
7
+ }
8
+ /** A network-error panel row. */
9
+ export interface NetworkEntry {
10
+ method?: string;
11
+ url: string;
12
+ status: number;
13
+ timestamp: number;
14
+ }
15
+ /** rrweb console plugin tag (P1 PRD §F.3). */
16
+ export declare const CONSOLE_PLUGIN = "rrweb/console@1";
17
+ /** Custom-event tag for the v1.1 rich network path (P1 PRD §E.3). */
18
+ export declare const NETWORK_EVENT_TAG = "tracelane.test.network-error";
19
+ /** console.error prefix for the v1 network fallback (P1 PRD §E.2). */
20
+ export declare const NETWORK_CONSOLE_PREFIX = "[tracelane.net]";
21
+ /**
22
+ * Extract console rows: EventType.Plugin events emitted by the rrweb console
23
+ * plugin. The plugin nests the level + serialized args under `data.payload`.
24
+ */
25
+ export declare function extractConsole(events: readonly eventWithTime[]): ConsoleEntry[];
26
+ /**
27
+ * Extract network-error rows. Prefers the v1.1 custom-event path
28
+ * (EventType.Custom, tag 'tracelane.test.network-error'); when none are present,
29
+ * falls back to scraping console.error lines prefixed '[tracelane.net]' (v1).
30
+ */
31
+ export declare function extractNetwork(events: readonly eventWithTime[]): NetworkEntry[];
32
+ //# sourceMappingURL=panels.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panels.d.ts","sourceRoot":"","sources":["../src/panels.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAE1D,2BAA2B;AAC3B,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,iCAAiC;AACjC,MAAM,WAAW,YAAY;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,8CAA8C;AAC9C,eAAO,MAAM,cAAc,oBAAoB,CAAC;AAChD,qEAAqE;AACrE,eAAO,MAAM,iBAAiB,iCAAiC,CAAC;AAChE,sEAAsE;AACtE,eAAO,MAAM,sBAAsB,oBAAoB,CAAC;AAkDxD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,YAAY,EAAE,CAW/E;AAkBD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,SAAS,aAAa,EAAE,GAAG,YAAY,EAAE,CA2B/E"}
package/dist/panels.js ADDED
@@ -0,0 +1,125 @@
1
+ // Console + network panel extraction (Task 2.10).
2
+ //
3
+ // Pure, build-time functions that walk the rrweb event stream and produce the
4
+ // compact row arrays embedded into the report (the in-page bootstrap only
5
+ // renders them — no event filtering happens in the browser). Extracting at
6
+ // build time keeps all the parsing logic unit-testable and the runtime thin.
7
+ //
8
+ // Sources (P1 PRD §F.3 / §E.2):
9
+ // • Console — EventType.Plugin (6) with data.plugin === 'rrweb/console@1'.
10
+ // • Network — EventType.Custom (5) with data.tag === 'tracelane.test.network-error'
11
+ // (the v1.1 rich path); falls back to scraping console.error messages
12
+ // prefixed '[tracelane.net]' (the v1 path) when no custom events exist.
13
+ import { EventType } from '@cubenest/rrweb-core';
14
+ /** rrweb console plugin tag (P1 PRD §F.3). */
15
+ export const CONSOLE_PLUGIN = 'rrweb/console@1';
16
+ /** Custom-event tag for the v1.1 rich network path (P1 PRD §E.3). */
17
+ export const NETWORK_EVENT_TAG = 'tracelane.test.network-error';
18
+ /** console.error prefix for the v1 network fallback (P1 PRD §E.2). */
19
+ export const NETWORK_CONSOLE_PREFIX = '[tracelane.net]';
20
+ function isPlugin(e) {
21
+ return e.type === EventType.Plugin;
22
+ }
23
+ function isCustom(e) {
24
+ return e.type === EventType.Custom;
25
+ }
26
+ /** Coerce an arbitrary console-arg payload into a flat display string. */
27
+ function stringifyArgs(payload) {
28
+ if (Array.isArray(payload)) {
29
+ return payload
30
+ .map((a) => (typeof a === 'string' ? stripQuotes(a) : safeStringify(a)))
31
+ .join(' ');
32
+ }
33
+ return typeof payload === 'string' ? payload : safeStringify(payload);
34
+ }
35
+ // The console plugin serializes string args as JSON (so they arrive quoted,
36
+ // e.g. '"hello"'); unwrap a single layer of quoting for readability.
37
+ function stripQuotes(s) {
38
+ if (s.length >= 2 && s[0] === '"' && s[s.length - 1] === '"') {
39
+ try {
40
+ const parsed = JSON.parse(s);
41
+ if (typeof parsed === 'string')
42
+ return parsed;
43
+ }
44
+ catch {
45
+ // not JSON — fall through
46
+ }
47
+ }
48
+ return s;
49
+ }
50
+ function safeStringify(value) {
51
+ try {
52
+ return JSON.stringify(value) ?? String(value);
53
+ }
54
+ catch {
55
+ return String(value);
56
+ }
57
+ }
58
+ /**
59
+ * Extract console rows: EventType.Plugin events emitted by the rrweb console
60
+ * plugin. The plugin nests the level + serialized args under `data.payload`.
61
+ */
62
+ export function extractConsole(events) {
63
+ const rows = [];
64
+ for (const e of events) {
65
+ if (!isPlugin(e))
66
+ continue;
67
+ const data = e.data;
68
+ if (data.plugin !== CONSOLE_PLUGIN)
69
+ continue;
70
+ const level = typeof data.payload?.level === 'string' ? data.payload.level : 'log';
71
+ const message = stringifyArgs(data.payload?.payload);
72
+ rows.push({ level, message, timestamp: e.timestamp });
73
+ }
74
+ return rows;
75
+ }
76
+ /** Parse a '[tracelane.net] <METHOD> <STATUS> <URL>' console line, if it is one. */
77
+ function parseNetConsoleLine(message, timestamp) {
78
+ const idx = message.indexOf(NETWORK_CONSOLE_PREFIX);
79
+ if (idx === -1)
80
+ return undefined;
81
+ const rest = message.slice(idx + NETWORK_CONSOLE_PREFIX.length).trim();
82
+ // "GET 404 https://…" or "404 https://…"
83
+ const m = rest.match(/^(?:([A-Z]+)\s+)?(\d{3})\s+(\S+)/);
84
+ if (!m)
85
+ return undefined;
86
+ return {
87
+ ...(m[1] !== undefined ? { method: m[1] } : {}),
88
+ status: Number(m[2]),
89
+ url: m[3] ?? '',
90
+ timestamp,
91
+ };
92
+ }
93
+ /**
94
+ * Extract network-error rows. Prefers the v1.1 custom-event path
95
+ * (EventType.Custom, tag 'tracelane.test.network-error'); when none are present,
96
+ * falls back to scraping console.error lines prefixed '[tracelane.net]' (v1).
97
+ */
98
+ export function extractNetwork(events) {
99
+ const rich = [];
100
+ for (const e of events) {
101
+ if (!isCustom(e))
102
+ continue;
103
+ const data = e.data;
104
+ if (data.tag !== NETWORK_EVENT_TAG)
105
+ continue;
106
+ const p = (data.payload ?? {});
107
+ rich.push({
108
+ ...(typeof p.method === 'string' ? { method: p.method } : {}),
109
+ url: typeof p.url === 'string' ? p.url : '',
110
+ status: typeof p.status === 'number' ? p.status : 0,
111
+ timestamp: e.timestamp,
112
+ });
113
+ }
114
+ if (rich.length > 0)
115
+ return rich;
116
+ // v1 fallback: scrape the console.
117
+ const scraped = [];
118
+ for (const row of extractConsole(events)) {
119
+ const parsed = parseNetConsoleLine(row.message, row.timestamp);
120
+ if (parsed)
121
+ scraped.push(parsed);
122
+ }
123
+ return scraped;
124
+ }
125
+ //# sourceMappingURL=panels.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panels.js","sourceRoot":"","sources":["../src/panels.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { ConsoleEntry, NetworkEntry } from './panels.js';
2
+ import type { ReportMeta } from './types.js';
3
+ /** Everything `renderReportHtml` needs; build-report.ts prepares it. */
4
+ export interface ReportTemplateData {
5
+ meta: ReportMeta;
6
+ /** base64(gzip(events)) — decompressed in-page for the player. */
7
+ eventsGzB64: string;
8
+ /** Extracted console panel rows (Task 2.10). */
9
+ console: ConsoleEntry[];
10
+ /** Extracted network panel rows (Task 2.10). */
11
+ network: NetworkEntry[];
12
+ /** Pre-rendered "Copy as Markdown for AI paste" payload (Task 2.12). */
13
+ markdown: string;
14
+ /** Whether the events were pruned to fit the size budget (ADR-0005 banner). */
15
+ pruned: boolean;
16
+ }
17
+ /** Compose the full self-contained HTML document. */
18
+ export declare function renderReportHtml(data: ReportTemplateData): string;
19
+ //# sourceMappingURL=template.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.d.ts","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,UAAU,CAAC;IACjB,kEAAkE;IAClE,WAAW,EAAE,MAAM,CAAC;IACpB,gDAAgD;IAChD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,gDAAgD;IAChD,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,wEAAwE;IACxE,QAAQ,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,MAAM,EAAE,OAAO,CAAC;CACjB;AA6GD,qDAAqD;AACrD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,CA4CjE"}
@@ -0,0 +1,158 @@
1
+ // HTML composition for the self-contained report (P1 PRD §F.1).
2
+ //
3
+ // `renderReportHtml` assembles the single-file document from already-prepared
4
+ // pieces (the caller — build-report.ts — does the extraction + encoding). This
5
+ // module owns the static shell: the report CSS, the metadata header markup, the
6
+ // panel containers, and the in-page bootstrap script. The large vendored assets
7
+ // (player UMD/CSS, fflate UMD) and the data payloads are passed in.
8
+ import { loadFflateGunzipSource, loadPlayerCss, loadPlayerUmd } from './assets.js';
9
+ import { escapeHtml, serializeForScript } from './html.js';
10
+ import { renderMetaHeader } from './metadata.js';
11
+ /** Report shell CSS (~ a few KB). Kept terse; no external fonts or assets. */
12
+ const SHELL_CSS = `
13
+ :root { color-scheme: light dark; --fg:#1a1a1a; --bg:#fff; --muted:#666; --border:#e2e2e2; --accent:#2563eb; --err:#dc2626; --warn:#d97706; --panel:#fafafa; }
14
+ @media (prefers-color-scheme: dark){ :root { --fg:#e6e6e6; --bg:#161616; --muted:#9a9a9a; --border:#2c2c2c; --accent:#60a5fa; --err:#f87171; --warn:#fbbf24; --panel:#1e1e1e; } }
15
+ * { box-sizing: border-box; }
16
+ body { margin:0; font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; color:var(--fg); background:var(--bg); }
17
+ header.meta { padding:12px 16px; border-bottom:1px solid var(--border); }
18
+ header.meta h1 { margin:0 0 6px; font-size:16px; }
19
+ header.meta .status { display:inline-block; padding:1px 8px; border-radius:10px; font-size:12px; font-weight:600; text-transform:uppercase; }
20
+ header.meta .status.failed,header.meta .status.broken { background:var(--err); color:#fff; }
21
+ header.meta .status.passed { background:#16a34a; color:#fff; }
22
+ header.meta .status.skipped { background:var(--muted); color:#fff; }
23
+ header.meta dl { display:grid; grid-template-columns:max-content 1fr; gap:2px 12px; margin:8px 0 0; font-size:13px; }
24
+ header.meta dt { color:var(--muted); }
25
+ header.meta dd { margin:0; word-break:break-word; }
26
+ header.meta .error { margin-top:8px; padding:8px; border-left:3px solid var(--err); background:var(--panel); white-space:pre-wrap; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
27
+ .banner { padding:6px 16px; background:var(--warn); color:#1a1a1a; font-size:13px; }
28
+ .toolbar { padding:8px 16px; border-bottom:1px solid var(--border); }
29
+ button.copy-md { font:inherit; padding:6px 12px; border:1px solid var(--accent); background:var(--accent); color:#fff; border-radius:6px; cursor:pointer; }
30
+ button.copy-md:active { opacity:.8; }
31
+ main { display:flex; gap:0; align-items:stretch; min-height:60vh; }
32
+ #player { flex:1 1 auto; min-width:0; padding:12px; overflow:auto; }
33
+ aside#panels { flex:0 0 360px; border-left:1px solid var(--border); display:flex; flex-direction:column; overflow:hidden; }
34
+ aside#panels section { display:flex; flex-direction:column; min-height:0; flex:1 1 50%; }
35
+ aside#panels h2 { margin:0; padding:8px 12px; font-size:13px; background:var(--panel); border-bottom:1px solid var(--border); position:sticky; top:0; }
36
+ .rows { overflow:auto; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; font-size:12px; }
37
+ .row { padding:4px 12px; border-bottom:1px solid var(--border); white-space:pre-wrap; word-break:break-word; }
38
+ .row.error { color:var(--err); } .row.warn { color:var(--warn); }
39
+ .row .lvl { font-weight:600; margin-right:6px; }
40
+ .row .st { font-weight:600; margin-right:6px; } .row .st4,.row .st5 { color:var(--err); }
41
+ .empty { padding:12px; color:var(--muted); font-style:italic; }
42
+ `;
43
+ /**
44
+ * The in-page bootstrap (runs at view time, plain ES5-ish JS so it executes in
45
+ * any browser without a build step). Reads the embedded payloads, decompresses
46
+ * the events with the inlined fflate, mounts rrweb-player, renders the panels,
47
+ * and wires the copy-as-markdown button. Authored as a single string so it ships
48
+ * verbatim in a `<script>` — it must not reference any TS/Node symbol.
49
+ */
50
+ const BOOTSTRAP = `
51
+ (function () {
52
+ function decodeEvents(b64) {
53
+ var bin = atob(b64);
54
+ var bytes = new Uint8Array(bin.length);
55
+ for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
56
+ var json = fflate.strFromU8(fflate.gunzipSync(bytes));
57
+ return JSON.parse(json);
58
+ }
59
+
60
+ var events = decodeEvents(EVENTS_GZ_B64);
61
+
62
+ // rrweb-player needs at least two events to compute a timeline; guard so a
63
+ // truncated/empty capture degrades to a message instead of throwing.
64
+ var playerEl = document.getElementById('player');
65
+ if (events.length >= 2 && typeof rrwebPlayer !== 'undefined') {
66
+ new rrwebPlayer({ target: playerEl, props: { events: events, showController: true, autoPlay: false } });
67
+ } else {
68
+ playerEl.innerHTML = '<p class="empty">Not enough recorded events to replay.</p>';
69
+ }
70
+
71
+ function el(tag, cls, text) {
72
+ var n = document.createElement(tag);
73
+ if (cls) n.className = cls;
74
+ if (text != null) n.textContent = text;
75
+ return n;
76
+ }
77
+
78
+ function renderConsole(container, rows) {
79
+ if (!rows.length) { container.appendChild(el('div', 'empty', 'No console output captured.')); return; }
80
+ for (var i = 0; i < rows.length; i++) {
81
+ var r = rows[i];
82
+ var row = el('div', 'row ' + (r.level || 'log'));
83
+ row.appendChild(el('span', 'lvl', (r.level || 'log').toUpperCase()));
84
+ row.appendChild(document.createTextNode(r.message));
85
+ container.appendChild(row);
86
+ }
87
+ }
88
+
89
+ function renderNetwork(container, rows) {
90
+ if (!rows.length) { container.appendChild(el('div', 'empty', 'No failed network requests captured.')); return; }
91
+ for (var i = 0; i < rows.length; i++) {
92
+ var r = rows[i];
93
+ var row = el('div', 'row');
94
+ row.appendChild(el('span', 'st st' + String(r.status).charAt(0), String(r.status)));
95
+ row.appendChild(document.createTextNode((r.method ? r.method + ' ' : '') + r.url));
96
+ container.appendChild(row);
97
+ }
98
+ }
99
+
100
+ renderConsole(document.getElementById('console-rows'), CONSOLE);
101
+ renderNetwork(document.getElementById('network-rows'), NETWORK);
102
+
103
+ var btn = document.getElementById('copy-md');
104
+ if (btn) {
105
+ btn.addEventListener('click', function () {
106
+ var done = function () { var t = btn.textContent; btn.textContent = 'Copied!'; setTimeout(function () { btn.textContent = t; }, 1500); };
107
+ if (navigator.clipboard && navigator.clipboard.writeText) {
108
+ navigator.clipboard.writeText(MARKDOWN).then(done, function () { window.prompt('Copy the Markdown below:', MARKDOWN); });
109
+ } else {
110
+ window.prompt('Copy the Markdown below:', MARKDOWN);
111
+ }
112
+ });
113
+ }
114
+ })();
115
+ `;
116
+ /** Compose the full self-contained HTML document. */
117
+ export function renderReportHtml(data) {
118
+ const { meta, eventsGzB64, console: consoleRows, network, markdown, pruned } = data;
119
+ const title = `tracelane — ${meta.spec ?? '(no spec)'} :: ${meta.title} (${meta.status})`;
120
+ const banner = pruned
121
+ ? '<div class="banner">Some recorded events were pruned to fit the 25 MB report budget — replay may skip detail.</div>'
122
+ : '';
123
+ // Data payloads embedded as JS consts, all escaped for inline-script safety.
124
+ // The events blob is base64 (already inline-safe); the rest go through
125
+ // serializeForScript to neutralise any `</script>` in user data.
126
+ const dataScript = `const META = ${serializeForScript(meta)};\n` +
127
+ `const EVENTS_GZ_B64 = "${eventsGzB64}";\n` +
128
+ `const CONSOLE = ${serializeForScript(consoleRows)};\n` +
129
+ `const NETWORK = ${serializeForScript(network)};\n` +
130
+ `const MARKDOWN = ${serializeForScript(markdown)};`;
131
+ return `<!doctype html>
132
+ <html lang="en">
133
+ <head>
134
+ <meta charset="utf-8">
135
+ <meta name="viewport" content="width=device-width, initial-scale=1">
136
+ <title>${escapeHtml(title)}</title>
137
+ <style>${loadPlayerCss()}</style>
138
+ <style>${SHELL_CSS}</style>
139
+ </head>
140
+ <body>
141
+ ${renderMetaHeader(meta)}
142
+ ${banner}
143
+ <div class="toolbar"><button id="copy-md" class="copy-md" type="button">Copy as Markdown for AI paste</button></div>
144
+ <main>
145
+ <section id="player" aria-label="Session replay"></section>
146
+ <aside id="panels">
147
+ <section aria-label="Console"><h2>Console</h2><div id="console-rows" class="rows"></div></section>
148
+ <section aria-label="Network"><h2>Network errors</h2><div id="network-rows" class="rows"></div></section>
149
+ </aside>
150
+ </main>
151
+ <script>${loadFflateGunzipSource()}</script>
152
+ <script>${loadPlayerUmd()}</script>
153
+ <script>${dataScript}</script>
154
+ <script>${BOOTSTRAP}</script>
155
+ </body>
156
+ </html>`;
157
+ }
158
+ //# sourceMappingURL=template.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template.js","sourceRoot":"","sources":["../src/template.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAChE,EAAE;AACF,8EAA8E;AAC9E,+EAA+E;AAC/E,gFAAgF;AAChF,gFAAgF;AAChF,oEAAoE;AAEpE,OAAO,EAAE,sBAAsB,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAmBjD,8EAA8E;AAC9E,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8BjB,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,SAAS,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiEjB,CAAC;AAEF,qDAAqD;AACrD,MAAM,UAAU,gBAAgB,CAAC,IAAwB;IACvD,MAAM,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAEpF,MAAM,KAAK,GAAG,eAAe,IAAI,CAAC,IAAI,IAAI,WAAW,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,MAAM,GAAG,CAAC;IAC1F,MAAM,MAAM,GAAG,MAAM;QACnB,CAAC,CAAC,qHAAqH;QACvH,CAAC,CAAC,EAAE,CAAC;IAEP,6EAA6E;IAC7E,uEAAuE;IACvE,iEAAiE;IACjE,MAAM,UAAU,GACd,gBAAgB,kBAAkB,CAAC,IAAI,CAAC,KAAK;QAC7C,0BAA0B,WAAW,MAAM;QAC3C,mBAAmB,kBAAkB,CAAC,WAAW,CAAC,KAAK;QACvD,mBAAmB,kBAAkB,CAAC,OAAO,CAAC,KAAK;QACnD,oBAAoB,kBAAkB,CAAC,QAAQ,CAAC,GAAG,CAAC;IAEtD,OAAO;;;;;SAKA,UAAU,CAAC,KAAK,CAAC;SACjB,aAAa,EAAE;SACf,SAAS;;;EAGhB,gBAAgB,CAAC,IAAI,CAAC;EACtB,MAAM;;;;;;;;;UASE,sBAAsB,EAAE;UACxB,aAAa,EAAE;UACf,UAAU;UACV,SAAS;;QAEX,CAAC;AACT,CAAC"}
@@ -0,0 +1,36 @@
1
+ /** Test outcome rendered in the report header + markdown export. */
2
+ export type ReportStatus = 'passed' | 'failed' | 'skipped' | 'broken';
3
+ /** Recording viewport, surfaced in the metadata header. */
4
+ export interface Viewport {
5
+ width: number;
6
+ height: number;
7
+ }
8
+ /**
9
+ * Metadata for a single test report (P1 PRD §F.1 / §F.3). All fields except
10
+ * `title` and `status` are optional — adapters fill what they can, and CI
11
+ * provenance (commit SHA, build URL) is auto-detected from the environment
12
+ * when omitted (see {@link resolveCiMetadata}).
13
+ */
14
+ export interface ReportMeta {
15
+ /** Spec file path, e.g. `test/login.spec.ts`. */
16
+ spec?: string;
17
+ /** Test title, e.g. `logs in with valid credentials`. */
18
+ title: string;
19
+ /** Test outcome. */
20
+ status: ReportStatus;
21
+ /** Failure message, when the test failed/broke. */
22
+ error?: string;
23
+ /** Total test duration in milliseconds. */
24
+ durationMs?: number;
25
+ /** Browser name, e.g. `chrome`. */
26
+ browserName?: string;
27
+ /** Browser version, e.g. `124.0.6367.78`. */
28
+ browserVersion?: string;
29
+ /** Recording viewport. */
30
+ viewport?: Viewport;
31
+ /** Commit SHA. Auto-detected from `GITHUB_SHA` / `CI_COMMIT_SHA` when omitted. */
32
+ commitSha?: string;
33
+ /** CI build URL. Auto-detected from common CI env vars when omitted. */
34
+ buildUrl?: string;
35
+ }
36
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,oEAAoE;AACpE,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;AAEtE,2DAA2D;AAC3D,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IACzB,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,oBAAoB;IACpB,MAAM,EAAE,YAAY,CAAC;IACrB,mDAAmD;IACnD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0BAA0B;IAC1B,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ // Public types for the report builder.
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,uCAAuC"}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@tracelane/report",
3
+ "version": "0.1.0-alpha.1",
4
+ "description": "Self-contained, offline HTML report builder for tracelane. Embeds the rrweb player and a gzipped event blob into a single .html file.",
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
+ "@tracelane/core": "workspace:*",
24
+ "fflate": "^0.8.2",
25
+ "rrweb-player": "1.0.0-alpha.4"
26
+ },
27
+ "devDependencies": {
28
+ "jsdom": "^25.0.1"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/Cubenest/rrweb-stack.git",
37
+ "directory": "packages/tracelane-report"
38
+ },
39
+ "homepage": "https://github.com/Cubenest/rrweb-stack/tree/main/packages/tracelane-report#readme"
40
+ }