mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(browser): separate domain readiness and worker attribution coverage
This commit is contained in:
1 parent
91f06f55a3
commit
ba781330d6
8 files changed
+147
-11
No files matched your search
@@ -54,6 +54,20 @@ from a protocol omission. Rejected DOM facts also mark derived URL and agent-hin
|
||||
metadata incomplete, while valid facts from other nodes and documents remain
|
||||
available in the capture.
|
||||
|
||||
Page network and console coverage starts independently at the first observed
|
||||
event from that domain or its successful enable acknowledgment. The capture's
|
||||
lifecycle start is earlier and does not establish domain readiness. Events
|
||||
received before the enable reply remain part of the observation.
|
||||
|
||||
Worker output is a final target-inventory snapshot. `attached` describes any
|
||||
debugging client, not an REA-owned observing session. The selected page-session
|
||||
collector does not collect worker-session requests, console events, or scripts,
|
||||
and workers that disappeared before the snapshot may be missing; worker
|
||||
coverage is therefore attach-limited. Missing ownership metadata is reported as
|
||||
`unknown_target_relationship` with unavailable worker attribution, separately
|
||||
from a proven foreign owner excluded as `out_of_target_scope`. A shared origin
|
||||
alone establishes neither relationship.
|
||||
|
||||
Passive comparison keeps lookup identity, cross-capture correspondence, content
|
||||
evidence, and inventory coverage separate:
|
||||
|
||||
|
||||
@@ -62,7 +62,8 @@ export class CdpCaptureCompleteness {
|
||||
if (POLICY_REASONS.has(reason)) this.#policyFiltered.add(section);
|
||||
if (
|
||||
reason === "provider_unavailable" ||
|
||||
reason === "invalid_protocol_value"
|
||||
reason === "invalid_protocol_value" ||
|
||||
reason === "unknown_target_relationship"
|
||||
)
|
||||
this.#unavailable.add(section);
|
||||
if (reason === "resource_budget_exhausted") this.#truncated.add(section);
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { inspectWebPageInputSchema } from "../domain/browserObservation.js";
|
||||
import { CdpCaptureEvents } from "./CdpCaptureEvents.js";
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("starts each domain at its first event or enable acknowledgment, preserving events before the reply", () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-10-24T00:00:00.000Z"));
|
||||
const events = new CdpCaptureEvents(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: "http://127.0.0.1:9222",
|
||||
target_id: "page",
|
||||
allowed_origins: ["https://example.test"],
|
||||
}),
|
||||
new Set(["https://example.test"]),
|
||||
);
|
||||
events.beginAuthorizedFrame("main");
|
||||
vi.setSystemTime(new Date("2026-10-24T00:00:01.000Z"));
|
||||
events.ingest({
|
||||
method: "Network.loadingFinished",
|
||||
params: { requestId: "unobserved", encodedDataLength: 0 },
|
||||
});
|
||||
vi.setSystemTime(new Date("2026-10-24T00:00:02.000Z"));
|
||||
events.markDomainEnabled("Network");
|
||||
events.markDomainEnabled("Runtime");
|
||||
expect(events.coverageStartedAt("Network")).toBe("2026-10-24T00:00:01.000Z");
|
||||
expect(events.coverageStartedAt("Runtime")).toBe("2026-10-24T00:00:02.000Z");
|
||||
});
|
||||
@@ -16,6 +16,7 @@ export interface ResponseBodyCandidate {
|
||||
|
||||
/** Event accumulator that validates and normalizes payloads at ingestion. */
|
||||
export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
readonly #coverageStarts = new Map<"Network" | "Runtime", string>();
|
||||
#scriptMetadataBudgetExclusionsRecorded = false;
|
||||
readonly #networkRequestOccurrences = new Map<string, number>();
|
||||
readonly #responseBodyExaminedOccurrences = new Map<string, number>();
|
||||
@@ -60,6 +61,7 @@ export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
|
||||
beginAuthorizedFrame(frameId: string): void {
|
||||
this.mainFrameId = frameId;
|
||||
this.#coverageStarts.clear();
|
||||
this.originViolation = false;
|
||||
this.navigationDuringCapture = false;
|
||||
this.unknownRootIdentity = false;
|
||||
@@ -91,6 +93,19 @@ export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
this.completeness.exclude("websocket_shapes", "not_approved", null);
|
||||
}
|
||||
|
||||
/** The first observed domain event or successful enable reply proves activity. */
|
||||
markDomainEnabled(domain: "Network" | "Runtime"): void {
|
||||
if (!this.#coverageStarts.has(domain))
|
||||
this.#coverageStarts.set(domain, new Date().toISOString());
|
||||
}
|
||||
|
||||
coverageStartedAt(domain: "Network" | "Runtime"): string {
|
||||
const started = this.#coverageStarts.get(domain);
|
||||
if (started === undefined)
|
||||
throw new Error(`${domain} observation has not been enabled`);
|
||||
return started;
|
||||
}
|
||||
|
||||
beginFinalCapture(frameId: string): void {
|
||||
this.mainFrameId = frameId;
|
||||
// Deliberately does not clear `navigationDuringCapture`. A main-frame
|
||||
@@ -126,6 +141,12 @@ export class CdpCaptureEvents implements CdpCaptureEventsState {
|
||||
}
|
||||
|
||||
ingest(event: CdpEvent): void {
|
||||
if (this.mainFrameId !== undefined) {
|
||||
if (event.method.startsWith("Network."))
|
||||
this.markDomainEnabled("Network");
|
||||
if (event.method.startsWith("Runtime."))
|
||||
this.markDomainEnabled("Runtime");
|
||||
}
|
||||
if (event.method === "Page.frameNavigated") {
|
||||
handlers.handleFrameNavigated(this, event.params);
|
||||
return;
|
||||
|
||||
@@ -86,6 +86,7 @@ export const capturePage = async (
|
||||
allowedOrigins,
|
||||
limitations: [
|
||||
"Observation starts when REA attaches; prior network and console activity is unavailable.",
|
||||
"Worker targets are a final inventory snapshot, not worker-session observation. Their attached flag describes any debugging client; this page-session collector does not observe worker network, console, or script events, and short-lived workers may be absent from the snapshot.",
|
||||
"Raw network headers, bodies, cookies, storage values, console objects, and WebSocket payloads are never retained; explicitly requested primitive console text is retained verbatim, while payload captures retain value-free shapes.",
|
||||
"Source maps are reported only as declarative URLs and are not fetched.",
|
||||
"URL-less scripts and console events without an allowed source URL are excluded because their origin cannot be proven.",
|
||||
@@ -263,7 +264,7 @@ const authorizeObservationWindow = async (
|
||||
if (mainFrame === undefined)
|
||||
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
|
||||
events.beginAuthorizedFrame(mainFrame.frame_id);
|
||||
await enableObservationDomains(connection, sessionId, signal);
|
||||
await enableObservationDomains(state);
|
||||
await report(context.progress, 2, "Observing page events");
|
||||
await delayWithCancellation(
|
||||
context.input.observation_ms,
|
||||
@@ -368,12 +369,12 @@ const normalizedInspection = (
|
||||
network: {
|
||||
requests: [...state.events.network.values()],
|
||||
websocket_connections: [...state.events.webSocketConnectionsById.values()],
|
||||
coverage_started_at: state.startedAt,
|
||||
coverage_started_at: state.events.coverageStartedAt("Network"),
|
||||
prior_activity_available: false,
|
||||
},
|
||||
console: {
|
||||
events: [...state.events.console],
|
||||
coverage_started_at: state.startedAt,
|
||||
coverage_started_at: state.events.coverageStartedAt("Runtime"),
|
||||
prior_activity_available: false,
|
||||
},
|
||||
workers: [...captured.workers],
|
||||
@@ -391,14 +392,13 @@ const normalizedInspection = (
|
||||
limitations: state.limitations,
|
||||
});
|
||||
|
||||
const enableObservationDomains = async (
|
||||
connection: CdpConnection,
|
||||
sessionId: string | undefined,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> => {
|
||||
const enableObservationDomains = async (state: CaptureState): Promise<void> => {
|
||||
const { connection, sessionId, signal } = state.context;
|
||||
await connection.send("Runtime.enable", {}, sessionId, signal);
|
||||
state.events.markDomainEnabled("Runtime");
|
||||
await connection.send("Debugger.enable", {}, sessionId, signal);
|
||||
await connection.send("Network.enable", {}, sessionId, signal);
|
||||
state.events.markDomainEnabled("Network");
|
||||
};
|
||||
|
||||
const accessibilityForFrames = async (
|
||||
|
||||
@@ -23,6 +23,9 @@ export const captureWorkers = async (
|
||||
): Promise<WebPageInspection["workers"]> => {
|
||||
const { context, allowedOrigins, limitations, events } = input;
|
||||
const completeness = events.completeness;
|
||||
// This final target snapshot cannot establish worker lifetimes or observed
|
||||
// worker-session activity during the preceding page observation window.
|
||||
completeness.attachLimited("workers");
|
||||
const result = await optionalCdpCommand(
|
||||
{ ...context, sessionId: undefined },
|
||||
"Target.getTargets",
|
||||
@@ -78,8 +81,18 @@ export const captureWorkers = async (
|
||||
const relatedToPage =
|
||||
openerId === context.target.id ||
|
||||
(parentFrameId !== undefined && frameIds.has(parentFrameId));
|
||||
if (!relatedToPage) {
|
||||
completeness.exclude("workers", "out_of_target_scope");
|
||||
const relationship = relatedToPage
|
||||
? "related"
|
||||
: openerId !== undefined || parentFrameId !== undefined
|
||||
? "foreign"
|
||||
: "unknown";
|
||||
if (relationship !== "related") {
|
||||
completeness.exclude(
|
||||
"workers",
|
||||
relationship === "foreign"
|
||||
? "out_of_target_scope"
|
||||
: "unknown_target_relationship",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (url === undefined) {
|
||||
|
||||
@@ -36,6 +36,7 @@ const browserExclusionReasonSchema = z.enum([
|
||||
"unattributed_origin",
|
||||
"not_approved",
|
||||
"out_of_target_scope",
|
||||
"unknown_target_relationship",
|
||||
"provider_unavailable",
|
||||
"invalid_protocol_value",
|
||||
"resource_budget_exhausted",
|
||||
|
||||
@@ -5,6 +5,63 @@ import { inspectWebPageInputSchema } from "../../../src/domain/browserObservatio
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
it.each([
|
||||
["related", { openerId: "allowed-page" }, 1, null],
|
||||
["foreign", { openerId: "other-page" }, 0, "out_of_target_scope"],
|
||||
["unknown", {}, 0, "unknown_target_relationship"],
|
||||
] as const)(
|
||||
"keeps %s worker ownership separate from attachment state",
|
||||
async (_relation, ownership, count, exclusion) => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandResult: (command, origin) =>
|
||||
command.method === "Target.getTargets"
|
||||
? {
|
||||
targetInfos: [
|
||||
{
|
||||
targetId: "worker",
|
||||
type: "worker",
|
||||
url: `${origin}/worker.js`,
|
||||
attached: true,
|
||||
...ownership,
|
||||
},
|
||||
],
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
trackBrowser(browser);
|
||||
const result = await new CdpBrowserProvider().inspectPage(
|
||||
inspectWebPageInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
target_id: "allowed-page",
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
observation_ms: 0,
|
||||
}),
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value.workers).toHaveLength(count);
|
||||
expect(result.value.completeness.attach_limited_sections).toContain(
|
||||
"workers",
|
||||
);
|
||||
expect(result.value.limitations.join(" ")).toContain(
|
||||
"not worker-session observation",
|
||||
);
|
||||
if (exclusion !== null)
|
||||
expect(result.value.completeness.excluded).toContainEqual({
|
||||
section: "workers",
|
||||
reason: exclusion,
|
||||
count: 1,
|
||||
});
|
||||
if (_relation === "unknown") {
|
||||
expect(result.value.completeness.unavailable_sections).toContain(
|
||||
"workers",
|
||||
);
|
||||
expect(result.value.completeness.policy_filtered_sections).not.toContain(
|
||||
"workers",
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves Document Link fields, structured policies and derived agent hints", async () => {
|
||||
const browser = await startFakeCdpBrowser({
|
||||
commandEvents(command, origin) {
|
||||
|
||||
Reference in new issue
Block a user