diff --git a/packages/core/src/DdSdkReactNative.tsx b/packages/core/src/DdSdkReactNative.tsx index aa7e2ec36..f4a20992b 100644 --- a/packages/core/src/DdSdkReactNative.tsx +++ b/packages/core/src/DdSdkReactNative.tsx @@ -167,13 +167,24 @@ export class DdSdkReactNative { return new Promise(resolve => resolve()); } - return DdSdkReactNative.initializeNativeSDK( - buildConfigurationFromPartialConfiguration( - DdSdkReactNative.features, - configuration - ), - { initializationModeForTelemetry: 'PARTIAL' } + const builtConfiguration = buildConfigurationFromPartialConfiguration( + DdSdkReactNative.features, + configuration ); + + // The XHRProxy was installed at provider mount with the features defaults; + // re-apply the resolved values so a resourceTracingSamplingRate (or + // firstPartyHosts) supplied via DatadogProvider.initialize takes effect. + DdRumResourceTracking.updateTrackingContext({ + tracingSamplingRate: builtConfiguration.resourceTracingSamplingRate, + firstPartyHosts: formatFirstPartyHosts( + builtConfiguration.firstPartyHosts + ) + }); + + return DdSdkReactNative.initializeNativeSDK(builtConfiguration, { + initializationModeForTelemetry: 'PARTIAL' + }); }; /** diff --git a/packages/core/src/DdSdkReactNativeConfiguration.tsx b/packages/core/src/DdSdkReactNativeConfiguration.tsx index 158d258eb..1c04c3d06 100644 --- a/packages/core/src/DdSdkReactNativeConfiguration.tsx +++ b/packages/core/src/DdSdkReactNativeConfiguration.tsx @@ -469,6 +469,7 @@ export type PartialInitializationConfiguration = { readonly bundleLogsWithTraces?: boolean; readonly batchProcessingLevel?: BatchProcessingLevel; readonly initialResourceThreshold?: number; + readonly resourceTracingSamplingRate?: number; }; const setConfigurationAttribute = < diff --git a/packages/core/src/rum/instrumentation/resourceTracking/DdRumResourceTracking.tsx b/packages/core/src/rum/instrumentation/resourceTracking/DdRumResourceTracking.tsx index 1eaa6ba24..777b22480 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/DdRumResourceTracking.tsx +++ b/packages/core/src/rum/instrumentation/resourceTracking/DdRumResourceTracking.tsx @@ -58,6 +58,31 @@ export class DdRumResourceTracking { DdRumResourceTracking.isTracking = true; } + /** + * Applies a new tracing sampling rate and/or first-party hosts to the + * already-installed request proxy. Used by deferred-initialization flows + * (DatadogProvider.initialize) where tracking is started at provider mount + * with default features, and the final values are only known later. + * No-op if tracking has not started. + */ + static updateTrackingContext({ + tracingSamplingRate, + firstPartyHosts + }: { + tracingSamplingRate: number; + firstPartyHosts: FirstPartyHost[]; + }): void { + if (!DdRumResourceTracking.isTracking || !this.requestProxy) { + return; + } + this.requestProxy.onTrackingUpdate({ + tracingSamplingRate, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder( + firstPartyHosts + ) + }); + } + static stopTracking(): void { if (DdRumResourceTracking.isTracking) { DdRumResourceTracking.isTracking = false; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/__tests__/DdRumResourceTracking.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/__tests__/DdRumResourceTracking.test.ts index 1d75f0500..65e96ec56 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/__tests__/DdRumResourceTracking.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/__tests__/DdRumResourceTracking.test.ts @@ -9,6 +9,7 @@ import { NativeModules } from 'react-native'; import { BufferSingleton } from '../../../../sdk/DatadogProvider/Buffer/BufferSingleton'; import { PropagatorType } from '../../../types'; import { DdRumResourceTracking } from '../DdRumResourceTracking'; +import { SAMPLING_PRIORITY_HEADER_KEY } from '../distributedTracing/distributedTracingHeaders'; import { XMLHttpRequestMock } from './__utils__/XMLHttpRequestMock'; @@ -90,4 +91,114 @@ describe('DdRumResourceTracking', () => { expect(DdRum.startResource).not.toHaveBeenCalled(); expect(DdRum.stopResource).not.toHaveBeenCalled(); }); + + describe('updateTrackingContext', () => { + beforeEach(() => { + // earlier tests in this file may leave tracking enabled — reset + // so each updateTrackingContext test starts from a clean state. + DdRumResourceTracking.stopTracking(); + }); + + afterEach(() => { + DdRumResourceTracking.stopTracking(); + }); + + it('is a no-op when called before startTracking', async () => { + // GIVEN tracking was never started + + // WHEN + DdRumResourceTracking.updateTrackingContext({ + tracingSamplingRate: 100, + firstPartyHosts: [ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ] + }); + + executeRequest('https://api.example.com/v2/user'); + await flushPromises(); + + // THEN: no XHR proxy was installed; no resource events captured + expect(DdRum.startResource).not.toHaveBeenCalled(); + expect(DdRum.stopResource).not.toHaveBeenCalled(); + }); + + it('applies the updated sampling rate to subsequent requests', () => { + // GIVEN tracking installed with rate=0 + DdRumResourceTracking.startTracking({ + tracingSamplingRate: 0, + firstPartyHosts: [ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ] + }); + + // pre-update request gets sampling priority '0' + const xhrBeforeUpdate = new XMLHttpRequestMock(); + xhrBeforeUpdate.open('GET', 'https://api.example.com/v2/user'); + xhrBeforeUpdate.send(); + expect( + (xhrBeforeUpdate.requestHeaders as any)[ + SAMPLING_PRIORITY_HEADER_KEY + ] + ).toBe('0'); + + // WHEN + DdRumResourceTracking.updateTrackingContext({ + tracingSamplingRate: 100, + firstPartyHosts: [ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ] + }); + + // THEN: post-update request uses the new rate + const xhrAfterUpdate = new XMLHttpRequestMock(); + xhrAfterUpdate.open('GET', 'https://api.example.com/v2/user'); + xhrAfterUpdate.send(); + expect( + (xhrAfterUpdate.requestHeaders as any)[ + SAMPLING_PRIORITY_HEADER_KEY + ] + ).toBe('1'); + }); + + it('is a no-op after tracking has been stopped', async () => { + // GIVEN + DdRumResourceTracking.startTracking({ + tracingSamplingRate: 100, + firstPartyHosts: [ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ] + }); + DdRumResourceTracking.stopTracking(); + + // WHEN + DdRumResourceTracking.updateTrackingContext({ + tracingSamplingRate: 100, + firstPartyHosts: [ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ] + }); + + executeRequest('https://api.example.com/v2/user'); + await flushPromises(); + + // THEN: tracking remains stopped, nothing captured + expect(DdRum.startResource).not.toHaveBeenCalled(); + expect(DdRum.stopResource).not.toHaveBeenCalled(); + }); + }); }); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index d48c4f01a..c624559f6 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -56,6 +56,7 @@ interface XHRProxyProviders { */ export class XHRProxy extends RequestProxy { private providers: XHRProxyProviders; + private context: RequestProxyOptions | null = null; private static originalXhrOpen: typeof XMLHttpRequest.prototype.open; private static originalXhrSend: typeof XMLHttpRequest.prototype.send; private static originalXhrSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader; @@ -69,6 +70,7 @@ export class XHRProxy extends RequestProxy { XHRProxy.originalXhrOpen = this.providers.xhrType.prototype.open; XHRProxy.originalXhrSend = this.providers.xhrType.prototype.send; XHRProxy.originalXhrSetRequestHeader = this.providers.xhrType.prototype.setRequestHeader; + this.context = context; proxyRequests(this.providers, context); }; @@ -77,6 +79,15 @@ export class XHRProxy extends RequestProxy { this.providers.xhrType.prototype.send = XHRProxy.originalXhrSend; this.providers.xhrType.prototype.setRequestHeader = XHRProxy.originalXhrSetRequestHeader; + this.context = null; + }; + + onTrackingUpdate = (options: RequestProxyOptions) => { + if (this.context === null) { + return; + } + this.context.tracingSamplingRate = options.tracingSamplingRate; + this.context.firstPartyHostsRegexMap = options.firstPartyHostsRegexMap; }; } @@ -94,8 +105,6 @@ const proxyOpen = ( context: RequestProxyOptions ): void => { const originalXhrOpen = xhrType.prototype.open; - const firstPartyHostsRegexMap = context.firstPartyHostsRegexMap; - const tracingSamplingRate = context.tracingSamplingRate; xhrType.prototype.open = function open( this: DdRumXhr, @@ -113,8 +122,8 @@ const proxyOpen = ( graphql: {}, tracingAttributes: getTracingAttributes({ hostname, - firstPartyHostsRegexMap, - tracingSamplingRate, + firstPartyHostsRegexMap: context.firstPartyHostsRegexMap, + tracingSamplingRate: context.tracingSamplingRate, rumSessionId: getCachedSessionId() }), baggageHeaderEntries: new Set() diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index cfc5e0178..b2426ff45 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -425,6 +425,74 @@ describe('XHRProxy', () => { expect(xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY]).toBe('0'); }); + it('applies a tracingSamplingRate updated via onTrackingUpdate to subsequent XHRs (regression: RUMS-5973)', async () => { + // GIVEN + const method = 'GET'; + const url = 'https://api.example.com/v2/user'; + const firstPartyHostsRegexMap = firstPartyHostsRegexMapBuilder([ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ]); + xhrProxy.onTrackingStart({ + tracingSamplingRate: 0, + firstPartyHostsRegexMap + }); + + // WHEN: an XHR is opened with the initial rate of 0 + const xhrBeforeUpdate = new XMLHttpRequestMock(); + xhrBeforeUpdate.open(method, url); + xhrBeforeUpdate.send(); + xhrBeforeUpdate.notifyResponseArrived(); + xhrBeforeUpdate.complete(200, 'ok'); + await flushPromises(); + + // AND: the tracking context is updated to 100 + xhrProxy.onTrackingUpdate({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap + }); + + // AND: a second XHR is opened + const xhrAfterUpdate = new XMLHttpRequestMock(); + xhrAfterUpdate.open(method, url); + xhrAfterUpdate.send(); + xhrAfterUpdate.notifyResponseArrived(); + xhrAfterUpdate.complete(200, 'ok'); + await flushPromises(); + + // THEN: the pre-update request keeps priority 0, the post-update one is sampled at 100% + expect( + xhrBeforeUpdate.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY] + ).toBe('0'); + expect( + xhrAfterUpdate.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY] + ).toBe('1'); + }); + + it('ignores onTrackingUpdate when tracking has not started', () => { + // GIVEN: tracking was never started, so XMLHttpRequest is not proxied + + // WHEN + xhrProxy.onTrackingUpdate({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([ + { + match: 'api.example.com', + propagatorTypes: [PropagatorType.DATADOG] + } + ]) + }); + + // THEN: no throw, and a subsequent open is not instrumented + const xhr = new XMLHttpRequestMock(); + xhr.open('GET', 'https://api.example.com/v2/user'); + expect( + xhr.requestHeaders[SAMPLING_PRIORITY_HEADER_KEY] + ).toBeUndefined(); + }); + it('adds tracecontext request headers when the host is instrumented with tracecontext and request is sampled', async () => { // GIVEN const method = 'GET'; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RequestProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RequestProxy.ts index 61838d695..8b94534a0 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RequestProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RequestProxy.ts @@ -19,4 +19,5 @@ export type RegexMap = { export abstract class RequestProxy { abstract onTrackingStart: (context: RequestProxyOptions) => void; abstract onTrackingStop: () => void; + abstract onTrackingUpdate: (context: RequestProxyOptions) => void; }