mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(browser): retain excluded source-map declaration coverage
This commit is contained in:
@@ -558,6 +558,13 @@ Network and console coverage starts only after REA attaches and enables the rele
|
||||
|
||||
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation uses `source_map_fetch_timeout_ms` (default 30000) and a 64 MiB aggregate streamed-response budget across redirects. The byte budget has no runtime override. A fetch overrun is an item limitation, not a thrown analysis timeout. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
|
||||
|
||||
When fetching is selected, source-map `excluded` facts preserve the capture
|
||||
ledger's reasons and counts for declarations rejected before download. No
|
||||
disallowed URL details are retained or fetched. `requested` and `processed`
|
||||
count the admitted fetch requests; exclusions are counted separately. Excluded
|
||||
declarations make the collection unavailable, or partial when maps were retained.
|
||||
An empty collection with no declarations remains `not_requested`.
|
||||
|
||||
Source-map items retain the script's `declared_url` separately from the observed
|
||||
`effective_url`. Successful maps use the latter as their source-resolution base.
|
||||
Failed fetches retain the last response URL observed within the approved origins;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type WebPageInspection,
|
||||
} from "../domain/browserObservationSchemas.js";
|
||||
import { IsolatedWebBundleAnalyzer } from "./IsolatedWebBundleAnalyzer.js";
|
||||
import { sourceMapsWithCaptureExclusions } from "../domain/webSourceMapCoverage.js";
|
||||
import type {
|
||||
AnalyzeWebBundleInput,
|
||||
WebBundleAnalysis,
|
||||
@@ -195,16 +196,19 @@ export class CdpBrowserProvider implements BrowserObservationPort {
|
||||
try {
|
||||
captured = await this.#capture(input, options, "analyze_web_bundle");
|
||||
const sourceMaps = input.fetch_source_maps
|
||||
? await fetchWebSourceMaps(
|
||||
captured.sourceMapRequests,
|
||||
{
|
||||
...input,
|
||||
allowed_origins:
|
||||
input.allowed_origins.length > 0
|
||||
? input.allowed_origins
|
||||
: [captured.inspection.target.origin],
|
||||
},
|
||||
options.signal,
|
||||
? sourceMapsWithCaptureExclusions(
|
||||
await fetchWebSourceMaps(
|
||||
captured.sourceMapRequests,
|
||||
{
|
||||
...input,
|
||||
allowed_origins:
|
||||
input.allowed_origins.length > 0
|
||||
? input.allowed_origins
|
||||
: [captured.inspection.target.origin],
|
||||
},
|
||||
options.signal,
|
||||
),
|
||||
captured.inspection.completeness.excluded,
|
||||
)
|
||||
: undefined;
|
||||
return await this.#bundleAnalyzer.analyze(
|
||||
|
||||
@@ -45,7 +45,7 @@ export type BrowserExclusionReason = z.infer<
|
||||
typeof browserExclusionReasonSchema
|
||||
>;
|
||||
|
||||
const browserExclusionSchema = z.object({
|
||||
export const browserExclusionSchema = z.object({
|
||||
section: browserSectionSchema,
|
||||
reason: browserExclusionReasonSchema,
|
||||
count: z.int().min(0).nullable(),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { browserExclusionSchema } from "./browserCompleteness.js";
|
||||
import { javaScriptRouteInventorySchema } from "./javascript/javascriptRouteInventory.js";
|
||||
|
||||
import {
|
||||
@@ -315,6 +316,12 @@ export const webSourceMapsSchema = z
|
||||
requested: z.int().min(0),
|
||||
processed: z.int().min(0),
|
||||
items: z.array(sourceMapSchema),
|
||||
/** Capture exclusions retain counts/reasons without disallowed URL details. */
|
||||
excluded: z
|
||||
.array(
|
||||
browserExclusionSchema.extend({ section: z.literal("source_maps") }),
|
||||
)
|
||||
.optional(),
|
||||
limitation: z.string().min(1).optional(),
|
||||
})
|
||||
.superRefine((sourceMaps, context) => {
|
||||
@@ -341,7 +348,9 @@ export const webSourceMapsSchema = z
|
||||
(status) => status === "included" || status === "partial",
|
||||
).length;
|
||||
const allowedStatuses =
|
||||
sourceMaps.processed < sourceMaps.requested
|
||||
sourceMaps.processed < sourceMaps.requested ||
|
||||
sourceMaps.excluded?.some(({ count }) => count === null || count > 0) ===
|
||||
true
|
||||
? retained === 0
|
||||
? ["unavailable"]
|
||||
: ["partial"]
|
||||
|
||||
@@ -88,7 +88,7 @@ export const buildCapturedWebBundleAnalysis = (
|
||||
"script_sources",
|
||||
);
|
||||
const omittedSourceMaps =
|
||||
sourceMaps.status === "not_requested"
|
||||
sourceMaps.status === "not_requested" || sourceMaps.excluded !== undefined
|
||||
? undefined
|
||||
: captureBudgetOmission(inspection, "source_maps");
|
||||
const sourceMapIncomplete =
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { BrowserExclusion } from "./browserCompleteness.js";
|
||||
import {
|
||||
webSourceMapsSchema,
|
||||
type WebSourceMaps,
|
||||
} from "./webBundleAnalysis.js";
|
||||
|
||||
/** Preserve capture admission facts when map fetching was selected. */
|
||||
export const sourceMapsWithCaptureExclusions = (
|
||||
sourceMaps: WebSourceMaps,
|
||||
exclusions: readonly BrowserExclusion[],
|
||||
): WebSourceMaps => {
|
||||
const excluded = exclusions.flatMap((exclusion) =>
|
||||
exclusion.section === "source_maps" && exclusion.count !== 0
|
||||
? [{ ...exclusion, section: "source_maps" as const }]
|
||||
: [],
|
||||
);
|
||||
if (excluded.length === 0) return sourceMaps;
|
||||
const summary = excluded
|
||||
.map(
|
||||
({ reason, count }) =>
|
||||
`${count === null ? "An unknown number of" : String(count)} source-map declaration(s) excluded during capture: ${reason}.`,
|
||||
)
|
||||
.join(" ");
|
||||
return webSourceMapsSchema.parse({
|
||||
...sourceMaps,
|
||||
status: sourceMaps.items.some(
|
||||
({ status }) => status === "included" || status === "partial",
|
||||
)
|
||||
? "partial"
|
||||
: "unavailable",
|
||||
excluded,
|
||||
limitation: [sourceMaps.limitation, summary]
|
||||
.filter((value) => value !== undefined)
|
||||
.join(" "),
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
|
||||
import { analyzeWebBundleInputSchema } from "../../../src/domain/webBundleAnalysis.js";
|
||||
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
|
||||
import { trackBrowser } from "./cdpBrowserProvider.support.js";
|
||||
|
||||
it.each(["none", "excluded", "mixed", "redirect", "not-selected"] as const)(
|
||||
"preserves captured source-map exclusion coverage: %s",
|
||||
async (mode) => {
|
||||
const source = "export const observed = true;";
|
||||
const browser = await startFakeCdpBrowser({
|
||||
sourceMapBody: JSON.stringify({
|
||||
version: 3,
|
||||
names: [],
|
||||
sources: ["main.js"],
|
||||
sourcesContent: [source],
|
||||
mappings: "AAAA",
|
||||
}),
|
||||
...(mode === "redirect"
|
||||
? { sourceMapRedirect: "https://outside.example.test/main.js.map" }
|
||||
: {}),
|
||||
commandResult: ({ method }) =>
|
||||
method === "Debugger.getScriptSource"
|
||||
? { scriptSource: source }
|
||||
: undefined,
|
||||
commandEvents: (command, origin) => {
|
||||
if (command.method !== "Debugger.enable") return undefined;
|
||||
const script = (id: string, sourceMapURL?: string) => ({
|
||||
method: "Debugger.scriptParsed",
|
||||
...(command.sessionId === undefined
|
||||
? {}
|
||||
: { sessionId: command.sessionId }),
|
||||
params: {
|
||||
scriptId: id,
|
||||
url: `${origin}/${id}.js`,
|
||||
hash: id,
|
||||
length: Buffer.byteLength(source),
|
||||
isModule: true,
|
||||
executionContextId: 1,
|
||||
scriptLanguage: "JavaScript",
|
||||
...(sourceMapURL === undefined ? {} : { sourceMapURL }),
|
||||
},
|
||||
});
|
||||
return [
|
||||
script(
|
||||
"main",
|
||||
mode === "none"
|
||||
? undefined
|
||||
: mode === "redirect"
|
||||
? "/app.js.map"
|
||||
: "https://outside.example.test/main.js.map",
|
||||
),
|
||||
...(mode === "mixed" ? [script("second", "/app.js.map")] : []),
|
||||
];
|
||||
},
|
||||
});
|
||||
trackBrowser(browser);
|
||||
const provider = new CdpBrowserProvider();
|
||||
try {
|
||||
const result = await provider.analyzeBundle(
|
||||
analyzeWebBundleInputSchema.parse({
|
||||
cdp_endpoint: browser.endpoint,
|
||||
allowed_origins: [browser.allowedOrigin],
|
||||
target_id: "allowed-page",
|
||||
observation_ms: 0,
|
||||
fetch_source_maps: mode !== "not-selected",
|
||||
}),
|
||||
);
|
||||
if (!result.ok) throw result.error;
|
||||
const maps = result.value.observations.source_maps;
|
||||
if (mode === "none" || mode === "not-selected") {
|
||||
expect(maps).toEqual({
|
||||
status: "not_requested",
|
||||
requested: 0,
|
||||
processed: 0,
|
||||
items: [],
|
||||
});
|
||||
expect(result.value.completeness.status).toBe("complete");
|
||||
expect(result.value.unknowns).toEqual([]);
|
||||
} else {
|
||||
expect(maps.status).toBe(mode === "mixed" ? "partial" : "unavailable");
|
||||
expect(result.value.completeness.status).toBe("partial");
|
||||
expect(result.value.unknowns).toContainEqual(
|
||||
expect.objectContaining({ dimension: "source_maps" }),
|
||||
);
|
||||
if (mode === "redirect") {
|
||||
expect(maps.excluded).toBeUndefined();
|
||||
expect(maps.items[0]?.status).toBe("policy_filtered");
|
||||
} else {
|
||||
expect(maps.excluded).toEqual([
|
||||
{ section: "source_maps", reason: "disallowed_origin", count: 1 },
|
||||
]);
|
||||
expect(maps.requested).toBe(mode === "mixed" ? 1 : 0);
|
||||
expect(maps.processed).toBe(mode === "mixed" ? 1 : 0);
|
||||
if (mode === "mixed") expect(maps.items[0]?.status).toBe("included");
|
||||
}
|
||||
}
|
||||
expect(JSON.stringify(result.value)).not.toContain(
|
||||
"outside.example.test",
|
||||
);
|
||||
expect(
|
||||
browser.httpRequests.filter(({ url }) => url.startsWith("/app.js.map")),
|
||||
).toHaveLength(mode === "mixed" || mode === "redirect" ? 1 : 0);
|
||||
} finally {
|
||||
await provider.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user