Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/browser-core/src/domain/error/error.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 15 additions & 2 deletions packages/browser-core/src/domain/report/browser.types.ts
Original file line number Diff line number Diff line change
@@ -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
}

Expand Down Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -74,4 +79,43 @@ describe('report observable', () => {

expect(notifyReport).not.toHaveBeenCalled()
})

it(`should notify ${RawReportType.documentPolicyViolation} reports`, () => {
consoleSubscription = initReportObservable([RawReportType.documentPolicyViolation]).subscribe(notifyReport)
reportingObserver.raiseReport('document-policy-violation')

expect(notifyReport).toHaveBeenCalledOnceWith(
jasmine.objectContaining({
message: 'document-policy-violation: Document policy violation: resource compression is required.',
type: 'document-policy-violation',
featureId: 'network-efficiency-guardrails',
csp: { disposition: 'report' },
})
)
})

it(`should compute stack for ${RawReportType.documentPolicyViolation}`, () => {
consoleSubscription = initReportObservable([RawReportType.documentPolicyViolation]).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 <anonymous> @ https://foo.bar/large-uncompressed.js`)
})

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).toHaveBeenCalledOnceWith(
jasmine.objectContaining({
type: 'document-policy-violation',
featureId: 'some-other-policy',
})
)
})
})
29 changes: 22 additions & 7 deletions packages/browser-core/src/domain/report/reportObservable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
documentPolicyViolation: 'document-policy-violation',
} 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[]) {
Expand All @@ -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 = apis.filter((api): api is ReportType => api !== RawReportType.cspViolation)
if (reportTypes.length) {
observables.push(createReportObservable(reportTypes))
}
Expand All @@ -41,8 +42,9 @@ function createReportObservable(reportTypes: ReportType[]) {
return
}

const handleReports = monitor((reports: Array<DeprecationReport | InterventionReport>, _: ReportingObserver) =>
reports.forEach((report) => observable.notify(buildRawReportErrorFromReport(report)))
const handleReports = monitor(
(reports: Array<DeprecationReport | InterventionReport | DocumentPolicyViolationReport>, _: ReportingObserver) =>
reports.forEach((report) => observable.notify(buildRawReportErrorFromReport(report)))
) as ReportingObserverCallback

const observer = new window.ReportingObserver(handleReports, {
Expand Down Expand Up @@ -72,9 +74,22 @@ function createCspViolationReportObservable() {
})
}

function buildRawReportErrorFromReport(report: DeprecationReport | InterventionReport): RawReportError {
const { type, body } = report
function buildRawReportErrorFromReport(
report: DeprecationReport | InterventionReport | DocumentPolicyViolationReport
): RawReportError {
if (report.type === 'document-policy-violation') {
const { featureId, message, disposition, sourceFile } = report.body
return buildRawReportError({
type: report.type,
featureId,
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}`,
Expand Down
29 changes: 26 additions & 3 deletions packages/browser-core/test/emulate/mockReportingObserver.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
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'
Expand Down Expand Up @@ -39,9 +43,13 @@ export function mockReportingObserver() {
})

return {
raiseReport(type: ReportType) {
raiseReport(type: ReportType, overrides?: Partial<DocumentPolicyViolationReport>) {
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))
}
},
}
Expand Down Expand Up @@ -85,6 +93,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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ describe('createErrorFieldFromRawError', () => {
componentStack: 'at Flex',
originalError: new Error('baz'),
type: 'qux',
featureId: 'quux-feature',
message: 'quux',
stack: 'quuz',
causes: [
Expand Down Expand Up @@ -43,6 +44,7 @@ describe('createErrorFieldFromRawError', () => {
expect(createErrorFieldFromRawError(exhaustiveRawError)).toEqual({
message: undefined,
kind: 'qux',
feature_id: 'quux-feature',
stack: 'quuz',
causes: [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/browser-logs/src/rawLogsEvent.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type RawLogsEvent =
interface Error {
message?: string
kind?: string
feature_id?: string
stack?: string
fingerprint?: string
causes?: RawErrorCause[]
Expand Down
32 changes: 18 additions & 14 deletions packages/browser-rum-core/src/domain/error/errorCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,24 @@ export function doStartErrorCollection(lifeCycle: LifeCycle) {
function processError(error: RawError): RawRumEventCollectedData<RawRumErrorEvent> {
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,
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import type { Observable, RawError } from '@datadog/browser-core'
import { initReportObservable, RawReportType } from '@datadog/browser-core'
export function trackReportError(errorObservable: Observable<RawError>) {
const subscription = initReportObservable([RawReportType.cspViolation, RawReportType.intervention]).subscribe(
(rawError) => errorObservable.notify(rawError)
)
const subscription = initReportObservable([
RawReportType.cspViolation,
RawReportType.intervention,
RawReportType.documentPolicyViolation,
]).subscribe((rawError) => errorObservable.notify(rawError))

return {
stop: () => {
Expand Down
13 changes: 13 additions & 0 deletions test/e2e/lib/framework/serverApps/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
8 changes: 7 additions & 1 deletion test/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,10 +168,16 @@ function getProjects() {
}

function project(name: string, device: string) {
const isChromium = name === 'chromium'
return {
name,
metadata: { sessionName: device, name } satisfies BrowserConfiguration,
use: devices[device],
use: {
...devices[device],
// 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'] } } : {}),
},
}
}

Expand Down
Loading
Loading