@descryy/runtime-browser 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-action-collector.d.ts +181 -0
- package/dist/browser-action-collector.d.ts.map +1 -0
- package/dist/browser-action-collector.js +427 -0
- package/dist/browser-action-collector.js.map +1 -0
- package/dist/browser-console-collector.d.ts +33 -0
- package/dist/browser-console-collector.d.ts.map +1 -0
- package/dist/browser-console-collector.js +185 -0
- package/dist/browser-console-collector.js.map +1 -0
- package/dist/browser-network-collector.d.ts +112 -0
- package/dist/browser-network-collector.d.ts.map +1 -0
- package/dist/browser-network-collector.js +682 -0
- package/dist/browser-network-collector.js.map +1 -0
- package/dist/browser-session.d.ts +144 -0
- package/dist/browser-session.d.ts.map +1 -0
- package/dist/browser-session.js +136 -0
- package/dist/browser-session.js.map +1 -0
- package/dist/collector-version.d.ts +10 -0
- package/dist/collector-version.d.ts.map +1 -0
- package/dist/collector-version.js +12 -0
- package/dist/collector-version.js.map +1 -0
- package/dist/dom-evidence.d.ts +20 -0
- package/dist/dom-evidence.d.ts.map +1 -0
- package/dist/dom-evidence.js +32 -0
- package/dist/dom-evidence.js.map +1 -0
- package/dist/frontend-initiator-capture.d.ts +87 -0
- package/dist/frontend-initiator-capture.d.ts.map +1 -0
- package/dist/frontend-initiator-capture.js +142 -0
- package/dist/frontend-initiator-capture.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/network-correlation.d.ts +15 -0
- package/dist/network-correlation.d.ts.map +1 -0
- package/dist/network-correlation.js +29 -0
- package/dist/network-correlation.js.map +1 -0
- package/dist/script-url-mapping.d.ts +40 -0
- package/dist/script-url-mapping.d.ts.map +1 -0
- package/dist/script-url-mapping.js +96 -0
- package/dist/script-url-mapping.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch-initiator capture — the page-side half of resolving *which*
|
|
3
|
+
* frontend function issued an observed request, not merely which functions
|
|
4
|
+
* the graph says are capable of it (`resolveFrontendCallers`'s own
|
|
5
|
+
* structural limit, `@descryy/runtime-graph-correlator`).
|
|
6
|
+
*
|
|
7
|
+
* **This is a write, not a read**, in the sense
|
|
8
|
+
* `PROPOSAL-code-injection-into-observed-processes.md` asks every injection
|
|
9
|
+
* mechanism to state for itself: the wrapped `fetch()` adds a real header
|
|
10
|
+
* (`x-descry-initiator-id`) to a real outgoing request. That line does not
|
|
11
|
+
* run when Descry is not attached — a same-origin request can pick up a
|
|
12
|
+
* fresh CORS preflight it would not otherwise have had. That side effect is
|
|
13
|
+
* the one `PROPOSAL-observed-frontend-caller-needs-initiator-capture.md`
|
|
14
|
+
* asked a human to sign off on before this was built; nothing here reopens
|
|
15
|
+
* that call.
|
|
16
|
+
*
|
|
17
|
+
* The call-site stack itself never travels over the wire that carries the
|
|
18
|
+
* application's own traffic — only a short, opaque per-call id does, as the
|
|
19
|
+
* header value. The stack text instead travels over Playwright's own CDP
|
|
20
|
+
* binding channel (`page.exposeFunction`), straight to `take()` below,
|
|
21
|
+
* keyed by that same id.
|
|
22
|
+
*
|
|
23
|
+
* **Why the real `fetch()` is held until the exposed call returns.** Firing
|
|
24
|
+
* the buffering call and the real request concurrently would make
|
|
25
|
+
* correctness depend on which of two independent async channels — the CDP
|
|
26
|
+
* binding call, and Chromium's own network stack — happens to land at this
|
|
27
|
+
* process first. That is exactly the kind of timing assumption §27's
|
|
28
|
+
* "similarity must never be treated as causality" rules out. Awaiting the
|
|
29
|
+
* binding call before dispatching `fetch` makes the ordering true by
|
|
30
|
+
* construction instead: the real request cannot be sent until this module
|
|
31
|
+
* has already buffered the stack it will be looked up by.
|
|
32
|
+
*
|
|
33
|
+
* Chromium only — `page.addInitScript` + `page.exposeFunction` is the
|
|
34
|
+
* mechanism, and no non-CDP-based engine here exposes an equivalent pair.
|
|
35
|
+
* `fetch()` only, not `XMLHttpRequest`. Both boundaries are the scope
|
|
36
|
+
* `PROPOSAL-observed-frontend-caller-needs-initiator-capture.md` drew, not
|
|
37
|
+
* an oversight of this module.
|
|
38
|
+
*/
|
|
39
|
+
export const INITIATOR_HEADER_NAME = "x-descry-initiator-id";
|
|
40
|
+
/**
|
|
41
|
+
* Bound on stacks buffered but never claimed. Without it, every request
|
|
42
|
+
* whose id is never looked up — the header stripped in transit, a page that
|
|
43
|
+
* never triggers a matching `onRequest` at all — would grow this map for
|
|
44
|
+
* the entire life of the page. FIFO eviction via `Map`'s own insertion
|
|
45
|
+
* order; a stack this old was never going to be claimed by the time
|
|
46
|
+
* something newer needs the room.
|
|
47
|
+
*/
|
|
48
|
+
const MAX_BUFFERED_STACKS = 2000;
|
|
49
|
+
const CAPTURE_BINDING_NAME = "__descryCaptureInitiatorStack";
|
|
50
|
+
export function createFrontendInitiatorCapture() {
|
|
51
|
+
const buffered = new Map();
|
|
52
|
+
function record(id, stackText) {
|
|
53
|
+
if (buffered.size >= MAX_BUFFERED_STACKS) {
|
|
54
|
+
const oldest = buffered.keys().next().value;
|
|
55
|
+
if (oldest !== undefined)
|
|
56
|
+
buffered.delete(oldest);
|
|
57
|
+
}
|
|
58
|
+
buffered.set(id, stackText);
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
async install(page) {
|
|
62
|
+
await page.exposeFunction(CAPTURE_BINDING_NAME, record);
|
|
63
|
+
await page.addInitScript({ content: initScriptSource() });
|
|
64
|
+
},
|
|
65
|
+
take(id) {
|
|
66
|
+
const stackText = buffered.get(id);
|
|
67
|
+
buffered.delete(id);
|
|
68
|
+
return stackText;
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The injected wrapper, as a plain string — `page.addInitScript`'s
|
|
74
|
+
* string form, so this never needs its own transpilation step and runs
|
|
75
|
+
* exactly as written in the page's own JS engine, not this process's.
|
|
76
|
+
*
|
|
77
|
+
* Every call site outcome degrades to "no id header" or "no functionName"
|
|
78
|
+
* rather than throwing: `bind(...)` is wrapped in its own `.catch`, and a
|
|
79
|
+
* `fetch()` whose `input` is neither a string, `URL`, nor `Request` still
|
|
80
|
+
* reaches `originalFetch` unmodified so the native `TypeError` a caller
|
|
81
|
+
* would have gotten anyway is the one they still get — this wrapper adds a
|
|
82
|
+
* header, it does not change what counts as a valid `fetch()` call.
|
|
83
|
+
*/
|
|
84
|
+
function initScriptSource() {
|
|
85
|
+
return `(() => {
|
|
86
|
+
const originalFetch = window.fetch;
|
|
87
|
+
if (typeof originalFetch !== "function") return;
|
|
88
|
+
const bind = window[${JSON.stringify(CAPTURE_BINDING_NAME)}];
|
|
89
|
+
if (typeof bind !== "function") return;
|
|
90
|
+
let counter = 0;
|
|
91
|
+
window.fetch = function descryFetch(input, init) {
|
|
92
|
+
const id = Date.now().toString(36) + "-" + Math.random().toString(36).slice(2) + "-" + (counter++);
|
|
93
|
+
const stack = (new Error()).stack || "";
|
|
94
|
+
const self = this;
|
|
95
|
+
const dispatch = () => {
|
|
96
|
+
let headerSource = init && init.headers !== undefined ? init.headers : undefined;
|
|
97
|
+
if (headerSource === undefined && typeof Request !== "undefined" && input instanceof Request) {
|
|
98
|
+
headerSource = input.headers;
|
|
99
|
+
}
|
|
100
|
+
const headers = new Headers(headerSource);
|
|
101
|
+
headers.set(${JSON.stringify(INITIATOR_HEADER_NAME)}, id);
|
|
102
|
+
if (typeof Request !== "undefined" && input instanceof Request) {
|
|
103
|
+
return originalFetch.call(self, new Request(input, { headers: headers }));
|
|
104
|
+
}
|
|
105
|
+
return originalFetch.call(self, input, Object.assign({}, init, { headers: headers }));
|
|
106
|
+
};
|
|
107
|
+
return bind(id, stack).catch(function () {}).then(dispatch);
|
|
108
|
+
};
|
|
109
|
+
})();`;
|
|
110
|
+
}
|
|
111
|
+
const FRAME_LINE = /^\s*at\s/;
|
|
112
|
+
/**
|
|
113
|
+
* Strips `descryFetch`'s own frame from a raw V8 stack, so the remaining
|
|
114
|
+
* text's first frame is the real call site rather than this module's own
|
|
115
|
+
* wrapper.
|
|
116
|
+
*
|
|
117
|
+
* **Why this can be a fixed "drop the first frame line" rule rather than a
|
|
118
|
+
* search for a named function.** `new Error().stack` is captured
|
|
119
|
+
* synchronously, as the wrapper's first statement — before the `await`-
|
|
120
|
+
* shaped `.then(dispatch)` chain below it ever runs. V8's stack is built
|
|
121
|
+
* from the synchronous call chain live at capture time, so the innermost
|
|
122
|
+
* frame is deterministically `descryFetch` itself, every time, regardless
|
|
123
|
+
* of what called it. Stripping by *position* (first frame line) rather than
|
|
124
|
+
* by *name* also survives minification of this wrapper's own name, which a
|
|
125
|
+
* name-based strip would not.
|
|
126
|
+
*
|
|
127
|
+
* Applied to raw text, before parsing — same reasoning
|
|
128
|
+
* `rewriteScriptUrls`'s own doc gives for rewriting before rather than
|
|
129
|
+
* after: `createV8StackTraceParser` already treats frame 0 of whatever it
|
|
130
|
+
* receives as `primaryFrameIndex`, so removing the wrapper's line here
|
|
131
|
+
* means the parser's existing, already-tested convention does the right
|
|
132
|
+
* thing unmodified, instead of teaching every consumer a second index to
|
|
133
|
+
* skip.
|
|
134
|
+
*/
|
|
135
|
+
export function stripInitiatorWrapperFrame(rawStackText) {
|
|
136
|
+
const lines = rawStackText.split(/\r\n|\r|\n/);
|
|
137
|
+
const frameIndex = lines.findIndex((line) => FRAME_LINE.test(line));
|
|
138
|
+
if (frameIndex === -1)
|
|
139
|
+
return rawStackText;
|
|
140
|
+
return [...lines.slice(0, frameIndex), ...lines.slice(frameIndex + 1)].join("\n");
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=frontend-initiator-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontend-initiator-capture.js","sourceRoot":"","sources":["../src/frontend-initiator-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAIH,MAAM,CAAC,MAAM,qBAAqB,GAAG,uBAAuB,CAAC;AAE7D;;;;;;;GAOG;AACH,MAAM,mBAAmB,GAAG,IAAI,CAAC;AAEjC,MAAM,oBAAoB,GAAG,+BAA+B,CAAC;AAwB7D,MAAM,UAAU,8BAA8B;IAC5C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE3C,SAAS,MAAM,CAAC,EAAU,EAAE,SAAiB;QAC3C,IAAI,QAAQ,CAAC,IAAI,IAAI,mBAAmB,EAAE,CAAC;YACzC,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC;YAC5C,IAAI,MAAM,KAAK,SAAS;gBAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACpD,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;IAC9B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,OAAO,CAAC,IAAU;YACtB,MAAM,IAAI,CAAC,cAAc,CAAC,oBAAoB,EAAE,MAAM,CAAC,CAAC;YACxD,MAAM,IAAI,CAAC,aAAa,CAAC,EAAE,OAAO,EAAE,gBAAgB,EAAE,EAAE,CAAC,CAAC;QAC5D,CAAC;QACD,IAAI,CAAC,EAAU;YACb,MAAM,SAAS,GAAG,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACnC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACpB,OAAO,SAAS,CAAC;QACnB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;GAWG;AACH,SAAS,gBAAgB;IACvB,OAAO;;;0BAGiB,IAAI,CAAC,SAAS,CAAC,oBAAoB,CAAC;;;;;;;;;;;;;sBAaxC,IAAI,CAAC,SAAS,CAAC,qBAAqB,CAAC;;;;;;;;QAQnD,CAAC;AACT,CAAC;AAED,MAAM,UAAU,GAAG,UAAU,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,0BAA0B,CAAC,YAAoB;IAC7D,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;IAC/C,MAAM,UAAU,GAAG,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IACpE,IAAI,UAAU,KAAK,CAAC,CAAC;QAAE,OAAO,YAAY,CAAC;IAC3C,OAAO,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,EAAE,GAAG,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpF,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type { BrowserSessionOptions, BrowserSession, ChromiumUnavailable, ChromiumAvailability, BrowserSessionLaunch } from "./browser-session.ts";
|
|
2
|
+
export { launchBrowserSession, launchBrowserSessionOrRefuse, resolveChromiumExecutable, BrowserUnavailableError, CHROMIUM_INSTALL_REMEDY, } from "./browser-session.ts";
|
|
3
|
+
export type { DomElementSnapshot } from "./dom-evidence.ts";
|
|
4
|
+
export { captureElementSnapshot } from "./dom-evidence.ts";
|
|
5
|
+
export type { BrowserActionCollectorOptions, BrowserActionCollector, WaitForOptions } from "./browser-action-collector.ts";
|
|
6
|
+
export { createBrowserActionCollector } from "./browser-action-collector.ts";
|
|
7
|
+
export type { BrowserConsoleCollectorOptions } from "./browser-console-collector.ts";
|
|
8
|
+
export { createBrowserConsoleCollector } from "./browser-console-collector.ts";
|
|
9
|
+
export type { BrowserNetworkCollectorOptions } from "./browser-network-collector.ts";
|
|
10
|
+
export { createBrowserNetworkCollector } from "./browser-network-collector.ts";
|
|
11
|
+
export type { HeaderCorrelationIds } from "./network-correlation.ts";
|
|
12
|
+
export { extractHeaderCorrelationIds } from "./network-correlation.ts";
|
|
13
|
+
export type { ScriptUrlRewrite, SourceRootResolver } from "./script-url-mapping.ts";
|
|
14
|
+
export { rewriteScriptUrls } from "./script-url-mapping.ts";
|
|
15
|
+
export { INITIATOR_HEADER_NAME, stripInitiatorWrapperFrame } from "./frontend-initiator-capture.ts";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,qBAAqB,EAAE,cAAc,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACnJ,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAE3D,YAAY,EAAE,6BAA6B,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,+BAA+B,CAAC;AAC3H,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAE7E,YAAY,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAE/E,YAAY,EAAE,8BAA8B,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAE/E,YAAY,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AACrE,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAEvE,YAAY,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AACpF,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { launchBrowserSession, launchBrowserSessionOrRefuse, resolveChromiumExecutable, BrowserUnavailableError, CHROMIUM_INSTALL_REMEDY, } from "./browser-session.js";
|
|
2
|
+
export { captureElementSnapshot } from "./dom-evidence.js";
|
|
3
|
+
export { createBrowserActionCollector } from "./browser-action-collector.js";
|
|
4
|
+
export { createBrowserConsoleCollector } from "./browser-console-collector.js";
|
|
5
|
+
export { createBrowserNetworkCollector } from "./browser-network-collector.js";
|
|
6
|
+
export { extractHeaderCorrelationIds } from "./network-correlation.js";
|
|
7
|
+
export { rewriteScriptUrls } from "./script-url-mapping.js";
|
|
8
|
+
export { INITIATOR_HEADER_NAME, stripInitiatorWrapperFrame } from "./frontend-initiator-capture.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,oBAAoB,EACpB,4BAA4B,EAC5B,yBAAyB,EACzB,uBAAuB,EACvB,uBAAuB,GACxB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAG3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAC;AAG7E,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAG/E,OAAO,EAAE,6BAA6B,EAAE,MAAM,gCAAgC,CAAC;AAG/E,OAAO,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAE5D,OAAO,EAAE,qBAAqB,EAAE,0BAA0B,EAAE,MAAM,iCAAiC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation-id extraction from real HTTP headers (structured access, not
|
|
3
|
+
* text regex) -- covers the top three levels of plan §10's six-level
|
|
4
|
+
* preference order: trace ID, request ID, W3C trace context. Direct header
|
|
5
|
+
* access makes the W3C case cheap here in a way it wasn't for
|
|
6
|
+
* `backend-observation`'s `extractCorrelationIds`, which only ever sees
|
|
7
|
+
* unstructured log text and explicitly cuts W3C parsing as a documented
|
|
8
|
+
* V1 gap -- this collector doesn't need that same cut.
|
|
9
|
+
*/
|
|
10
|
+
export interface HeaderCorrelationIds {
|
|
11
|
+
readonly traceId: string | null;
|
|
12
|
+
readonly requestId: string | null;
|
|
13
|
+
}
|
|
14
|
+
export declare function extractHeaderCorrelationIds(headers: Readonly<Record<string, string>>): HeaderCorrelationIds;
|
|
15
|
+
//# sourceMappingURL=network-correlation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-correlation.d.ts","sourceRoot":"","sources":["../src/network-correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAUD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,oBAAoB,CAS3G"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation-id extraction from real HTTP headers (structured access, not
|
|
3
|
+
* text regex) -- covers the top three levels of plan §10's six-level
|
|
4
|
+
* preference order: trace ID, request ID, W3C trace context. Direct header
|
|
5
|
+
* access makes the W3C case cheap here in a way it wasn't for
|
|
6
|
+
* `backend-observation`'s `extractCorrelationIds`, which only ever sees
|
|
7
|
+
* unstructured log text and explicitly cuts W3C parsing as a documented
|
|
8
|
+
* V1 gap -- this collector doesn't need that same cut.
|
|
9
|
+
*/
|
|
10
|
+
// W3C Trace Context: "00-<32 hex trace-id>-<16 hex parent-id>-<2 hex flags>".
|
|
11
|
+
const TRACEPARENT_PATTERN = /^([0-9a-f]{2})-([0-9a-f]{32})-([0-9a-f]{16})-([0-9a-f]{2})$/i;
|
|
12
|
+
function firstHeader(headers, names) {
|
|
13
|
+
for (const name of names) {
|
|
14
|
+
const value = headers[name];
|
|
15
|
+
if (value !== undefined && value.length > 0)
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
export function extractHeaderCorrelationIds(headers) {
|
|
21
|
+
const explicitTraceId = firstHeader(headers, ["x-trace-id", "trace-id"]);
|
|
22
|
+
const traceparent = headers["traceparent"];
|
|
23
|
+
const w3cTraceId = traceparent !== undefined ? (TRACEPARENT_PATTERN.exec(traceparent)?.[2] ?? null) : null;
|
|
24
|
+
return {
|
|
25
|
+
traceId: explicitTraceId ?? w3cTraceId,
|
|
26
|
+
requestId: firstHeader(headers, ["x-request-id", "request-id"]),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=network-correlation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-correlation.js","sourceRoot":"","sources":["../src/network-correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,8EAA8E;AAC9E,MAAM,mBAAmB,GAAG,8DAA8D,CAAC;AAO3F,SAAS,WAAW,CAAC,OAAyC,EAAE,KAAwB;IACtF,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;QAC5B,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;IAC5D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,2BAA2B,CAAC,OAAyC;IACnF,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,EAAE,CAAC,YAAY,EAAE,UAAU,CAAC,CAAC,CAAC;IACzE,MAAM,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IAC3C,MAAM,UAAU,GAAG,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAE3G,OAAO;QACL,OAAO,EAAE,eAAe,IAAI,UAAU;QACtC,SAAS,EAAE,WAAW,CAAC,OAAO,EAAE,CAAC,cAAc,EAAE,YAAY,CAAC,CAAC;KAChE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RT-023's collector half: rewriting a browser script URL to the on-disk
|
|
3
|
+
* file it was served from, so a page-error stack can be parsed into real
|
|
4
|
+
* `SourceLocation`s instead of preserved as text.
|
|
5
|
+
*
|
|
6
|
+
* **The rewrite happens on the stack text, before parsing, deliberately.**
|
|
7
|
+
* A `StackTraceParser` and its `SourceLocationResolver` already know how to
|
|
8
|
+
* turn `at f (/abs/path.js:12:5)` into a resolved location — including
|
|
9
|
+
* source-map handling and staleness detection, which took real work to get
|
|
10
|
+
* right. Rewriting the URL first lets all of that apply unchanged, where
|
|
11
|
+
* post-processing parsed frames would mean reimplementing it here and
|
|
12
|
+
* keeping two copies honest.
|
|
13
|
+
*
|
|
14
|
+
* **What this can and cannot claim.** `CollectorContext.resolveSourceRoot`
|
|
15
|
+
* answers "which service's declared root does this origin belong to", and
|
|
16
|
+
* `ConfiguredServiceRoot.declaredCwd` is named for what it is: configured,
|
|
17
|
+
* not verified. So a successful rewrite proves the origin maps to a
|
|
18
|
+
* directory — **not** that the file the browser executed came from the
|
|
19
|
+
* checkout in it. The downstream resolver's own existence check is the real
|
|
20
|
+
* refusal point, and an unmapped or non-existent path degrades to the URL
|
|
21
|
+
* itself rather than to a fabricated file. That is RT-031's class again:
|
|
22
|
+
* a match on an observable proxy is not proof of the underlying identity.
|
|
23
|
+
*
|
|
24
|
+
* Nothing is rewritten when the resolver refuses, when it is absent, or
|
|
25
|
+
* when the URL is not one this execution spawned — a CDN script, a browser
|
|
26
|
+
* extension, `about:blank`. Those are routine, not failures.
|
|
27
|
+
*/
|
|
28
|
+
import type { ServiceRootLookup } from "@descryy/runtime-contracts";
|
|
29
|
+
export type SourceRootResolver = (origin: string) => ServiceRootLookup;
|
|
30
|
+
export interface ScriptUrlRewrite {
|
|
31
|
+
readonly text: string;
|
|
32
|
+
/** Every URL seen, and what happened to it — so a collector can report coverage rather than implying it. */
|
|
33
|
+
readonly outcomes: readonly {
|
|
34
|
+
readonly url: string;
|
|
35
|
+
readonly mapped: string | null;
|
|
36
|
+
readonly reason: string | null;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
export declare function rewriteScriptUrls(stackText: string, resolveSourceRoot: SourceRootResolver | undefined): ScriptUrlRewrite;
|
|
40
|
+
//# sourceMappingURL=script-url-mapping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-url-mapping.d.ts","sourceRoot":"","sources":["../src/script-url-mapping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAKH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEpE,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,iBAAiB,CAAC;AAKvE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,4GAA4G;IAC5G,QAAQ,CAAC,QAAQ,EAAE,SAAS;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,EAAE,CAAC;CACxH;AAED,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,kBAAkB,GAAG,SAAS,GAAG,gBAAgB,CAoExH"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RT-023's collector half: rewriting a browser script URL to the on-disk
|
|
3
|
+
* file it was served from, so a page-error stack can be parsed into real
|
|
4
|
+
* `SourceLocation`s instead of preserved as text.
|
|
5
|
+
*
|
|
6
|
+
* **The rewrite happens on the stack text, before parsing, deliberately.**
|
|
7
|
+
* A `StackTraceParser` and its `SourceLocationResolver` already know how to
|
|
8
|
+
* turn `at f (/abs/path.js:12:5)` into a resolved location — including
|
|
9
|
+
* source-map handling and staleness detection, which took real work to get
|
|
10
|
+
* right. Rewriting the URL first lets all of that apply unchanged, where
|
|
11
|
+
* post-processing parsed frames would mean reimplementing it here and
|
|
12
|
+
* keeping two copies honest.
|
|
13
|
+
*
|
|
14
|
+
* **What this can and cannot claim.** `CollectorContext.resolveSourceRoot`
|
|
15
|
+
* answers "which service's declared root does this origin belong to", and
|
|
16
|
+
* `ConfiguredServiceRoot.declaredCwd` is named for what it is: configured,
|
|
17
|
+
* not verified. So a successful rewrite proves the origin maps to a
|
|
18
|
+
* directory — **not** that the file the browser executed came from the
|
|
19
|
+
* checkout in it. The downstream resolver's own existence check is the real
|
|
20
|
+
* refusal point, and an unmapped or non-existent path degrades to the URL
|
|
21
|
+
* itself rather than to a fabricated file. That is RT-031's class again:
|
|
22
|
+
* a match on an observable proxy is not proof of the underlying identity.
|
|
23
|
+
*
|
|
24
|
+
* Nothing is rewritten when the resolver refuses, when it is absent, or
|
|
25
|
+
* when the URL is not one this execution spawned — a CDN script, a browser
|
|
26
|
+
* extension, `about:blank`. Those are routine, not failures.
|
|
27
|
+
*/
|
|
28
|
+
import { statSync } from "node:fs";
|
|
29
|
+
import { join } from "node:path";
|
|
30
|
+
/** `http://localhost:41237/public/app.js:12:5` — the trailing `:line:col` is left in place for the stack parser to read. */
|
|
31
|
+
const SCRIPT_URL = /\bhttps?:\/\/[^\s()]+/g;
|
|
32
|
+
export function rewriteScriptUrls(stackText, resolveSourceRoot) {
|
|
33
|
+
if (resolveSourceRoot === undefined) {
|
|
34
|
+
return { text: stackText, outcomes: [] };
|
|
35
|
+
}
|
|
36
|
+
const outcomes = [];
|
|
37
|
+
const text = stackText.replace(SCRIPT_URL, (match) => {
|
|
38
|
+
// Strip the stack's own `:line:column` suffix before treating the rest
|
|
39
|
+
// as a URL — `new URL()` would otherwise parse `:12` as part of the
|
|
40
|
+
// path and produce a file that never existed.
|
|
41
|
+
const suffix = /:(\d+):(\d+)$/.exec(match);
|
|
42
|
+
const urlText = suffix === null ? match : match.slice(0, match.length - suffix[0].length);
|
|
43
|
+
let url;
|
|
44
|
+
try {
|
|
45
|
+
url = new URL(urlText);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
outcomes.push({ url: match, mapped: null, reason: "not a parseable URL" });
|
|
49
|
+
return match;
|
|
50
|
+
}
|
|
51
|
+
const lookup = resolveSourceRoot(url.origin);
|
|
52
|
+
if (!lookup.found) {
|
|
53
|
+
outcomes.push({ url: match, mapped: null, reason: lookup.message });
|
|
54
|
+
return match;
|
|
55
|
+
}
|
|
56
|
+
// `url.pathname` is always absolute; `join` would otherwise treat a
|
|
57
|
+
// leading slash as a fresh root and discard the service's own cwd.
|
|
58
|
+
const candidate = join(lookup.root.declaredCwd, url.pathname.replace(/^\/+/, ""));
|
|
59
|
+
// **`isFile()`, not `existsSync` — and the difference is not pedantry.**
|
|
60
|
+
// An inline `<script>` on a page served at `/` produces frames V8 names
|
|
61
|
+
// after the *document* URL, whose pathname is `/` and nothing else. That
|
|
62
|
+
// joins to `declaredCwd` itself: a real directory, so `existsSync` said
|
|
63
|
+
// yes and this function rewrote a stack frame to a directory no browser
|
|
64
|
+
// ever executed. The downstream `SourceLocationResolver` then reached
|
|
65
|
+
// `readFileSync(directory)`, threw `EISDIR`, and the whole
|
|
66
|
+
// `NETWORK_REQUEST` row was lost with it — an observation discarded to
|
|
67
|
+
// protect a nullable field. `statSync` answers the question that was
|
|
68
|
+
// always being asked (is there a FILE here?) rather than the weaker one
|
|
69
|
+
// that happened to be cheap.
|
|
70
|
+
//
|
|
71
|
+
// `statSync` throws on a missing path where `existsSync` returned false,
|
|
72
|
+
// so the absence case is caught rather than propagated: both branches end
|
|
73
|
+
// in the same refusal, with the reason naming which one it was.
|
|
74
|
+
let isFile;
|
|
75
|
+
try {
|
|
76
|
+
isFile = statSync(candidate).isFile();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
outcomes.push({ url: match, mapped: null, reason: `resolved to "${candidate}", which does not exist on disk` });
|
|
80
|
+
return match;
|
|
81
|
+
}
|
|
82
|
+
if (!isFile) {
|
|
83
|
+
// The origin maps to a root and the pathname points at something that
|
|
84
|
+
// is not a file in it — a directory (the `/` case above), a socket, a
|
|
85
|
+
// device. Reported rather than rewritten: a path that is not a source
|
|
86
|
+
// file is worse than the URL, which at least says truthfully where the
|
|
87
|
+
// browser got it.
|
|
88
|
+
outcomes.push({ url: match, mapped: null, reason: `resolved to "${candidate}", which is not a file on disk` });
|
|
89
|
+
return match;
|
|
90
|
+
}
|
|
91
|
+
outcomes.push({ url: match, mapped: candidate, reason: null });
|
|
92
|
+
return suffix === null ? candidate : `${candidate}${suffix[0]}`;
|
|
93
|
+
});
|
|
94
|
+
return { text, outcomes };
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=script-url-mapping.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"script-url-mapping.js","sourceRoot":"","sources":["../src/script-url-mapping.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@descryy/runtime-browser",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Browser runtime: launch, isolated context, navigation, click/type/wait, DOM inspection, screenshots, console/page-error/network capture, all through the Collector contract (plan \u00a77/\u00a78). See documents/decisions-inbox/PROPOSAL-B-playwright-for-browser-runtime.md for the Playwright-vs-CDP call.",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.5"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"registry": "https://registry.npmjs.org",
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -b"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@descryy/runtime-contracts": "0.2.0",
|
|
28
|
+
"@descryy/runtime-backend-observation": "0.2.0",
|
|
29
|
+
"playwright-core": "^1.62.1",
|
|
30
|
+
"@descryy/runtime-adapter-typescript": "0.2.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@descryy/runtime-controller": "*",
|
|
34
|
+
"@descryy/runtime-evidence-store": "*"
|
|
35
|
+
}
|
|
36
|
+
}
|