From c5073012739d1c21013be95eddd30f3afddf5879 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Wed, 17 Jun 2026 10:42:01 +0200 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20collect=20Network=20Ef?= =?UTF-8?q?ficiency=20Guardrails=20document-policy-violation=20reports=20a?= =?UTF-8?q?s=20errors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/report/browser.types.ts | 17 ++- .../domain/report/reportObservable.spec.ts | 39 ++++++- .../src/domain/report/reportObservable.ts | 59 +++++++++-- .../test/emulate/mockReportingObserver.ts | 25 ++++- .../src/domain/error/trackReportError.ts | 6 +- test/e2e/lib/framework/serverApps/mock.ts | 13 +++ .../networkEfficiencyGuardrails.scenario.ts | 100 ++++++++++++++++++ 7 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts diff --git a/packages/browser-core/src/domain/report/browser.types.ts b/packages/browser-core/src/domain/report/browser.types.ts index 1434b91b9b..d1637dc429 100644 --- a/packages/browser-core/src/domain/report/browser.types.ts +++ b/packages/browser-core/src/domain/report/browser.types.ts @@ -1,9 +1,9 @@ -export type ReportType = DeprecationReport['type'] | InterventionReport['type'] +export type ReportType = DeprecationReport['type'] | InterventionReport['type'] | DocumentPolicyViolationReport['type'] interface Report { type: ReportType url: string - body: DeprecationReportBody | InterventionReportBody + body: DeprecationReportBody | InterventionReportBody | DocumentPolicyViolationReportBody toJSON(): any } @@ -36,3 +36,16 @@ export interface InterventionReportBody extends ReportBody { columnNumber: number | null sourceFile: string | null } + +export interface DocumentPolicyViolationReport extends Report { + type: 'document-policy-violation' + body: DocumentPolicyViolationReportBody +} +export interface DocumentPolicyViolationReportBody extends ReportBody { + featureId: string + message: string + disposition: 'enforce' | 'report' + lineNumber: null + columnNumber: null + sourceFile: string | null +} diff --git a/packages/browser-core/src/domain/report/reportObservable.spec.ts b/packages/browser-core/src/domain/report/reportObservable.spec.ts index 95da8f37d0..2c42c7b725 100644 --- a/packages/browser-core/src/domain/report/reportObservable.spec.ts +++ b/packages/browser-core/src/domain/report/reportObservable.spec.ts @@ -1,5 +1,10 @@ import type { MockCspEventListener, MockReportingObserver } from '../../../test' -import { mockReportingObserver, mockCspEventListener, FAKE_CSP_VIOLATION_EVENT } from '../../../test' +import { + mockReportingObserver, + mockCspEventListener, + FAKE_CSP_VIOLATION_EVENT, + FAKE_DOCUMENT_POLICY_VIOLATION_REPORT, +} from '../../../test' import type { Subscription } from '../../tools/observable' import { ErrorHandling, ErrorSource } from '../error/error.types' import type { RawReportError } from './reportObservable' @@ -74,4 +79,36 @@ describe('report observable', () => { expect(notifyReport).not.toHaveBeenCalled() }) + + it(`should notify ${RawReportType.networkEfficiencyGuardrails} reports`, () => { + consoleSubscription = initReportObservable([RawReportType.networkEfficiencyGuardrails]).subscribe(notifyReport) + reportingObserver.raiseReport('document-policy-violation') + + expect(notifyReport).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + message: 'document-policy-violation: Document policy violation: resource compression is required.', + type: 'network-efficiency-guardrails', + csp: { disposition: 'report' }, + }) + ) + }) + + it(`should compute stack for ${RawReportType.networkEfficiencyGuardrails}`, () => { + consoleSubscription = initReportObservable([RawReportType.networkEfficiencyGuardrails]).subscribe(notifyReport) + reportingObserver.raiseReport('document-policy-violation') + + const [report] = notifyReport.calls.mostRecent().args + + expect(report.stack).toEqual(`network-efficiency-guardrails: Document policy violation: resource compression is required. + at @ https://foo.bar/large-uncompressed.js`) + }) + + it(`should not notify document-policy-violation reports with a featureId other than ${RawReportType.networkEfficiencyGuardrails}`, () => { + consoleSubscription = initReportObservable([RawReportType.networkEfficiencyGuardrails]).subscribe(notifyReport) + reportingObserver.raiseReport('document-policy-violation', { + body: { ...FAKE_DOCUMENT_POLICY_VIOLATION_REPORT.body, featureId: 'some-other-policy' }, + }) + + expect(notifyReport).not.toHaveBeenCalled() + }) }) diff --git a/packages/browser-core/src/domain/report/reportObservable.ts b/packages/browser-core/src/domain/report/reportObservable.ts index 21a36fd735..7747b238e8 100644 --- a/packages/browser-core/src/domain/report/reportObservable.ts +++ b/packages/browser-core/src/domain/report/reportObservable.ts @@ -6,18 +6,19 @@ import { addEventListener, DOM_EVENT, isEventSupported } from '../../browser/add import { safeTruncate } from '../../tools/utils/stringUtils' import type { RawError } from '../error/error.types' import { ErrorHandling, ErrorSource } from '../error/error.types' -import type { ReportType, InterventionReport, DeprecationReport } from './browser.types' +import type { ReportType, InterventionReport, DeprecationReport, DocumentPolicyViolationReport } from './browser.types' export const RawReportType = { intervention: 'intervention', deprecation: 'deprecation', cspViolation: 'csp_violation', + networkEfficiencyGuardrails: 'network-efficiency-guardrails', } as const export type RawReportType = (typeof RawReportType)[keyof typeof RawReportType] export type RawReportError = RawError & { - originalError: SecurityPolicyViolationEvent | DeprecationReport | InterventionReport + originalError: SecurityPolicyViolationEvent | DeprecationReport | InterventionReport | DocumentPolicyViolationReport } export function initReportObservable(apis: RawReportType[]) { @@ -27,7 +28,7 @@ export function initReportObservable(apis: RawReportType[]) { observables.push(createCspViolationReportObservable()) } - const reportTypes = apis.filter((api: RawReportType): api is ReportType => api !== RawReportType.cspViolation) + const reportTypes = buildReportObserverTypes(apis) if (reportTypes.length) { observables.push(createReportObservable(reportTypes)) } @@ -35,14 +36,47 @@ export function initReportObservable(apis: RawReportType[]) { return mergeObservables(...observables) } +/** + * Maps internal RawReportType values to the browser ReportingObserver type strings. + * `network-efficiency-guardrails` is exposed via `document-policy-violation` reports, + * filtered by `body.featureId === 'network-efficiency-guardrails'`. + */ +function buildReportObserverTypes(apis: RawReportType[]): ReportType[] { + const types = new Set() + for (const api of apis) { + if (api === RawReportType.cspViolation) { + continue + } + if (api === RawReportType.networkEfficiencyGuardrails) { + types.add('document-policy-violation') + } else { + types.add(api as ReportType) + } + } + return Array.from(types) +} + function createReportObservable(reportTypes: ReportType[]) { return new Observable((observable) => { if (!window.ReportingObserver) { return } - const handleReports = monitor((reports: Array, _: ReportingObserver) => - reports.forEach((report) => observable.notify(buildRawReportErrorFromReport(report))) + const handleReports = monitor( + (reports: Array, _: ReportingObserver) => + reports.forEach((report) => { + // document-policy-violation reports are only subscribed to when + // network-efficiency-guardrails is requested. Skip any document policy violation + // whose featureId does not match network-efficiency-guardrails. + if ( + report.type === 'document-policy-violation' && + (report.body as DocumentPolicyViolationReport['body']).featureId !== + RawReportType.networkEfficiencyGuardrails + ) { + return + } + observable.notify(buildRawReportErrorFromReport(report)) + }) ) as ReportingObserverCallback const observer = new window.ReportingObserver(handleReports, { @@ -72,9 +106,22 @@ function createCspViolationReportObservable() { }) } -function buildRawReportErrorFromReport(report: DeprecationReport | InterventionReport): RawReportError { +function buildRawReportErrorFromReport( + report: DeprecationReport | InterventionReport | DocumentPolicyViolationReport +): RawReportError { const { type, body } = report + if (type === 'document-policy-violation') { + const { featureId, message, disposition, sourceFile } = body as DocumentPolicyViolationReport['body'] + return buildRawReportError({ + type: featureId, + message: `${type}: ${message}`, + originalError: report, + csp: { disposition }, + stack: buildStack(featureId, message, sourceFile, null, null), + }) + } + return buildRawReportError({ type: body.id, message: `${type}: ${body.message}`, diff --git a/packages/browser-core/test/emulate/mockReportingObserver.ts b/packages/browser-core/test/emulate/mockReportingObserver.ts index a278ece395..3fdcd6379e 100644 --- a/packages/browser-core/test/emulate/mockReportingObserver.ts +++ b/packages/browser-core/test/emulate/mockReportingObserver.ts @@ -1,4 +1,4 @@ -import type { InterventionReport, ReportType } from '../../src/domain/report/browser.types' +import type { DocumentPolicyViolationReport, InterventionReport, ReportType } from '../../src/domain/report/browser.types' import { noop } from '../../src/tools/utils/functionUtils' import { registerCleanupTask } from '../registerCleanupTask' import { createNewEvent } from './createNewEvent' @@ -39,9 +39,13 @@ export function mockReportingObserver() { }) return { - raiseReport(type: ReportType) { + raiseReport(type: ReportType, overrides?: Partial) { if (callbacks[type]) { - callbacks[type].forEach((callback) => callback([{ ...FAKE_REPORT, type }], reportingObserver)) + const report = + type === 'document-policy-violation' + ? { ...FAKE_DOCUMENT_POLICY_VIOLATION_REPORT, ...overrides } + : { ...FAKE_REPORT, type } + callbacks[type].forEach((callback) => callback([report], reportingObserver)) } }, } @@ -85,6 +89,21 @@ export const FAKE_CSP_VIOLATION_EVENT = createNewEvent('securitypolicyviolation' violatedDirective: 'worker-src', }) +export const FAKE_DOCUMENT_POLICY_VIOLATION_REPORT: DocumentPolicyViolationReport = { + type: 'document-policy-violation', + url: 'http://foo.bar', + body: { + featureId: 'network-efficiency-guardrails', + message: 'Document policy violation: resource compression is required.', + disposition: 'report', + lineNumber: null, + columnNumber: null, + sourceFile: 'https://foo.bar/large-uncompressed.js', + toJSON: noop, + }, + toJSON: noop, +} + export const FAKE_REPORT: InterventionReport = { type: 'intervention', url: 'http://foo.bar', diff --git a/packages/browser-rum-core/src/domain/error/trackReportError.ts b/packages/browser-rum-core/src/domain/error/trackReportError.ts index afa136e7c1..cf85da12b4 100644 --- a/packages/browser-rum-core/src/domain/error/trackReportError.ts +++ b/packages/browser-rum-core/src/domain/error/trackReportError.ts @@ -1,7 +1,11 @@ import type { Observable, RawError } from '@datadog/browser-core' import { initReportObservable, RawReportType } from '@datadog/browser-core' export function trackReportError(errorObservable: Observable) { - const subscription = initReportObservable([RawReportType.cspViolation, RawReportType.intervention]).subscribe( + const subscription = initReportObservable([ + RawReportType.cspViolation, + RawReportType.intervention, + RawReportType.networkEfficiencyGuardrails, + ]).subscribe( (rawError) => errorObservable.notify(rawError) ) diff --git a/test/e2e/lib/framework/serverApps/mock.ts b/test/e2e/lib/framework/serverApps/mock.ts index 148f813600..63bacea6ef 100644 --- a/test/e2e/lib/framework/serverApps/mock.ts +++ b/test/e2e/lib/framework/serverApps/mock.ts @@ -181,10 +181,23 @@ export function createMockServerApp(servers: Servers, setup: string, setupOption if (req.query['js-profiling'] === 'true') { res.header('Document-Policy', 'js-profiling') } + if (req.query['network-efficiency-guardrails'] === 'true') { + res.header('Document-Policy', 'network-efficiency-guardrails') + } res.send(setup) res.end() }) + // Serves an uncompressed JavaScript file large enough to trigger a network-efficiency-guardrails + // policy violation (text resources must be HTTP-compressed). + app.get('/uncompressed-script.js', (_req, res) => { + res.removeHeader('Content-Encoding') + res.header('Content-Type', 'application/javascript') + // Explicitly disable compression for this endpoint so the browser detects a violation + res.header('Cache-Control', 'no-store') + res.send(`// uncompressed script\n${'// padding\n'.repeat(500)}`) + }) + app.get('/no-blob-worker-csp', (_req, res) => { res.header( 'Content-Security-Policy', diff --git a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts new file mode 100644 index 0000000000..cb6972f3ee --- /dev/null +++ b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts @@ -0,0 +1,100 @@ +import type { RumErrorEvent } from '@datadog/browser-rum-core' +import { test, expect } from '@playwright/test' +import { createTest } from '../lib/framework' + +// Network Efficiency Guardrails is a Document Policy feature currently only available in Edge 146+ +// and in Chromium behind the "Experimental Web Platform features" flag. +// We run these tests only on Chromium. + +// Enable the Experimental Web Platform features flag required for Network Efficiency Guardrails. +// Must be top-level: launchOptions forces a new worker and cannot be inside describe(). +test.use({ + launchOptions: { + args: ['--enable-experimental-web-platform-features'], + }, +}) + +test.describe('network efficiency guardrails', () => { + test.beforeEach(({ browserName }) => { + test.skip(browserName !== 'chromium', 'Network Efficiency Guardrails is only available in Chromium-based browsers') + }) + + test.describe('RUM', () => { + createTest('should collect network-efficiency-guardrails violations as RUM errors') + .withRum() + .withBasePath('/?network-efficiency-guardrails=true') + .run(async ({ page, intakeRegistry, flushEvents, withBrowserLogs }) => { + // Trigger a violation after the SDK has initialized: fetch an uncompressed JS resource. + // The Document-Policy header on the page opts into monitoring, and the lack of + // Content-Encoding on this endpoint triggers a "resource compression" violation. + await page.evaluate(() => fetch('/uncompressed-script.js')) + + await flushEvents() + + const guardrailErrors = intakeRegistry.rumErrorEvents.filter((event) => + event.error.message.startsWith('document-policy-violation:') + ) + + // The SDK bundles themselves (served uncompressed in dev) also trigger violations, + // so we may receive more than one. Assert we got at least one for our resource. + expect(guardrailErrors.length).toBeGreaterThanOrEqual(1) + + const error = guardrailErrors[0].error as RumErrorEvent['error'] + expect(error.source).toBe('report') + expect(error.handling).toBe('unhandled') + expect(error.csp?.disposition).toMatch(/enforce|report/) + + // The browser logs a console error for each violation — acknowledge them so the + // framework teardown check doesn't fail. + withBrowserLogs((logs) => { + const errors = logs.filter((log) => log.level === 'error') + expect(errors.length).toBeGreaterThanOrEqual(1) + expect(errors[0].message).toContain('Document policy violation: resource compression is required') + }) + }) + }) + + test.describe('Logs', () => { + createTest('should forward network-efficiency-guardrails violations via forwardReports') + .withLogs({ forwardReports: ['network-efficiency-guardrails'] }) + .withBasePath('/?network-efficiency-guardrails=true') + .run(async ({ page, intakeRegistry, flushEvents, withBrowserLogs }) => { + await page.evaluate(() => fetch('/uncompressed-script.js')) + + await flushEvents() + + const guardrailLogs = intakeRegistry.logsEvents.filter((event) => + event.message.startsWith('document-policy-violation:') + ) + + expect(guardrailLogs).toHaveLength(1) + expect(guardrailLogs[0].origin).toBe('report') + expect(guardrailLogs[0].status).toBe('error') + + withBrowserLogs((logs) => { + expect(logs.filter((log) => log.level === 'error')).toHaveLength(1) + expect(logs[0].message).toContain('Document policy violation: resource compression is required') + }) + }) + + createTest('should not forward network-efficiency-guardrails violations when not in forwardReports') + .withLogs({ forwardReports: [] }) + .withBasePath('/?network-efficiency-guardrails=true') + .run(async ({ page, intakeRegistry, flushEvents, withBrowserLogs }) => { + await page.evaluate(() => fetch('/uncompressed-script.js')) + + await flushEvents() + + const guardrailLogs = intakeRegistry.logsEvents.filter((event) => + event.message.startsWith('document-policy-violation:') + ) + + expect(guardrailLogs).toHaveLength(0) + + withBrowserLogs((logs) => { + expect(logs.filter((log) => log.level === 'error')).toHaveLength(1) + expect(logs[0].message).toContain('Document policy violation: resource compression is required') + }) + }) + }) +}) From e2a8ea4d59324ca6dc2658edd8e3ca251dc5b599 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Wed, 17 Jun 2026 11:05:36 +0200 Subject: [PATCH 2/6] =?UTF-8?q?=F0=9F=8E=A8=20fix=20lint=20and=20format=20?= =?UTF-8?q?issues?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/domain/report/reportObservable.spec.ts | 3 ++- .../src/domain/report/reportObservable.ts | 14 ++++++-------- .../test/emulate/mockReportingObserver.ts | 6 +++++- .../src/domain/error/trackReportError.ts | 4 +--- .../networkEfficiencyGuardrails.scenario.ts | 3 +-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/browser-core/src/domain/report/reportObservable.spec.ts b/packages/browser-core/src/domain/report/reportObservable.spec.ts index 2c42c7b725..58cada8827 100644 --- a/packages/browser-core/src/domain/report/reportObservable.spec.ts +++ b/packages/browser-core/src/domain/report/reportObservable.spec.ts @@ -99,7 +99,8 @@ describe('report observable', () => { const [report] = notifyReport.calls.mostRecent().args - expect(report.stack).toEqual(`network-efficiency-guardrails: Document policy violation: resource compression is required. + expect(report.stack) + .toEqual(`network-efficiency-guardrails: Document policy violation: resource compression is required. at @ https://foo.bar/large-uncompressed.js`) }) diff --git a/packages/browser-core/src/domain/report/reportObservable.ts b/packages/browser-core/src/domain/report/reportObservable.ts index 7747b238e8..f8128601cc 100644 --- a/packages/browser-core/src/domain/report/reportObservable.ts +++ b/packages/browser-core/src/domain/report/reportObservable.ts @@ -50,7 +50,7 @@ function buildReportObserverTypes(apis: RawReportType[]): ReportType[] { if (api === RawReportType.networkEfficiencyGuardrails) { types.add('document-policy-violation') } else { - types.add(api as ReportType) + types.add(api) } } return Array.from(types) @@ -70,8 +70,7 @@ function createReportObservable(reportTypes: ReportType[]) { // whose featureId does not match network-efficiency-guardrails. if ( report.type === 'document-policy-violation' && - (report.body as DocumentPolicyViolationReport['body']).featureId !== - RawReportType.networkEfficiencyGuardrails + report.body.featureId !== RawReportType.networkEfficiencyGuardrails ) { return } @@ -109,19 +108,18 @@ function createCspViolationReportObservable() { function buildRawReportErrorFromReport( report: DeprecationReport | InterventionReport | DocumentPolicyViolationReport ): RawReportError { - const { type, body } = report - - if (type === 'document-policy-violation') { - const { featureId, message, disposition, sourceFile } = body as DocumentPolicyViolationReport['body'] + if (report.type === 'document-policy-violation') { + const { featureId, message, disposition, sourceFile } = report.body return buildRawReportError({ type: featureId, - message: `${type}: ${message}`, + message: `${report.type}: ${message}`, originalError: report, csp: { disposition }, stack: buildStack(featureId, message, sourceFile, null, null), }) } + const { type, body } = report return buildRawReportError({ type: body.id, message: `${type}: ${body.message}`, diff --git a/packages/browser-core/test/emulate/mockReportingObserver.ts b/packages/browser-core/test/emulate/mockReportingObserver.ts index 3fdcd6379e..95699933ed 100644 --- a/packages/browser-core/test/emulate/mockReportingObserver.ts +++ b/packages/browser-core/test/emulate/mockReportingObserver.ts @@ -1,4 +1,8 @@ -import type { DocumentPolicyViolationReport, InterventionReport, ReportType } from '../../src/domain/report/browser.types' +import type { + DocumentPolicyViolationReport, + InterventionReport, + ReportType, +} from '../../src/domain/report/browser.types' import { noop } from '../../src/tools/utils/functionUtils' import { registerCleanupTask } from '../registerCleanupTask' import { createNewEvent } from './createNewEvent' diff --git a/packages/browser-rum-core/src/domain/error/trackReportError.ts b/packages/browser-rum-core/src/domain/error/trackReportError.ts index cf85da12b4..4a596e36b2 100644 --- a/packages/browser-rum-core/src/domain/error/trackReportError.ts +++ b/packages/browser-rum-core/src/domain/error/trackReportError.ts @@ -5,9 +5,7 @@ export function trackReportError(errorObservable: Observable) { RawReportType.cspViolation, RawReportType.intervention, RawReportType.networkEfficiencyGuardrails, - ]).subscribe( - (rawError) => errorObservable.notify(rawError) - ) + ]).subscribe((rawError) => errorObservable.notify(rawError)) return { stop: () => { diff --git a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts index cb6972f3ee..2f6fbf1fa7 100644 --- a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts +++ b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts @@ -1,4 +1,3 @@ -import type { RumErrorEvent } from '@datadog/browser-rum-core' import { test, expect } from '@playwright/test' import { createTest } from '../lib/framework' @@ -39,7 +38,7 @@ test.describe('network efficiency guardrails', () => { // so we may receive more than one. Assert we got at least one for our resource. expect(guardrailErrors.length).toBeGreaterThanOrEqual(1) - const error = guardrailErrors[0].error as RumErrorEvent['error'] + const error = guardrailErrors[0].error expect(error.source).toBe('report') expect(error.handling).toBe('unhandled') expect(error.csp?.disposition).toMatch(/enforce|report/) From 950fb9c34c69157f7b412b9c7e6d46759f029a5b Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Wed, 17 Jun 2026 11:40:18 +0200 Subject: [PATCH 3/6] =?UTF-8?q?=F0=9F=94=A7=20move=20--enable-experimental?= =?UTF-8?q?-web-platform-features=20to=20chromium=20project=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/playwright.config.ts | 8 +++++++- test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts | 8 -------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index 9ffd74279e..f6ce6666ee 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -168,10 +168,16 @@ function getProjects() { } function project(name: string, device: string) { + const isChromium = name.startsWith('chromium') return { name, metadata: { sessionName: device, name } satisfies BrowserConfiguration, - use: devices[device], + use: { + ...devices[device], + // Required for experimental APIs (e.g. Network Efficiency Guardrails) that are + // gated behind this flag in Chromium. Ignored for non-Chromium browsers. + ...(isChromium ? { launchOptions: { args: ['--enable-experimental-web-platform-features'] } } : {}), + }, } } diff --git a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts index 2f6fbf1fa7..d5579c2e5a 100644 --- a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts +++ b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts @@ -5,14 +5,6 @@ import { createTest } from '../lib/framework' // and in Chromium behind the "Experimental Web Platform features" flag. // We run these tests only on Chromium. -// Enable the Experimental Web Platform features flag required for Network Efficiency Guardrails. -// Must be top-level: launchOptions forces a new worker and cannot be inside describe(). -test.use({ - launchOptions: { - args: ['--enable-experimental-web-platform-features'], - }, -}) - test.describe('network efficiency guardrails', () => { test.beforeEach(({ browserName }) => { test.skip(browserName !== 'chromium', 'Network Efficiency Guardrails is only available in Chromium-based browsers') From 9df763e6440617240b544be5352bbcbc9988f47f Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Wed, 17 Jun 2026 14:28:33 +0200 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=91=8C=20skip=20NEG=20tests=20on=20no?= =?UTF-8?q?n-Chromium=20and=20pinned=20Chromium=20<=20146?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/e2e/playwright.config.ts | 6 +++--- .../e2e/scenario/networkEfficiencyGuardrails.scenario.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/e2e/playwright.config.ts b/test/e2e/playwright.config.ts index f6ce6666ee..294d2e81ad 100644 --- a/test/e2e/playwright.config.ts +++ b/test/e2e/playwright.config.ts @@ -168,14 +168,14 @@ function getProjects() { } function project(name: string, device: string) { - const isChromium = name.startsWith('chromium') + const isChromium = name === 'chromium' return { name, metadata: { sessionName: device, name } satisfies BrowserConfiguration, use: { ...devices[device], - // Required for experimental APIs (e.g. Network Efficiency Guardrails) that are - // gated behind this flag in Chromium. Ignored for non-Chromium browsers. + // Required for experimental APIs (e.g. Network Efficiency Guardrails). + // Only passed to current Chromium — pinned browsers use pinnedProject() and may not support it. ...(isChromium ? { launchOptions: { args: ['--enable-experimental-web-platform-features'] } } : {}), }, } diff --git a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts index d5579c2e5a..74c34f70e5 100644 --- a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts +++ b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts @@ -1,5 +1,6 @@ import { test, expect } from '@playwright/test' import { createTest } from '../lib/framework' +import type { BrowserConfiguration } from '../../browsers.conf' // Network Efficiency Guardrails is a Document Policy feature currently only available in Edge 146+ // and in Chromium behind the "Experimental Web Platform features" flag. @@ -7,7 +8,13 @@ import { createTest } from '../lib/framework' test.describe('network efficiency guardrails', () => { test.beforeEach(({ browserName }) => { - test.skip(browserName !== 'chromium', 'Network Efficiency Guardrails is only available in Chromium-based browsers') + const { version } = test.info().project.metadata as BrowserConfiguration + // Network Efficiency Guardrails requires Chromium 146+. Pinned projects set an explicit + // version; current (unversioned) Chromium is always new enough. + test.skip( + browserName !== 'chromium' || (version !== undefined && Number(version) < 146), + 'Network Efficiency Guardrails requires Chromium 146+' + ) }) test.describe('RUM', () => { From 53d0071d5c8594917dccd3975f2210f4d77e2b95 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Wed, 24 Jun 2026 14:11:35 +0200 Subject: [PATCH 5/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20generalise=20document-?= =?UTF-8?q?policy-violation=20support=20to=20all=20featureIds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the NEG-specific `networkEfficiencyGuardrails` RawReportType with a generic `documentPolicyViolation: 'document-policy-violation'` entry that mirrors how `intervention` and `deprecation` are modelled. - Remove the `buildReportObserverTypes()` mapping layer (it existed solely to translate 'network-efficiency-guardrails' → 'document-policy-violation') - Remove the featureId filter in the ReportingObserver callback that was silently dropping non-NEG document-policy-violation reports - RUM now collects all document-policy-violation reports automatically; Logs users opt in with `forwardReports: ['document-policy-violation']` - Future document policy features (e.g. js-profiling-mode, sync-xhr) are automatically captured without any SDK change --- .../domain/report/reportObservable.spec.ts | 18 +++++---- .../src/domain/report/reportObservable.ts | 37 ++----------------- .../src/domain/error/trackReportError.ts | 2 +- .../networkEfficiencyGuardrails.scenario.ts | 4 +- 4 files changed, 17 insertions(+), 44 deletions(-) diff --git a/packages/browser-core/src/domain/report/reportObservable.spec.ts b/packages/browser-core/src/domain/report/reportObservable.spec.ts index 58cada8827..2760e6cdae 100644 --- a/packages/browser-core/src/domain/report/reportObservable.spec.ts +++ b/packages/browser-core/src/domain/report/reportObservable.spec.ts @@ -80,8 +80,8 @@ describe('report observable', () => { expect(notifyReport).not.toHaveBeenCalled() }) - it(`should notify ${RawReportType.networkEfficiencyGuardrails} reports`, () => { - consoleSubscription = initReportObservable([RawReportType.networkEfficiencyGuardrails]).subscribe(notifyReport) + it(`should notify ${RawReportType.documentPolicyViolation} reports`, () => { + consoleSubscription = initReportObservable([RawReportType.documentPolicyViolation]).subscribe(notifyReport) reportingObserver.raiseReport('document-policy-violation') expect(notifyReport).toHaveBeenCalledOnceWith( @@ -93,8 +93,8 @@ describe('report observable', () => { ) }) - it(`should compute stack for ${RawReportType.networkEfficiencyGuardrails}`, () => { - consoleSubscription = initReportObservable([RawReportType.networkEfficiencyGuardrails]).subscribe(notifyReport) + it(`should compute stack for ${RawReportType.documentPolicyViolation}`, () => { + consoleSubscription = initReportObservable([RawReportType.documentPolicyViolation]).subscribe(notifyReport) reportingObserver.raiseReport('document-policy-violation') const [report] = notifyReport.calls.mostRecent().args @@ -104,12 +104,16 @@ describe('report observable', () => { at @ https://foo.bar/large-uncompressed.js`) }) - it(`should not notify document-policy-violation reports with a featureId other than ${RawReportType.networkEfficiencyGuardrails}`, () => { - consoleSubscription = initReportObservable([RawReportType.networkEfficiencyGuardrails]).subscribe(notifyReport) + it(`should notify ${RawReportType.documentPolicyViolation} reports regardless of featureId`, () => { + consoleSubscription = initReportObservable([RawReportType.documentPolicyViolation]).subscribe(notifyReport) reportingObserver.raiseReport('document-policy-violation', { body: { ...FAKE_DOCUMENT_POLICY_VIOLATION_REPORT.body, featureId: 'some-other-policy' }, }) - expect(notifyReport).not.toHaveBeenCalled() + expect(notifyReport).toHaveBeenCalledOnceWith( + jasmine.objectContaining({ + type: 'some-other-policy', + }) + ) }) }) diff --git a/packages/browser-core/src/domain/report/reportObservable.ts b/packages/browser-core/src/domain/report/reportObservable.ts index f8128601cc..e9ede89df6 100644 --- a/packages/browser-core/src/domain/report/reportObservable.ts +++ b/packages/browser-core/src/domain/report/reportObservable.ts @@ -12,7 +12,7 @@ export const RawReportType = { intervention: 'intervention', deprecation: 'deprecation', cspViolation: 'csp_violation', - networkEfficiencyGuardrails: 'network-efficiency-guardrails', + documentPolicyViolation: 'document-policy-violation', } as const export type RawReportType = (typeof RawReportType)[keyof typeof RawReportType] @@ -28,7 +28,7 @@ export function initReportObservable(apis: RawReportType[]) { observables.push(createCspViolationReportObservable()) } - const reportTypes = buildReportObserverTypes(apis) + const reportTypes = apis.filter((api): api is ReportType => api !== RawReportType.cspViolation) if (reportTypes.length) { observables.push(createReportObservable(reportTypes)) } @@ -36,26 +36,6 @@ export function initReportObservable(apis: RawReportType[]) { return mergeObservables(...observables) } -/** - * Maps internal RawReportType values to the browser ReportingObserver type strings. - * `network-efficiency-guardrails` is exposed via `document-policy-violation` reports, - * filtered by `body.featureId === 'network-efficiency-guardrails'`. - */ -function buildReportObserverTypes(apis: RawReportType[]): ReportType[] { - const types = new Set() - for (const api of apis) { - if (api === RawReportType.cspViolation) { - continue - } - if (api === RawReportType.networkEfficiencyGuardrails) { - types.add('document-policy-violation') - } else { - types.add(api) - } - } - return Array.from(types) -} - function createReportObservable(reportTypes: ReportType[]) { return new Observable((observable) => { if (!window.ReportingObserver) { @@ -64,18 +44,7 @@ function createReportObservable(reportTypes: ReportType[]) { const handleReports = monitor( (reports: Array, _: ReportingObserver) => - reports.forEach((report) => { - // document-policy-violation reports are only subscribed to when - // network-efficiency-guardrails is requested. Skip any document policy violation - // whose featureId does not match network-efficiency-guardrails. - if ( - report.type === 'document-policy-violation' && - report.body.featureId !== RawReportType.networkEfficiencyGuardrails - ) { - return - } - observable.notify(buildRawReportErrorFromReport(report)) - }) + reports.forEach((report) => observable.notify(buildRawReportErrorFromReport(report))) ) as ReportingObserverCallback const observer = new window.ReportingObserver(handleReports, { diff --git a/packages/browser-rum-core/src/domain/error/trackReportError.ts b/packages/browser-rum-core/src/domain/error/trackReportError.ts index 4a596e36b2..cf76b23f2d 100644 --- a/packages/browser-rum-core/src/domain/error/trackReportError.ts +++ b/packages/browser-rum-core/src/domain/error/trackReportError.ts @@ -4,7 +4,7 @@ export function trackReportError(errorObservable: Observable) { const subscription = initReportObservable([ RawReportType.cspViolation, RawReportType.intervention, - RawReportType.networkEfficiencyGuardrails, + RawReportType.documentPolicyViolation, ]).subscribe((rawError) => errorObservable.notify(rawError)) return { diff --git a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts index 74c34f70e5..db9efb4250 100644 --- a/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts +++ b/test/e2e/scenario/networkEfficiencyGuardrails.scenario.ts @@ -54,7 +54,7 @@ test.describe('network efficiency guardrails', () => { test.describe('Logs', () => { createTest('should forward network-efficiency-guardrails violations via forwardReports') - .withLogs({ forwardReports: ['network-efficiency-guardrails'] }) + .withLogs({ forwardReports: ['document-policy-violation'] }) .withBasePath('/?network-efficiency-guardrails=true') .run(async ({ page, intakeRegistry, flushEvents, withBrowserLogs }) => { await page.evaluate(() => fetch('/uncompressed-script.js')) @@ -75,7 +75,7 @@ test.describe('network efficiency guardrails', () => { }) }) - createTest('should not forward network-efficiency-guardrails violations when not in forwardReports') + createTest('should not forward network-efficiency-guardrails violations when not opted in') .withLogs({ forwardReports: [] }) .withBasePath('/?network-efficiency-guardrails=true') .run(async ({ page, intakeRegistry, flushEvents, withBrowserLogs }) => { From c61b11a8e3c2997da7a4290bb3f28da4740dedc3 Mon Sep 17 00:00:00 2001 From: Thomas Bertet Date: Wed, 24 Jun 2026 15:48:11 +0200 Subject: [PATCH 6/6] =?UTF-8?q?=E2=9C=A8=20add=20featureId=20to=20RawError?= =?UTF-8?q?=20for=20document-policy-violation=20reports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set `error.type` to `'document-policy-violation'` (the report type, matching the DOM/spec model) and expose `featureId` (e.g. `'network-efficiency-guardrails'`) as a dedicated top-level field on the error, following the spec's naming for DocumentPolicyViolationReportBody.featureId. - RawError gets a new optional `featureId` field - Forwarded as `feature_id` in RUM (via combine(), pending rum-events-format schema update) - Forwarded as `feature_id` in Logs (rawLogsEvent Error interface) - intervention/deprecation reports are unchanged (they use body.id as type) --- .../src/domain/error/error.types.ts | 2 ++ .../domain/report/reportObservable.spec.ts | 6 ++-- .../src/domain/report/reportObservable.ts | 3 +- .../createErrorFieldFromRawError.spec.ts | 2 ++ .../domain/createErrorFieldFromRawError.ts | 1 + .../browser-logs/src/rawLogsEvent.types.ts | 1 + .../src/domain/error/errorCollection.ts | 32 +++++++++++-------- 7 files changed, 30 insertions(+), 17 deletions(-) diff --git a/packages/browser-core/src/domain/error/error.types.ts b/packages/browser-core/src/domain/error/error.types.ts index 90f036b934..6b68c01515 100644 --- a/packages/browser-core/src/domain/error/error.types.ts +++ b/packages/browser-core/src/domain/error/error.types.ts @@ -23,12 +23,14 @@ export interface RawErrorCause { export interface Csp { disposition: 'enforce' | 'report' + featureId?: string } export interface RawError { startClocks: ClocksState message: string type?: string + featureId?: string stack?: string source: ErrorSource originalError?: unknown diff --git a/packages/browser-core/src/domain/report/reportObservable.spec.ts b/packages/browser-core/src/domain/report/reportObservable.spec.ts index 2760e6cdae..07f7b2f0a3 100644 --- a/packages/browser-core/src/domain/report/reportObservable.spec.ts +++ b/packages/browser-core/src/domain/report/reportObservable.spec.ts @@ -87,7 +87,8 @@ describe('report observable', () => { expect(notifyReport).toHaveBeenCalledOnceWith( jasmine.objectContaining({ message: 'document-policy-violation: Document policy violation: resource compression is required.', - type: 'network-efficiency-guardrails', + type: 'document-policy-violation', + featureId: 'network-efficiency-guardrails', csp: { disposition: 'report' }, }) ) @@ -112,7 +113,8 @@ describe('report observable', () => { expect(notifyReport).toHaveBeenCalledOnceWith( jasmine.objectContaining({ - type: 'some-other-policy', + type: 'document-policy-violation', + featureId: 'some-other-policy', }) ) }) diff --git a/packages/browser-core/src/domain/report/reportObservable.ts b/packages/browser-core/src/domain/report/reportObservable.ts index e9ede89df6..a31d64c4c0 100644 --- a/packages/browser-core/src/domain/report/reportObservable.ts +++ b/packages/browser-core/src/domain/report/reportObservable.ts @@ -80,7 +80,8 @@ function buildRawReportErrorFromReport( if (report.type === 'document-policy-violation') { const { featureId, message, disposition, sourceFile } = report.body return buildRawReportError({ - type: featureId, + type: report.type, + featureId, message: `${report.type}: ${message}`, originalError: report, csp: { disposition }, diff --git a/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts b/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts index 1ce6d3e5d3..802b19bf04 100644 --- a/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts +++ b/packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts @@ -14,6 +14,7 @@ describe('createErrorFieldFromRawError', () => { componentStack: 'at Flex', originalError: new Error('baz'), type: 'qux', + featureId: 'quux-feature', message: 'quux', stack: 'quuz', causes: [ @@ -43,6 +44,7 @@ describe('createErrorFieldFromRawError', () => { expect(createErrorFieldFromRawError(exhaustiveRawError)).toEqual({ message: undefined, kind: 'qux', + feature_id: 'quux-feature', stack: 'quuz', causes: [ { diff --git a/packages/browser-logs/src/domain/createErrorFieldFromRawError.ts b/packages/browser-logs/src/domain/createErrorFieldFromRawError.ts index a098151f95..cc0d80b053 100644 --- a/packages/browser-logs/src/domain/createErrorFieldFromRawError.ts +++ b/packages/browser-logs/src/domain/createErrorFieldFromRawError.ts @@ -14,6 +14,7 @@ export function createErrorFieldFromRawError( return { stack: rawError.stack, kind: rawError.type, + feature_id: rawError.featureId, message: includeMessage ? rawError.message : undefined, causes: rawError.causes, fingerprint: rawError.fingerprint, diff --git a/packages/browser-logs/src/rawLogsEvent.types.ts b/packages/browser-logs/src/rawLogsEvent.types.ts index 8ae09d843e..0bd0dc3b20 100644 --- a/packages/browser-logs/src/rawLogsEvent.types.ts +++ b/packages/browser-logs/src/rawLogsEvent.types.ts @@ -13,6 +13,7 @@ export type RawLogsEvent = interface Error { message?: string kind?: string + feature_id?: string stack?: string fingerprint?: string causes?: RawErrorCause[] diff --git a/packages/browser-rum-core/src/domain/error/errorCollection.ts b/packages/browser-rum-core/src/domain/error/errorCollection.ts index e4bd4c972a..e02498445c 100644 --- a/packages/browser-rum-core/src/domain/error/errorCollection.ts +++ b/packages/browser-rum-core/src/domain/error/errorCollection.ts @@ -70,20 +70,24 @@ export function doStartErrorCollection(lifeCycle: LifeCycle) { function processError(error: RawError): RawRumEventCollectedData { const rawRumEvent: RawRumErrorEvent = { date: error.startClocks.timeStamp, - error: { - id: generateUUID(), - message: error.message, - source: error.source, - stack: error.stack, - handling_stack: error.handlingStack, - component_stack: error.componentStack, - type: error.type, - handling: error.handling, - causes: error.causes, - source_type: 'browser', - fingerprint: error.fingerprint, - csp: error.csp, - }, + error: combine( + { + id: generateUUID(), + message: error.message, + source: error.source, + stack: error.stack, + handling_stack: error.handlingStack, + component_stack: error.componentStack, + type: error.type, + handling: error.handling, + causes: error.causes, + source_type: 'browser' as const, + fingerprint: error.fingerprint, + csp: error.csp, + }, + // TODO: add feature_id to the rum-events-format schema then remove this combine() + error.featureId !== undefined ? { feature_id: error.featureId } : {} + ), type: RumEventType.ERROR, context: error.context, }