@tracelane/wdio 0.1.0-alpha.2 → 0.1.0-alpha.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -40
- package/dist/hooks.d.ts +8 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +9 -4
- package/dist/hooks.js.map +1 -1
- package/dist/options.d.ts +52 -2
- package/dist/options.d.ts.map +1 -1
- package/dist/options.js.map +1 -1
- package/dist/rrweb-bundle.js +49 -49
- package/dist/security-suppress.d.ts +10 -0
- package/dist/security-suppress.d.ts.map +1 -0
- package/dist/security-suppress.js +58 -0
- package/dist/security-suppress.js.map +1 -0
- package/dist/service.d.ts +33 -8
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +36 -7
- package/dist/service.js.map +1 -1
- package/dist/tracelane-session.d.ts +11 -1
- package/dist/tracelane-session.d.ts.map +1 -1
- package/dist/tracelane-session.js +44 -8
- package/dist/tracelane-session.js.map +1 -1
- package/package.json +28 -5
- package/dist/inpage-bundle.d.ts +0 -9
- package/dist/inpage-bundle.d.ts.map +0 -1
- package/dist/inpage-bundle.js +0 -47
- package/dist/inpage-bundle.js.map +0 -1
- package/dist/network-capture.d.ts +0 -25
- package/dist/network-capture.d.ts.map +0 -1
- package/dist/network-capture.js +0 -56
- package/dist/network-capture.js.map +0 -1
- package/dist/report-writer.d.ts +0 -36
- package/dist/report-writer.d.ts.map +0 -1
- package/dist/report-writer.js +0 -56
- package/dist/report-writer.js.map +0 -1
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Suppression } from '@tracelane/security';
|
|
2
|
+
/** The fixed filename looked up in the project cwd. */
|
|
3
|
+
export declare const SUPPRESS_FILE_NAME = "tracelane.security.suppress.json";
|
|
4
|
+
/**
|
|
5
|
+
* Load `tracelane.security.suppress.json` from `cwd`, if present. Returns the
|
|
6
|
+
* parsed suppression rules, or `[]` when the file is missing/unreadable/
|
|
7
|
+
* malformed/wrong-shaped. Never throws.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadSecuritySuppressions(cwd: string): Suppression[];
|
|
10
|
+
//# sourceMappingURL=security-suppress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-suppress.d.ts","sourceRoot":"","sources":["../src/security-suppress.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAEvD,uDAAuD;AACvD,eAAO,MAAM,kBAAkB,qCAAqC,CAAC;AA0BrE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,EAAE,CAUnE"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Optional security-suppression file loader (Task 13).
|
|
2
|
+
//
|
|
3
|
+
// tracelane's advisory security analyzer (`@tracelane/security`) accepts a list
|
|
4
|
+
// of `Suppression` rules to silence known-acceptable signals. To let teams
|
|
5
|
+
// commit those rules alongside their suite, the wdio adapter looks for a
|
|
6
|
+
// `tracelane.security.suppress.json` in the project cwd at report-write time.
|
|
7
|
+
//
|
|
8
|
+
// This loader is deliberately defensive: a missing, unreadable, malformed, or
|
|
9
|
+
// wrong-shaped file MUST NEVER throw and MUST NEVER break the report. Any of
|
|
10
|
+
// those cases degrade to `[]` (no suppressions). The file only ever carries
|
|
11
|
+
// advisory `{ signal?, evidence? }` rules — no secrets — so reading it from cwd
|
|
12
|
+
// is safe (P1 security MVP privacy invariant).
|
|
13
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
/** The fixed filename looked up in the project cwd. */
|
|
16
|
+
export const SUPPRESS_FILE_NAME = 'tracelane.security.suppress.json';
|
|
17
|
+
/**
|
|
18
|
+
* Coerce arbitrary parsed JSON into a `Suppression[]`, leniently:
|
|
19
|
+
* - a bare array → used as-is;
|
|
20
|
+
* - an object with a `suppressions` array → that array;
|
|
21
|
+
* - anything else → `[]`.
|
|
22
|
+
*
|
|
23
|
+
* Element shape is not validated beyond "is an object" — the analyzer reads
|
|
24
|
+
* only `signal` / `evidence` and ignores the rest, so a loose pass-through is
|
|
25
|
+
* both safe and forgiving of hand-edited files.
|
|
26
|
+
*/
|
|
27
|
+
function coerceSuppressions(parsed) {
|
|
28
|
+
const arr = Array.isArray(parsed)
|
|
29
|
+
? parsed
|
|
30
|
+
: isRecord(parsed) && Array.isArray(parsed.suppressions)
|
|
31
|
+
? parsed.suppressions
|
|
32
|
+
: undefined;
|
|
33
|
+
if (!arr)
|
|
34
|
+
return [];
|
|
35
|
+
return arr.filter(isRecord);
|
|
36
|
+
}
|
|
37
|
+
function isRecord(v) {
|
|
38
|
+
return typeof v === 'object' && v !== null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Load `tracelane.security.suppress.json` from `cwd`, if present. Returns the
|
|
42
|
+
* parsed suppression rules, or `[]` when the file is missing/unreadable/
|
|
43
|
+
* malformed/wrong-shaped. Never throws.
|
|
44
|
+
*/
|
|
45
|
+
export function loadSecuritySuppressions(cwd) {
|
|
46
|
+
const filePath = join(cwd, SUPPRESS_FILE_NAME);
|
|
47
|
+
try {
|
|
48
|
+
if (!existsSync(filePath))
|
|
49
|
+
return [];
|
|
50
|
+
const raw = readFileSync(filePath, 'utf8');
|
|
51
|
+
return coerceSuppressions(JSON.parse(raw));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Missing/unreadable/malformed file must never break the report.
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=security-suppress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"security-suppress.js","sourceRoot":"","sources":["../src/security-suppress.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,EAAE;AACF,gFAAgF;AAChF,2EAA2E;AAC3E,yEAAyE;AACzE,8EAA8E;AAC9E,EAAE;AACF,8EAA8E;AAC9E,6EAA6E;AAC7E,4EAA4E;AAC5E,gFAAgF;AAChF,+CAA+C;AAE/C,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,uDAAuD;AACvD,MAAM,CAAC,MAAM,kBAAkB,GAAG,kCAAkC,CAAC;AAErE;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CAAC,MAAe;IACzC,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAC/B,CAAC,CAAC,MAAM;QACR,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,YAAY,CAAC;YACtD,CAAC,CAAC,MAAM,CAAC,YAAY;YACrB,CAAC,CAAC,SAAS,CAAC;IAChB,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAC;IACpB,OAAO,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAkB,CAAC;AAC/C,CAAC;AAED,SAAS,QAAQ,CAAC,CAAU;IAC1B,OAAO,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC;AAC7C,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAW;IAClD,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,kBAAkB,CAAC,CAAC;IAC/C,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;YAAE,OAAO,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QAC3C,OAAO,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,iEAAiE;QACjE,OAAO,EAAE,CAAC;IACZ,CAAC;AACH,CAAC"}
|
package/dist/service.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Frameworks, Services } from '@wdio/types';
|
|
1
|
+
import type { Capabilities, Frameworks, Options, Services } from '@wdio/types';
|
|
2
2
|
import type { TraceLaneOptions } from './options.js';
|
|
3
3
|
/**
|
|
4
4
|
* The tracelane WebdriverIO Service. Implements the worker + launcher hooks from
|
|
@@ -6,18 +6,27 @@ import type { TraceLaneOptions } from './options.js';
|
|
|
6
6
|
* `afterScenario`) are declared as plain methods since `@wdio/types` doesn't put
|
|
7
7
|
* them on `ServiceInstance` (they only exist at runtime under the Cucumber
|
|
8
8
|
* framework).
|
|
9
|
+
*
|
|
10
|
+
* The constructor signature deliberately matches `Services.ServiceClass`:
|
|
11
|
+
* `new (options: WebdriverIO.ServiceOption,
|
|
12
|
+
* capabilities: ResolvedTestrunnerCapabilities,
|
|
13
|
+
* config: Options.Testrunner): ServiceInstance`
|
|
14
|
+
* (T-4 fix, 2026-05-28 QA walk). Earlier intersection-typing on `options` was
|
|
15
|
+
* not enough — the `capabilities` and `config` parameters had to ALSO be
|
|
16
|
+
* compatible with the interface, otherwise registering the class as
|
|
17
|
+
* `services: [[TraceLaneService, { ... }]]` raised the "not assignable to
|
|
18
|
+
* ServiceClass" error in the user's wdio.conf.ts. With the full triple in
|
|
19
|
+
* place the tuple form typechecks without `@ts-expect-error`.
|
|
9
20
|
*/
|
|
10
21
|
export default class TraceLaneService implements Services.ServiceInstance {
|
|
11
22
|
private readonly session;
|
|
12
23
|
/**
|
|
13
24
|
* WDIO instantiates the Service with `(options, capabilities, config)`. We
|
|
14
25
|
* read `config.framework` so the result-shape switch (P1 PRD §A.2) picks the
|
|
15
|
-
* right normalization
|
|
16
|
-
* `
|
|
26
|
+
* right normalization. `capabilities` is accepted for ServiceClass
|
|
27
|
+
* compatibility but unused (the live browser arrives in the `before` hook).
|
|
17
28
|
*/
|
|
18
|
-
constructor(options?: TraceLaneOptions & WebdriverIO.ServiceOption, _capabilities?:
|
|
19
|
-
framework?: string;
|
|
20
|
-
});
|
|
29
|
+
constructor(options?: TraceLaneOptions & WebdriverIO.ServiceOption, _capabilities?: Capabilities.ResolvedTestrunnerCapabilities, config?: Options.Testrunner);
|
|
21
30
|
/** Refine the framework + worker id once the session is initializing. */
|
|
22
31
|
beforeSession(_config: unknown, _capabilities: unknown, _specs: string[], cid?: string): void;
|
|
23
32
|
/** Worker hook: stash the live browser + build the recorder. */
|
|
@@ -32,8 +41,24 @@ export default class TraceLaneService implements Services.ServiceInstance {
|
|
|
32
41
|
beforeScenario(world: unknown, _context?: unknown): Promise<void>;
|
|
33
42
|
/** Cucumber: scenario end (runtime-only hook; mirrors `afterTest`). */
|
|
34
43
|
afterScenario(world: unknown, result?: unknown): Promise<void>;
|
|
35
|
-
/**
|
|
36
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Re-inject the recorder AFTER a `url(...)` navigation (ADR-0006, T-9 fix).
|
|
46
|
+
*
|
|
47
|
+
* WDIO's `beforeCommand` fires before the command executes; if we re-inject
|
|
48
|
+
* there, the page is about to be torn down and Chrome's load of the new URL
|
|
49
|
+
* destroys the just-injected rrweb instance + the `__tracelane__events`
|
|
50
|
+
* buffer. By the time the test starts interacting nothing is recording.
|
|
51
|
+
*
|
|
52
|
+
* `afterCommand` fires after WDIO returns from `url(...)`, which only
|
|
53
|
+
* happens once Chrome has navigated and the document has loaded. Injecting
|
|
54
|
+
* here lands rrweb on the NEW page, where it can observe DOM mutations,
|
|
55
|
+
* console output, and fetches.
|
|
56
|
+
*
|
|
57
|
+
* Errors from the command itself (e.g. a malformed URL) skip re-injection —
|
|
58
|
+
* if the navigation failed, the page never changed and the existing
|
|
59
|
+
* recorder (if any) is still attached to the old page; this is a no-op.
|
|
60
|
+
*/
|
|
61
|
+
afterCommand(commandName: string, args: unknown[], _result: unknown, error?: Error): Promise<void>;
|
|
37
62
|
/** No-op in v1; present for the documented hook surface (ADR-0004). */
|
|
38
63
|
afterSuite(_suite: Frameworks.Suite): void;
|
|
39
64
|
/** Worker teardown: stop the drain poll so no timer leaks. */
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAKrD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAiB,YAAW,QAAQ,CAAC,eAAe;IACvE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAE3C;;;;;OAKG;gBAED,OAAO,GAAE,gBAAgB,GAAG,WAAW,CAAC,aAAkB,EAC1D,aAAa,CAAC,EAAE,YAAY,CAAC,8BAA8B,EAC3D,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU;IAK7B,yEAAyE;IACzE,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,IAAI;IAI7F,gEAAgE;IAC1D,MAAM,CACV,aAAa,EAAE,OAAO,EACtB,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,WAAW,CAAC,OAAO,GAC3B,OAAO,CAAC,IAAI,CAAC;IAIhB,uEAAuE;IACvE,WAAW,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,GAAG,IAAI;IAE3C,mEAAmE;IAC7D,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKzE,yEAAyE;IACnE,SAAS,CACb,KAAK,EAAE,UAAU,CAAC,IAAI,EACtB,QAAQ,EAAE,OAAO,EACjB,MAAM,EAAE,UAAU,CAAC,UAAU,GAC5B,OAAO,CAAC,IAAI,CAAC;IAIhB,0EAA0E;IACpE,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvE,uEAAuE;IACjE,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAIpE;;;;;;;;;;;;;;;;OAgBG;IACG,YAAY,CAChB,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,OAAO,EAAE,EACf,OAAO,EAAE,OAAO,EAChB,KAAK,CAAC,EAAE,KAAK,GACZ,OAAO,CAAC,IAAI,CAAC;IAMhB,uEAAuE;IACvE,UAAU,CAAC,MAAM,EAAE,UAAU,CAAC,KAAK,GAAG,IAAI;IAE1C,8DAA8D;IACxD,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAIrF,8EAA8E;IAC9E,UAAU,CACR,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,OAAO,EAChB,aAAa,EAAE,OAAO,EACtB,QAAQ,EAAE,OAAO,GAChB,IAAI;CACR"}
|
package/dist/service.js
CHANGED
|
@@ -15,14 +15,25 @@ import { TraceLaneSession } from './tracelane-session.js';
|
|
|
15
15
|
* `afterScenario`) are declared as plain methods since `@wdio/types` doesn't put
|
|
16
16
|
* them on `ServiceInstance` (they only exist at runtime under the Cucumber
|
|
17
17
|
* framework).
|
|
18
|
+
*
|
|
19
|
+
* The constructor signature deliberately matches `Services.ServiceClass`:
|
|
20
|
+
* `new (options: WebdriverIO.ServiceOption,
|
|
21
|
+
* capabilities: ResolvedTestrunnerCapabilities,
|
|
22
|
+
* config: Options.Testrunner): ServiceInstance`
|
|
23
|
+
* (T-4 fix, 2026-05-28 QA walk). Earlier intersection-typing on `options` was
|
|
24
|
+
* not enough — the `capabilities` and `config` parameters had to ALSO be
|
|
25
|
+
* compatible with the interface, otherwise registering the class as
|
|
26
|
+
* `services: [[TraceLaneService, { ... }]]` raised the "not assignable to
|
|
27
|
+
* ServiceClass" error in the user's wdio.conf.ts. With the full triple in
|
|
28
|
+
* place the tuple form typechecks without `@ts-expect-error`.
|
|
18
29
|
*/
|
|
19
30
|
export default class TraceLaneService {
|
|
20
31
|
session;
|
|
21
32
|
/**
|
|
22
33
|
* WDIO instantiates the Service with `(options, capabilities, config)`. We
|
|
23
34
|
* read `config.framework` so the result-shape switch (P1 PRD §A.2) picks the
|
|
24
|
-
* right normalization
|
|
25
|
-
* `
|
|
35
|
+
* right normalization. `capabilities` is accepted for ServiceClass
|
|
36
|
+
* compatibility but unused (the live browser arrives in the `before` hook).
|
|
26
37
|
*/
|
|
27
38
|
constructor(options = {}, _capabilities, config) {
|
|
28
39
|
this.session = new TraceLaneSession(options, config?.framework);
|
|
@@ -55,11 +66,29 @@ export default class TraceLaneService {
|
|
|
55
66
|
async afterScenario(world, result) {
|
|
56
67
|
await this.session.onAfterTest(world, result);
|
|
57
68
|
}
|
|
58
|
-
/**
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Re-inject the recorder AFTER a `url(...)` navigation (ADR-0006, T-9 fix).
|
|
71
|
+
*
|
|
72
|
+
* WDIO's `beforeCommand` fires before the command executes; if we re-inject
|
|
73
|
+
* there, the page is about to be torn down and Chrome's load of the new URL
|
|
74
|
+
* destroys the just-injected rrweb instance + the `__tracelane__events`
|
|
75
|
+
* buffer. By the time the test starts interacting nothing is recording.
|
|
76
|
+
*
|
|
77
|
+
* `afterCommand` fires after WDIO returns from `url(...)`, which only
|
|
78
|
+
* happens once Chrome has navigated and the document has loaded. Injecting
|
|
79
|
+
* here lands rrweb on the NEW page, where it can observe DOM mutations,
|
|
80
|
+
* console output, and fetches.
|
|
81
|
+
*
|
|
82
|
+
* Errors from the command itself (e.g. a malformed URL) skip re-injection —
|
|
83
|
+
* if the navigation failed, the page never changed and the existing
|
|
84
|
+
* recorder (if any) is still attached to the old page; this is a no-op.
|
|
85
|
+
*/
|
|
86
|
+
async afterCommand(commandName, args, _result, error) {
|
|
87
|
+
if (commandName !== 'url' || typeof args[0] !== 'string')
|
|
88
|
+
return;
|
|
89
|
+
if (error !== undefined)
|
|
90
|
+
return;
|
|
91
|
+
await this.session.onUrl(args[0]);
|
|
63
92
|
}
|
|
64
93
|
/** No-op in v1; present for the documented hook surface (ADR-0004). */
|
|
65
94
|
afterSuite(_suite) { }
|
package/dist/service.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,2EAA2E;AAC3E,qEAAqE;AACrE,EAAE;AACF,iFAAiF;AACjF,gEAAgE;AAIhE,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D
|
|
1
|
+
{"version":3,"file":"service.js","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,EAAE;AACF,4EAA4E;AAC5E,4EAA4E;AAC5E,2EAA2E;AAC3E,qEAAqE;AACrE,EAAE;AACF,iFAAiF;AACjF,gEAAgE;AAIhE,OAAO,EAAiB,gBAAgB,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG1D;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,CAAC,OAAO,OAAO,gBAAgB;IAClB,OAAO,CAAmB;IAE3C;;;;;OAKG;IACH,YACE,UAAwD,EAAE,EAC1D,aAA2D,EAC3D,MAA2B;QAE3B,IAAI,CAAC,OAAO,GAAG,IAAI,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAC,CAAC;IAClE,CAAC;IAED,yEAAyE;IACzE,aAAa,CAAC,OAAgB,EAAE,aAAsB,EAAE,MAAgB,EAAE,GAAY;QACpF,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,gEAAgE;IAChE,KAAK,CAAC,MAAM,CACV,aAAsB,EACtB,MAAgB,EAChB,OAA4B;QAE5B,MAAM,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,OAAiC,CAAC,CAAC;IACjE,CAAC;IAED,uEAAuE;IACvE,WAAW,CAAC,MAAwB,IAAS,CAAC;IAE9C,mEAAmE;IACnE,KAAK,CAAC,UAAU,CAAC,IAAqB,EAAE,QAAiB;QACvD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,YAAY,CAAC,IAAgB,CAAC,CAAC;QACvD,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED,yEAAyE;IACzE,KAAK,CAAC,SAAS,CACb,KAAsB,EACtB,QAAiB,EACjB,MAA6B;QAE7B,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,0EAA0E;IAC1E,KAAK,CAAC,cAAc,CAAC,KAAc,EAAE,QAAkB;QACrD,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,gBAAgB,CAAC,KAAK,CAAC,CAAC;QAChD,MAAM,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC/C,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,aAAa,CAAC,KAAc,EAAE,MAAgB;QAClD,MAAM,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;OAgBG;IACH,KAAK,CAAC,YAAY,CAChB,WAAmB,EACnB,IAAe,EACf,OAAgB,EAChB,KAAa;QAEb,IAAI,WAAW,KAAK,KAAK,IAAI,OAAO,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ;YAAE,OAAO;QACjE,IAAI,KAAK,KAAK,SAAS;YAAE,OAAO;QAChC,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IACpC,CAAC;IAED,uEAAuE;IACvE,UAAU,CAAC,MAAwB,IAAS,CAAC;IAE7C,8DAA8D;IAC9D,KAAK,CAAC,KAAK,CAAC,OAAe,EAAE,aAAsB,EAAE,MAAgB;QACnE,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/B,CAAC;IAED,8EAA8E;IAC9E,UAAU,CACR,SAAiB,EACjB,OAAgB,EAChB,aAAsB,EACtB,QAAiB,IACV,CAAC;CACX"}
|
|
@@ -17,6 +17,8 @@ export declare class TraceLaneSession {
|
|
|
17
17
|
private readonly captureRrweb;
|
|
18
18
|
private readonly captureNetwork;
|
|
19
19
|
private readonly captureConsole;
|
|
20
|
+
/** Advisory security-hygiene capture + analysis. Default on (P1 security MVP). */
|
|
21
|
+
private readonly security;
|
|
20
22
|
private framework;
|
|
21
23
|
private cid;
|
|
22
24
|
private browser;
|
|
@@ -55,7 +57,15 @@ export declare class TraceLaneSession {
|
|
|
55
57
|
* exactly once (#2).
|
|
56
58
|
*/
|
|
57
59
|
private maybeAttachNetworkCapture;
|
|
58
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* WDIO `afterCommand('url', ...)`: re-inject the recorder AFTER the
|
|
62
|
+
* navigation has landed (T-9 fix). The Service / hooks factory used to
|
|
63
|
+
* call this from `beforeCommand`, but that fired before the navigation —
|
|
64
|
+
* the page was about to be torn down, and the freshly-re-injected rrweb
|
|
65
|
+
* + the `__tracelane__events` buffer were both wiped by the load. Now we
|
|
66
|
+
* inject on the NEW page so events flow from the first FullSnapshot
|
|
67
|
+
* forward.
|
|
68
|
+
*/
|
|
59
69
|
onUrl(url: string): Promise<void>;
|
|
60
70
|
/**
|
|
61
71
|
* `afterTest`/`afterScenario`: normalize the framework result, ask the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracelane-session.d.ts","sourceRoot":"","sources":["../src/tracelane-session.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tracelane-session.d.ts","sourceRoot":"","sources":["../src/tracelane-session.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAmB,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAEtE,OAAO,EAAE,KAAK,WAAW,EAAsB,MAAM,oBAAoB,CAAC;AAQ1E,qFAAqF;AACrF,UAAU,mBAAmB;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,+EAA+E;AAC/E,KAAK,cAAc,GAAG,WAAW,GAAG;IAClC,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmB;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAU;IACzC,kFAAkF;IAClF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAU;IACnC,OAAO,CAAC,SAAS,CAAiC;IAClD,OAAO,CAAC,GAAG,CAAqB;IAEhC,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,QAAQ,CAAuB;IACvC,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,eAAe,CAAS;IAChC,6EAA6E;IAC7E,OAAO,CAAC,kBAAkB,CAAS;gBAEvB,OAAO,GAAE,gBAAqB,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM;IAc5E,iFAAiF;IACjF,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIjD,+EAA+E;IAC/E,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI;IAIrC;;;;OAIG;IACG,QAAQ,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAItD;;;;;;OAMG;IACH,OAAO,CAAC,2BAA2B;IAKnC,sEAAsE;IACtE,OAAO,CAAC,4BAA4B;IA6BpC,iFAAiF;IAC3E,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAW/D;;;;;OAKG;YACW,yBAAyB;IAkBvC;;;;;;;;OAQG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKvC;;;;;;OAMG;IACG,WAAW,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC;IAmCnF;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAO9B,gFAAgF;IAChF,OAAO,CAAC,SAAS;CAclB"}
|
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
// the capture/inject/drain/report logic lives in exactly one place (ADR-0004:
|
|
6
6
|
// "the same logic exported as plain hook functions"). The session owns the
|
|
7
7
|
// recorder, the current-test metadata, and the report-write decision.
|
|
8
|
-
import {
|
|
8
|
+
import { cwd } from 'node:process';
|
|
9
|
+
import { attachNetworkCapture, createRecorder, loadRrwebBundle, } from '@tracelane/core';
|
|
10
|
+
import { writeReport } from '@tracelane/report';
|
|
9
11
|
import { normalizeResult } from './framework-result.js';
|
|
10
|
-
import { loadRrwebBundle } from './inpage-bundle.js';
|
|
11
|
-
import { attachNetworkCapture } from './network-capture.js';
|
|
12
12
|
import { DEFAULT_OUT_DIR } from './options.js';
|
|
13
|
-
import {
|
|
13
|
+
import { loadSecuritySuppressions } from './security-suppress.js';
|
|
14
14
|
import { createWdioExecutor } from './wdio-executor.js';
|
|
15
15
|
export class TraceLaneSession {
|
|
16
16
|
options;
|
|
@@ -18,6 +18,8 @@ export class TraceLaneSession {
|
|
|
18
18
|
captureRrweb;
|
|
19
19
|
captureNetwork;
|
|
20
20
|
captureConsole;
|
|
21
|
+
/** Advisory security-hygiene capture + analysis. Default on (P1 security MVP). */
|
|
22
|
+
security;
|
|
21
23
|
framework;
|
|
22
24
|
cid;
|
|
23
25
|
browser;
|
|
@@ -33,6 +35,9 @@ export class TraceLaneSession {
|
|
|
33
35
|
this.captureRrweb = options.capture?.rrweb !== false;
|
|
34
36
|
this.captureNetwork = options.capture?.network !== false;
|
|
35
37
|
this.captureConsole = options.capture?.console !== false;
|
|
38
|
+
// Advisory security-hygiene defaults on; `security: false` disables both the
|
|
39
|
+
// [tracelane.sec] capture and the report-side analysis.
|
|
40
|
+
this.security = options.security !== false;
|
|
36
41
|
this.framework = framework;
|
|
37
42
|
this.cid = cid;
|
|
38
43
|
}
|
|
@@ -70,7 +75,7 @@ export class TraceLaneSession {
|
|
|
70
75
|
createRecorderForCurrentTest(browser) {
|
|
71
76
|
const recorderOptions = {
|
|
72
77
|
executor: createWdioExecutor(browser),
|
|
73
|
-
rrwebBundle: loadRrwebBundle(),
|
|
78
|
+
rrwebBundle: loadRrwebBundle(import.meta.url),
|
|
74
79
|
};
|
|
75
80
|
if (this.options.drainIntervalMs !== undefined) {
|
|
76
81
|
recorderOptions.drainIntervalMs = this.options.drainIntervalMs;
|
|
@@ -82,6 +87,14 @@ export class TraceLaneSession {
|
|
|
82
87
|
if (consolePluginOptions !== undefined) {
|
|
83
88
|
recorderOptions.consolePluginOptions = consolePluginOptions;
|
|
84
89
|
}
|
|
90
|
+
// Network plugin opt-in: when capture.network !== false (default), forward
|
|
91
|
+
// the user's networkOptions (or `{}` for the plugin's defaults — bodies +
|
|
92
|
+
// headers off, PerformanceObserver on). When capture.network === false we
|
|
93
|
+
// pass nothing, leaving the legacy CDP path as the only network channel.
|
|
94
|
+
if (this.captureNetwork) {
|
|
95
|
+
recorderOptions.networkPluginOptions =
|
|
96
|
+
this.options.capture?.networkOptions ?? {};
|
|
97
|
+
}
|
|
85
98
|
if (this.options.mode !== undefined) {
|
|
86
99
|
recorderOptions.mode = this.options.mode;
|
|
87
100
|
}
|
|
@@ -111,7 +124,10 @@ export class TraceLaneSession {
|
|
|
111
124
|
if (!this.browser)
|
|
112
125
|
return;
|
|
113
126
|
try {
|
|
114
|
-
await attachNetworkCapture(createWdioExecutor(this.browser)
|
|
127
|
+
await attachNetworkCapture(createWdioExecutor(this.browser), {
|
|
128
|
+
security: this.security,
|
|
129
|
+
onSecurityMeta: (m) => this.recorder?.addCustomEvent('tracelane.sec', m),
|
|
130
|
+
});
|
|
115
131
|
this.networkAttached = true;
|
|
116
132
|
}
|
|
117
133
|
catch {
|
|
@@ -120,7 +136,15 @@ export class TraceLaneSession {
|
|
|
120
136
|
console.warn('[tracelane/wdio] network capture unavailable (CDP not attached); degrading to rrweb+console only.');
|
|
121
137
|
}
|
|
122
138
|
}
|
|
123
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* WDIO `afterCommand('url', ...)`: re-inject the recorder AFTER the
|
|
141
|
+
* navigation has landed (T-9 fix). The Service / hooks factory used to
|
|
142
|
+
* call this from `beforeCommand`, but that fired before the navigation —
|
|
143
|
+
* the page was about to be torn down, and the freshly-re-injected rrweb
|
|
144
|
+
* + the `__tracelane__events` buffer were both wiped by the load. Now we
|
|
145
|
+
* inject on the NEW page so events flow from the first FullSnapshot
|
|
146
|
+
* forward.
|
|
147
|
+
*/
|
|
124
148
|
async onUrl(url) {
|
|
125
149
|
if (!this.recorder)
|
|
126
150
|
return;
|
|
@@ -149,7 +173,19 @@ export class TraceLaneSession {
|
|
|
149
173
|
return undefined;
|
|
150
174
|
}
|
|
151
175
|
const meta = this.buildMeta(normalized);
|
|
152
|
-
|
|
176
|
+
// Load the optional suppression file at report-write time. The loader never
|
|
177
|
+
// throws and falls back to `[]`, so a missing/malformed file can't break the
|
|
178
|
+
// report. Skip the read entirely when security is off.
|
|
179
|
+
const securitySuppress = this.security ? loadSecuritySuppressions(cwd()) : [];
|
|
180
|
+
const path = writeReport({
|
|
181
|
+
outDir: this.outDir,
|
|
182
|
+
cid: this.cid,
|
|
183
|
+
events,
|
|
184
|
+
meta,
|
|
185
|
+
footer: this.options.report?.footer,
|
|
186
|
+
security: this.security,
|
|
187
|
+
securitySuppress,
|
|
188
|
+
});
|
|
153
189
|
this.current = undefined;
|
|
154
190
|
return path;
|
|
155
191
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracelane-session.js","sourceRoot":"","sources":["../src/tracelane-session.ts"],"names":[],"mappings":"AAAA,yCAAyC;AACzC,EAAE;AACF,mEAAmE;AACnE,+EAA+E;AAC/E,8EAA8E;AAC9E,2EAA2E;AAC3E,sEAAsE;AAEtE,OAAO,
|
|
1
|
+
{"version":3,"file":"tracelane-session.js","sourceRoot":"","sources":["../src/tracelane-session.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tracelane/wdio",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.20",
|
|
4
4
|
"description": "WebdriverIO Service that records rrweb sessions and writes a self-contained HTML report on failed tests. The user-facing tracelane integration for WDIO.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"tracelane",
|
|
7
|
+
"webdriverio",
|
|
8
|
+
"wdio",
|
|
9
|
+
"wdio-service",
|
|
10
|
+
"testing",
|
|
11
|
+
"e2e",
|
|
12
|
+
"reporter",
|
|
13
|
+
"rrweb",
|
|
14
|
+
"session-replay",
|
|
15
|
+
"html-report"
|
|
16
|
+
],
|
|
5
17
|
"license": "Apache-2.0",
|
|
18
|
+
"funding": {
|
|
19
|
+
"type": "github",
|
|
20
|
+
"url": "https://github.com/sponsors/harry-harish"
|
|
21
|
+
},
|
|
6
22
|
"type": "module",
|
|
7
23
|
"main": "./dist/index.js",
|
|
8
24
|
"types": "./dist/index.d.ts",
|
|
@@ -22,8 +38,9 @@
|
|
|
22
38
|
"README.md"
|
|
23
39
|
],
|
|
24
40
|
"dependencies": {
|
|
25
|
-
"@tracelane/
|
|
26
|
-
"@tracelane/
|
|
41
|
+
"@tracelane/report": "0.1.0-alpha.17",
|
|
42
|
+
"@tracelane/core": "0.1.0-alpha.15",
|
|
43
|
+
"@tracelane/security": "0.1.0-alpha.1"
|
|
27
44
|
},
|
|
28
45
|
"peerDependencies": {
|
|
29
46
|
"@wdio/types": "^9.0.0",
|
|
@@ -39,7 +56,7 @@
|
|
|
39
56
|
"esbuild": "^0.28.0",
|
|
40
57
|
"tsx": "^4.19.0",
|
|
41
58
|
"webdriverio": "^9.0.0",
|
|
42
|
-
"@cubenest/rrweb-core": "0.1.0-alpha.
|
|
59
|
+
"@cubenest/rrweb-core": "0.1.0-alpha.6"
|
|
43
60
|
},
|
|
44
61
|
"publishConfig": {
|
|
45
62
|
"access": "public",
|
|
@@ -50,7 +67,13 @@
|
|
|
50
67
|
"url": "https://github.com/Cubenest/rrweb-stack",
|
|
51
68
|
"directory": "packages/tracelane-wdio"
|
|
52
69
|
},
|
|
53
|
-
"
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/Cubenest/rrweb-stack/issues"
|
|
72
|
+
},
|
|
73
|
+
"homepage": "https://tracelane.cubenest.in",
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=22"
|
|
76
|
+
},
|
|
54
77
|
"scripts": {
|
|
55
78
|
"build": "tsc -p tsconfig.json && node scripts/build-rrweb-bundle.mjs",
|
|
56
79
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
package/dist/inpage-bundle.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rrweb in-page bundle source (defines `window.rrweb`). Read from
|
|
3
|
-
* `dist/rrweb-bundle.js`, cached after first read.
|
|
4
|
-
*
|
|
5
|
-
* @throws if the bundle is missing — that means the package was used without its
|
|
6
|
-
* build step (`pnpm --filter @tracelane/wdio build`) having run.
|
|
7
|
-
*/
|
|
8
|
-
export declare function loadRrwebBundle(): string;
|
|
9
|
-
//# sourceMappingURL=inpage-bundle.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"inpage-bundle.d.ts","sourceRoot":"","sources":["../src/inpage-bundle.ts"],"names":[],"mappings":"AAgCA;;;;;;GAMG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAWxC"}
|
package/dist/inpage-bundle.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
// Loads the in-page rrweb bundle source string.
|
|
2
|
-
//
|
|
3
|
-
// The bundle (`dist/rrweb-bundle.js`) is produced at build time by
|
|
4
|
-
// `scripts/build-rrweb-bundle.mjs` (esbuild → IIFE that defines `window.rrweb`
|
|
5
|
-
// with `record` + `getRecordConsolePlugin`). @tracelane/core's recorder is
|
|
6
|
-
// bundle-source-agnostic (ADR-0006) and expects this source as a plain string,
|
|
7
|
-
// which it `window.eval`s in the page on every (re-)injection.
|
|
8
|
-
//
|
|
9
|
-
// We read it once and cache it. In the published package this module compiles
|
|
10
|
-
// to `dist/inpage-bundle.js`, sitting next to `dist/rrweb-bundle.js`; under
|
|
11
|
-
// vitest the source lives in `src/`, so we also probe a sibling `../dist/`. We
|
|
12
|
-
// derive the module directory from `fileURLToPath(import.meta.url)` directly
|
|
13
|
-
// (not via `new URL(rel, import.meta.url)`) because under the jsdom test env the
|
|
14
|
-
// global `URL` resolves relative inputs against the page origin, not the file.
|
|
15
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
16
|
-
import { dirname, join } from 'node:path';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
-
let cached;
|
|
19
|
-
/** Candidate locations for the built bundle, in priority order. */
|
|
20
|
-
function candidatePaths() {
|
|
21
|
-
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
22
|
-
return [
|
|
23
|
-
// Published layout: dist/inpage-bundle.js → dist/rrweb-bundle.js.
|
|
24
|
-
join(moduleDir, 'rrweb-bundle.js'),
|
|
25
|
-
// Source/test layout: src/inpage-bundle.ts → ../dist/rrweb-bundle.js.
|
|
26
|
-
join(moduleDir, '..', 'dist', 'rrweb-bundle.js'),
|
|
27
|
-
];
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* The rrweb in-page bundle source (defines `window.rrweb`). Read from
|
|
31
|
-
* `dist/rrweb-bundle.js`, cached after first read.
|
|
32
|
-
*
|
|
33
|
-
* @throws if the bundle is missing — that means the package was used without its
|
|
34
|
-
* build step (`pnpm --filter @tracelane/wdio build`) having run.
|
|
35
|
-
*/
|
|
36
|
-
export function loadRrwebBundle() {
|
|
37
|
-
if (cached !== undefined)
|
|
38
|
-
return cached;
|
|
39
|
-
const candidates = candidatePaths();
|
|
40
|
-
const found = candidates.find((p) => existsSync(p));
|
|
41
|
-
if (found === undefined) {
|
|
42
|
-
throw new Error(`@tracelane/wdio: in-page rrweb bundle not found (looked in ${candidates.join(', ')}). Run the package build (\`pnpm --filter @tracelane/wdio build\`) to generate it.`);
|
|
43
|
-
}
|
|
44
|
-
cached = readFileSync(found, 'utf8');
|
|
45
|
-
return cached;
|
|
46
|
-
}
|
|
47
|
-
//# sourceMappingURL=inpage-bundle.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"inpage-bundle.js","sourceRoot":"","sources":["../src/inpage-bundle.ts"],"names":[],"mappings":"AAAA,gDAAgD;AAChD,EAAE;AACF,mEAAmE;AACnE,+EAA+E;AAC/E,2EAA2E;AAC3E,+EAA+E;AAC/E,+DAA+D;AAC/D,EAAE;AACF,8EAA8E;AAC9E,4EAA4E;AAC5E,+EAA+E;AAC/E,6EAA6E;AAC7E,iFAAiF;AACjF,+EAA+E;AAE/E,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,IAAI,MAA0B,CAAC;AAE/B,mEAAmE;AACnE,SAAS,cAAc;IACrB,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAC1D,OAAO;QACL,kEAAkE;QAClE,IAAI,CAAC,SAAS,EAAE,iBAAiB,CAAC;QAClC,sEAAsE;QACtE,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,iBAAiB,CAAC;KACjD,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe;IAC7B,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACxC,MAAM,UAAU,GAAG,cAAc,EAAE,CAAC;IACpC,MAAM,KAAK,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;IACpD,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CACb,8DAA8D,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,oFAAoF,CACxK,CAAC;IACJ,CAAC;IACD,MAAM,GAAG,YAAY,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;IACrC,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { BrowserExecutor } from '@tracelane/core';
|
|
2
|
-
/**
|
|
3
|
-
* Page-side logger. Self-contained (no Node closures) so it can be
|
|
4
|
-
* `.toString()`-serialized by `execute` (PRD §A.4). The console plugin captures
|
|
5
|
-
* this `console.error`; the `[tracelane.net]` prefix lets the report's network
|
|
6
|
-
* panel scrape it back out (PRD §E.2).
|
|
7
|
-
*/
|
|
8
|
-
declare function logNetworkErrorInPage(url: string, status: number, method: string): void;
|
|
9
|
-
/** Pull the request method out of CDP request headers, defaulting to GET. */
|
|
10
|
-
declare function methodOf(headers: Record<string, string> | undefined): string;
|
|
11
|
-
/**
|
|
12
|
-
* Attach CDP network capture to a BrowserExecutor (P1 PRD §E.2).
|
|
13
|
-
*
|
|
14
|
-
* Enables the Network domain and registers a `Network.responseReceived`
|
|
15
|
-
* subscriber that forwards 4xx/5xx responses into `console.error`. Resolves once
|
|
16
|
-
* `Network.enable` has been sent. The subscriber's own `execute` calls are
|
|
17
|
-
* fire-and-forget (their failures must not break the test).
|
|
18
|
-
*/
|
|
19
|
-
export declare function attachNetworkCapture(executor: BrowserExecutor): Promise<void>;
|
|
20
|
-
export declare const __internal: {
|
|
21
|
-
logNetworkErrorInPage: typeof logNetworkErrorInPage;
|
|
22
|
-
methodOf: typeof methodOf;
|
|
23
|
-
};
|
|
24
|
-
export {};
|
|
25
|
-
//# sourceMappingURL=network-capture.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"network-capture.d.ts","sourceRoot":"","sources":["../src/network-capture.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAWvD;;;;;GAKG;AACH,iBAAS,qBAAqB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAEhF;AAED,6EAA6E;AAC7E,iBAAS,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,GAAG,MAAM,CAKrE;AAED;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgBnF;AAGD,eAAO,MAAM,UAAU;;;CAAsC,CAAC"}
|
package/dist/network-capture.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
// CDP network capture wiring (Task 2.16 / P1 PRD §E.2).
|
|
2
|
-
//
|
|
3
|
-
// Enable the CDP Network domain, subscribe to `Network.responseReceived`, and
|
|
4
|
-
// route any response with `status >= 400` into the page's `console.error` via
|
|
5
|
-
// `executor.execute`. The rrweb console plugin (installed by @tracelane/core's
|
|
6
|
-
// recorder) then captures that console line, so failed responses show up in the
|
|
7
|
-
// report's console panel "for free" — no dedicated network transport in v1.
|
|
8
|
-
//
|
|
9
|
-
// The console line is prefixed `[tracelane.net]` so @tracelane/report's network
|
|
10
|
-
// panel can scrape it back out (NETWORK_CONSOLE_PREFIX in panels.ts).
|
|
11
|
-
/**
|
|
12
|
-
* Page-side logger. Self-contained (no Node closures) so it can be
|
|
13
|
-
* `.toString()`-serialized by `execute` (PRD §A.4). The console plugin captures
|
|
14
|
-
* this `console.error`; the `[tracelane.net]` prefix lets the report's network
|
|
15
|
-
* panel scrape it back out (PRD §E.2).
|
|
16
|
-
*/
|
|
17
|
-
function logNetworkErrorInPage(url, status, method) {
|
|
18
|
-
console.error(`[tracelane.net] ${method} ${status} ${url}`);
|
|
19
|
-
}
|
|
20
|
-
/** Pull the request method out of CDP request headers, defaulting to GET. */
|
|
21
|
-
function methodOf(headers) {
|
|
22
|
-
if (!headers)
|
|
23
|
-
return 'GET';
|
|
24
|
-
// CDP exposes the pseudo-header `:method` for HTTP/2/3; fall back to a plain
|
|
25
|
-
// `method` header, then GET.
|
|
26
|
-
return headers[':method'] ?? headers.method ?? 'GET';
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Attach CDP network capture to a BrowserExecutor (P1 PRD §E.2).
|
|
30
|
-
*
|
|
31
|
-
* Enables the Network domain and registers a `Network.responseReceived`
|
|
32
|
-
* subscriber that forwards 4xx/5xx responses into `console.error`. Resolves once
|
|
33
|
-
* `Network.enable` has been sent. The subscriber's own `execute` calls are
|
|
34
|
-
* fire-and-forget (their failures must not break the test).
|
|
35
|
-
*/
|
|
36
|
-
export async function attachNetworkCapture(executor) {
|
|
37
|
-
await executor.cdp('Network', 'enable');
|
|
38
|
-
executor.on('Network.responseReceived', (params) => {
|
|
39
|
-
const response = params?.response;
|
|
40
|
-
const status = response?.status;
|
|
41
|
-
if (typeof status !== 'number' || status < 400)
|
|
42
|
-
return;
|
|
43
|
-
const url = response?.url ?? '';
|
|
44
|
-
const method = methodOf(response?.requestHeaders);
|
|
45
|
-
// Fire-and-forget: a logging failure (e.g. page mid-navigation) must not
|
|
46
|
-
// surface as a test error.
|
|
47
|
-
void executor
|
|
48
|
-
.execute(logNetworkErrorInPage, url, status, method)
|
|
49
|
-
.catch(() => {
|
|
50
|
-
/* page may be navigating; drop this one line */
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
// Exposed for unit tests: the page-side logger + method resolver are pure.
|
|
55
|
-
export const __internal = { logNetworkErrorInPage, methodOf };
|
|
56
|
-
//# sourceMappingURL=network-capture.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"network-capture.js","sourceRoot":"","sources":["../src/network-capture.ts"],"names":[],"mappings":"AAAA,wDAAwD;AACxD,EAAE;AACF,8EAA8E;AAC9E,8EAA8E;AAC9E,+EAA+E;AAC/E,gFAAgF;AAChF,4EAA4E;AAC5E,EAAE;AACF,gFAAgF;AAChF,sEAAsE;AAatE;;;;;GAKG;AACH,SAAS,qBAAqB,CAAC,GAAW,EAAE,MAAc,EAAE,MAAc;IACxE,OAAO,CAAC,KAAK,CAAC,mBAAmB,MAAM,IAAI,MAAM,IAAI,GAAG,EAAE,CAAC,CAAC;AAC9D,CAAC;AAED,6EAA6E;AAC7E,SAAS,QAAQ,CAAC,OAA2C;IAC3D,IAAI,CAAC,OAAO;QAAE,OAAO,KAAK,CAAC;IAC3B,6EAA6E;IAC7E,6BAA6B;IAC7B,OAAO,OAAO,CAAC,SAAS,CAAC,IAAI,OAAO,CAAC,MAAM,IAAI,KAAK,CAAC;AACvD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,QAAyB;IAClE,MAAM,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IACxC,QAAQ,CAAC,EAAE,CAAC,0BAA0B,EAAE,CAAC,MAAe,EAAE,EAAE;QAC1D,MAAM,QAAQ,GAAI,MAAgC,EAAE,QAAQ,CAAC;QAC7D,MAAM,MAAM,GAAG,QAAQ,EAAE,MAAM,CAAC;QAChC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,GAAG,GAAG;YAAE,OAAO;QACvD,MAAM,GAAG,GAAG,QAAQ,EAAE,GAAG,IAAI,EAAE,CAAC;QAChC,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC,CAAC;QAClD,yEAAyE;QACzE,2BAA2B;QAC3B,KAAK,QAAQ;aACV,OAAO,CAAC,qBAAqD,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC;aACnF,KAAK,CAAC,GAAG,EAAE;YACV,gDAAgD;QAClD,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;AACL,CAAC;AAED,2EAA2E;AAC3E,MAAM,CAAC,MAAM,UAAU,GAAG,EAAE,qBAAqB,EAAE,QAAQ,EAAE,CAAC"}
|