Skip to content
Merged
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
23 changes: 17 additions & 6 deletions packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
});
};

/**
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/DdSdkReactNativeConfiguration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ export type PartialInitializationConfiguration = {
readonly bundleLogsWithTraces?: boolean;
readonly batchProcessingLevel?: BatchProcessingLevel;
readonly initialResourceThreshold?: number;
readonly resourceTracingSamplingRate?: number;
};
Comment thread
sbarrio marked this conversation as resolved.

const setConfigurationAttribute = <
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
});
}
Comment thread
sbarrio marked this conversation as resolved.

static stopTracking(): void {
if (DdRumResourceTracking.isTracking) {
DdRumResourceTracking.isTracking = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
};

Expand All @@ -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;
};
}

Expand All @@ -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,
Expand All @@ -113,8 +122,8 @@ const proxyOpen = (
graphql: {},
tracingAttributes: getTracingAttributes({
hostname,
firstPartyHostsRegexMap,
tracingSamplingRate,
firstPartyHostsRegexMap: context.firstPartyHostsRegexMap,
tracingSamplingRate: context.tracingSamplingRate,
rumSessionId: getCachedSessionId()
}),
baggageHeaderEntries: new Set<string>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ export type RegexMap = {
export abstract class RequestProxy {
abstract onTrackingStart: (context: RequestProxyOptions) => void;
abstract onTrackingStop: () => void;
abstract onTrackingUpdate: (context: RequestProxyOptions) => void;
}