From 0e835ad313c23df5d53929cfbfd387023a1c462a Mon Sep 17 00:00:00 2001 From: Miguel Arroz Date: Fri, 17 Apr 2026 14:40:35 +0100 Subject: [PATCH 1/3] RUM-15702: Adds `getIsTraceSampled()` to the event bridge. This way, when running inside a WebView, the native client can decide if tracing happens or not. By doing that, the native SDK can guarantee deterministic sampling between requests done by itself, and the ones done from the web page displayed in the WebView. The decision is passed through the bridge instead of the sampling rate because, since the session ID is not passed (a mock one is used, and later replaced in the native SDK after the events flow through the bridge), the Browser SDK does not have enough information to perform a deterministic sampling decision. --- packages/core/src/transport/eventBridge.ts | 7 +++++++ packages/core/test/emulate/mockEventBridge.ts | 9 +++++++- .../rum-core/src/boot/preStartRum.spec.ts | 21 +++++++++++++++++++ packages/rum-core/src/boot/preStartRum.ts | 5 ++++- 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/packages/core/src/transport/eventBridge.ts b/packages/core/src/transport/eventBridge.ts index ae93f6e1b0..0b19f667a4 100644 --- a/packages/core/src/transport/eventBridge.ts +++ b/packages/core/src/transport/eventBridge.ts @@ -8,6 +8,7 @@ export interface BrowserWindowWithEventBridge extends Window { export interface DatadogEventBridge { getCapabilities?(): string getPrivacyLevel?(): DefaultPrivacyLevel + getIsTraceSampled?(): string getAllowedWebViewHosts(): string send(msg: string): void } @@ -30,6 +31,12 @@ export function getEventBridge() { getPrivacyLevel() { return eventBridgeGlobal.getPrivacyLevel?.() }, + getIsTraceSampled() { + const value = eventBridgeGlobal.getIsTraceSampled?.() + if (value !== undefined) { + return value === 'true' + } + }, getAllowedWebViewHosts() { return JSON.parse(eventBridgeGlobal.getAllowedWebViewHosts()) as string[] }, diff --git a/packages/core/test/emulate/mockEventBridge.ts b/packages/core/test/emulate/mockEventBridge.ts index b9f76a82dd..c458fe2f91 100644 --- a/packages/core/test/emulate/mockEventBridge.ts +++ b/packages/core/test/emulate/mockEventBridge.ts @@ -7,12 +7,19 @@ export function mockEventBridge({ allowedWebViewHosts = [window.location.hostname], privacyLevel = DefaultPrivacyLevel.MASK, capabilities = [BridgeCapability.RECORDS], -}: { allowedWebViewHosts?: string[]; privacyLevel?: DefaultPrivacyLevel; capabilities?: BridgeCapability[] } = {}) { + isTraceSampled, +}: { + allowedWebViewHosts?: string[] + privacyLevel?: DefaultPrivacyLevel + capabilities?: BridgeCapability[] + isTraceSampled?: boolean +} = {}) { const eventBridge: DatadogEventBridge = { send: (_msg: string) => undefined, getAllowedWebViewHosts: () => JSON.stringify(allowedWebViewHosts), getCapabilities: () => JSON.stringify(capabilities), getPrivacyLevel: () => privacyLevel, + getIsTraceSampled: isTraceSampled !== undefined ? () => String(isTraceSampled) : undefined, } ;(window as BrowserWindowWithEventBridge).DatadogEventBridge = eventBridge diff --git a/packages/rum-core/src/boot/preStartRum.spec.ts b/packages/rum-core/src/boot/preStartRum.spec.ts index deb8ab34c8..b56494c085 100644 --- a/packages/rum-core/src/boot/preStartRum.spec.ts +++ b/packages/rum-core/src/boot/preStartRum.spec.ts @@ -141,6 +141,27 @@ describe('preStartRum', () => { ) }) + it('should set traceSampleRate to 100 when the bridge reports trace is sampled', () => { + mockEventBridge({ isTraceSampled: true }) + const hybridInitConfiguration: HybridInitConfiguration = {} + strategy.init(hybridInitConfiguration as RumInitConfiguration, PUBLIC_API) + expect((strategy.initConfiguration as RumInitConfiguration)?.traceSampleRate).toEqual(100) + }) + + it('should set traceSampleRate to 0 when the bridge reports trace is not sampled', () => { + mockEventBridge({ isTraceSampled: false }) + const hybridInitConfiguration: HybridInitConfiguration = {} + strategy.init(hybridInitConfiguration as RumInitConfiguration, PUBLIC_API) + expect((strategy.initConfiguration as RumInitConfiguration)?.traceSampleRate).toEqual(0) + }) + + it('should preserve the original traceSampleRate when the bridge does not provide isTraceSampled', () => { + mockEventBridge() + const hybridInitConfiguration: HybridInitConfiguration = { traceSampleRate: 50 } + strategy.init(hybridInitConfiguration as RumInitConfiguration, PUBLIC_API) + expect((strategy.initConfiguration as RumInitConfiguration)?.traceSampleRate).toEqual(50) + }) + it('should initialize even if session cannot be handled', () => { mockEventBridge() spyOnProperty(document, 'cookie', 'get').and.returnValue('') diff --git a/packages/rum-core/src/boot/preStartRum.ts b/packages/rum-core/src/boot/preStartRum.ts index 58cf5ad3e4..efcbd350cb 100644 --- a/packages/rum-core/src/boot/preStartRum.ts +++ b/packages/rum-core/src/boot/preStartRum.ts @@ -326,12 +326,15 @@ export function createPreStartStrategy( } function overrideInitConfigurationForBridge(initConfiguration: RumInitConfiguration): RumInitConfiguration { + const bridge = getEventBridge() + const isTraceSampled = bridge?.getIsTraceSampled() return { ...initConfiguration, applicationId: '00000000-aaaa-0000-aaaa-000000000000', clientToken: 'empty', sessionSampleRate: 100, - defaultPrivacyLevel: initConfiguration.defaultPrivacyLevel ?? getEventBridge()?.getPrivacyLevel(), + defaultPrivacyLevel: initConfiguration.defaultPrivacyLevel ?? bridge?.getPrivacyLevel(), + traceSampleRate: isTraceSampled !== undefined ? (isTraceSampled ? 100 : 0) : initConfiguration.traceSampleRate, } } From 447fdee8570bc4a06b208eb0329140c7e04e4d61 Mon Sep 17 00:00:00 2001 From: Miguel Arroz Date: Fri, 17 Apr 2026 15:34:01 +0100 Subject: [PATCH 2/3] RUM-15702: Adds e2e tests. --- test/e2e/lib/framework/createTest.ts | 8 +++---- test/e2e/lib/framework/pageSetups.ts | 26 +++++++++++++++++------ test/e2e/scenario/eventBridge.scenario.ts | 26 +++++++++++++++++++++++ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/test/e2e/lib/framework/createTest.ts b/test/e2e/lib/framework/createTest.ts index a1d5042867..bb443e9de2 100644 --- a/test/e2e/lib/framework/createTest.ts +++ b/test/e2e/lib/framework/createTest.ts @@ -14,7 +14,7 @@ import { IntakeRegistry } from './intakeRegistry' import { flushEvents } from './flushEvents' import type { Servers } from './httpServers' import { getTestServers, waitForServersIdle } from './httpServers' -import type { CallerLocation, SetupFactory, SetupOptions, UrlHook } from './pageSetups' +import type { CallerLocation, EventBridgeOptions, SetupFactory, SetupOptions, UrlHook } from './pageSetups' import { html, DEFAULT_SETUPS, npmSetup, appSetup, formatConfiguration } from './pageSetups' import { createIntakeServerApp } from './serverApps/intake' import { createMockServerApp } from './serverApps/mock' @@ -52,7 +52,7 @@ class TestBuilder { private head = '' private body = '' private baseUrlHooks: UrlHook[] = [] - private eventBridge = false + private eventBridge: EventBridgeOptions | undefined private setups: Array<{ factory: SetupFactory; name?: string }> = DEFAULT_SETUPS private testFixture: typeof test = test private extension: { @@ -101,8 +101,8 @@ class TestBuilder { return this } - withEventBridge() { - this.eventBridge = true + withEventBridge(options: EventBridgeOptions = {}) { + this.eventBridge = options return this } diff --git a/test/e2e/lib/framework/pageSetups.ts b/test/e2e/lib/framework/pageSetups.ts index dac6bec2a2..5029c7f5d2 100644 --- a/test/e2e/lib/framework/pageSetups.ts +++ b/test/e2e/lib/framework/pageSetups.ts @@ -12,7 +12,7 @@ export interface SetupOptions { logsInit: (initConfiguration: LogsInitConfiguration) => void rumInit: (initConfiguration: RumInitConfiguration) => void remoteConfiguration?: RemoteConfiguration - eventBridge: boolean + eventBridge?: EventBridgeOptions head?: string body?: string baseUrlHooks: UrlHook[] @@ -41,6 +41,10 @@ export interface WorkerOptions { logsConfiguration?: LogsInitConfiguration } +export interface EventBridgeOptions { + isTraceSampled?: boolean +} + export type SetupFactory = (options: SetupOptions, servers: Servers) => string export type UrlHook = (baseUrl: URL, servers: Servers, options: SetupOptions) => void @@ -60,7 +64,7 @@ export function asyncSetup(options: SetupOptions, servers: Servers) { let footer = '' if (options.eventBridge) { - header += setupEventBridge(servers) + header += setupEventBridge(servers, options.eventBridge) } if (options.extension) { @@ -108,7 +112,7 @@ export function bundleSetup(options: SetupOptions, servers: Servers) { let header = options.head || '' if (options.eventBridge) { - header += setupEventBridge(servers) + header += setupEventBridge(servers, options.eventBridge) } if (options.extension) { @@ -143,7 +147,7 @@ export function npmSetup(options: SetupOptions, servers: Servers) { let header = options.head || '' if (options.eventBridge) { - header += setupEventBridge(servers) + header += setupEventBridge(servers, options.eventBridge) } if (options.extension) { @@ -180,7 +184,7 @@ export function appSetup(options: SetupOptions, servers: Servers, appName: strin let header = options.head || '' if (options.eventBridge) { - header += setupEventBridge(servers) + header += setupEventBridge(servers, options.eventBridge) } if (options.extension) { @@ -238,7 +242,7 @@ export function microfrontendSetup(options: SetupOptions, servers: Servers) { let header = options.head || '' if (options.eventBridge) { - header += setupEventBridge(servers) + header += setupEventBridge(servers, options.eventBridge) } if (options.extension) { @@ -278,7 +282,7 @@ function js(parts: readonly string[], ...vars: string[]) { return parts.reduce((full, part, index) => full + vars[index - 1] + part) } -function setupEventBridge(servers: Servers) { +function setupEventBridge(servers: Servers, options: EventBridgeOptions = {}) { const baseHostname = new URL(servers.base.origin).hostname // Send EventBridge events to the intake so we can inspect them in our E2E test cases. The URL @@ -289,6 +293,13 @@ function setupEventBridge(servers: Servers) { bridge: 'true', }).toString()}` + const isTraceSampledMethod = + options.isTraceSampled !== undefined + ? `getIsTraceSampled() { + return '${options.isTraceSampled}' + },` + : '' + return html`