Skip to content
Open
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
3 changes: 2 additions & 1 deletion packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,8 @@ export class DdSdkReactNative {
if (trackResources) {
DdRumResourceTracking.startTracking({
resourceTraceSampleRate,
firstPartyHosts
firstPartyHosts,
resourceEventMapper
});
}

Expand Down
12 changes: 11 additions & 1 deletion packages/core/src/__tests__/DdSdkReactNative.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,8 @@ describe('DdSdkReactNative', () => {
match: 'something.fr',
propagatorTypes: ['datadog']
}
]
],
resourceEventMapper: undefined
});
});

Expand Down Expand Up @@ -835,6 +836,15 @@ describe('DdSdkReactNative', () => {
await DdRum.stopResource('key', 200, 'xhr', 22, {}, 345);

// THEN
expect(NativeModules.DdRum.startResource).toHaveBeenCalledWith(
'key',
'GET',
'https://datadoghq.com',
{
body: 'content'
},
234
);
expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith(
'key',
200,
Expand Down
55 changes: 35 additions & 20 deletions packages/core/src/rum/DdRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
getTracingContext,
getTracingContextForPropagators
} from './instrumentation/resourceTracking/distributedTracing/distributedTracingHeaders';
import { DdRumResourceTracking } from './instrumentation/resourceTracking/DdRumResourceTracking';
import type {
DdRumType,
FirstPartyHost,
Expand Down Expand Up @@ -253,17 +254,40 @@ class DdRumWrapper implements DdRumType {
context: object = {},
timestampMs: number = this.timeProvider.now()
): Promise<void> => {
const mappedEvent = this.resourceEventMapper.applyEventMapper({
key,
statusCode: 0,
kind: 'xhr',
size: -1,
context,
timestampMs,
resourceContext: { responseURL: url } as XMLHttpRequest
});
/**
* The keep/drop decision is made here, where the request URL is always
* available (`responseURL` is set to `url`). When the mapper drops the
* event (non-first-party host or missing consent) the resource is never
* started, so the matching `stopResource` is a native no-op. We always
* use the original `key` for the native call so start/stop correlate,
* and only sanitize the displayed URL and context.
*/
if (!mappedEvent) {
return generateEmptyPromise();
}

const startUrl = mappedEvent.resourceContext?.responseURL ?? url;

InternalLog.log(
`Starting RUM Resource #${key} ${method}: ${url}`,
`Starting RUM Resource #${key} ${method}: ${startUrl}`,
SdkVerbosity.DEBUG
);

return bufferVoidNativeCall(() =>
this.nativeRum.startResource(
key,
method,
url,
encodeAttributes(context),
startUrl,
Comment on lines 285 to +289
encodeAttributes(mappedEvent.context),
timestampMs
)
);
Expand All @@ -287,24 +311,13 @@ class DdRumWrapper implements DdRumType {
timestampMs,
resourceContext
});
/**
* We never drop at `stopResource`: the keep/drop decision was already
* made at `startResource`. A resource dropped there was never started,
* so this stop is a native no-op.
*/
if (!mappedEvent) {
/**
* To drop the resource we call `stopResource` and pass the `_dd.drop_resource` attribute in the context.
* It will be picked up by the resource mappers we implement on the native side that will drop the resource.
* This ensures we don't have any "started" resource left in memory on the native side.
*/
return bufferVoidNativeCall(() =>
this.nativeRum.stopResource(
key,
statusCode,
kind,
size,
{
'_dd.resource.drop_resource': true
},
timestampMs
)
);
return generateEmptyPromise();
}
Comment on lines 319 to 321
Comment on lines +314 to 321

InternalLog.log(
Expand Down Expand Up @@ -507,10 +520,12 @@ class DdRumWrapper implements DdRumType {
this.resourceEventMapper = generateResourceEventMapper(
resourceEventMapper
);
DdRumResourceTracking.updateResourceEventMapper(resourceEventMapper);
}

unregisterResourceEventMapper() {
this.resourceEventMapper = generateResourceEventMapper(undefined);
DdRumResourceTracking.updateResourceEventMapper(undefined);
}

registerActionEventMapper(actionEventMapper: ActionEventMapper) {
Expand Down
12 changes: 3 additions & 9 deletions packages/core/src/rum/__tests__/DdRum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1673,7 +1673,7 @@ describe('DdRum', () => {
);
});

it('adds the drop context key to the event if the mapper returns null', async () => {
it('does not start or stop the resource if the mapper returns null', async () => {
const resourceEventMapper: ResourceEventMapper = resource => {
return null;
};
Expand All @@ -1696,14 +1696,8 @@ describe('DdRum', () => {
245
);

expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith(
'key',
200,
'xhr',
302,
{ '_dd.resource.drop_resource': true },
245
);
expect(NativeModules.DdRum.startResource).not.toHaveBeenCalled();
expect(NativeModules.DdRum.stopResource).not.toHaveBeenCalled();
});
});

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/rum/eventMappers/resourceEventMapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type RawResource = {
resourceContext: XMLHttpRequest | undefined;
};

type ResourceEvent = RawResource & AdditionalEventDataForMapper;
export type ResourceEvent = RawResource & AdditionalEventDataForMapper;

type NativeResource = {
key: string;
Expand All @@ -27,6 +27,7 @@ type NativeResource = {
size: number;
context: object;
timestampMs: number;
resourceContext: XMLHttpRequest | undefined;
};

export type ResourceEventMapper = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import BigInt from 'big-integer';
import { InternalLog } from '../../../InternalLog';
import { SdkVerbosity } from '../../../config/types/SdkVerbosity';
import { getGlobalInstance } from '../../../utils/singletonUtils';
import type { ResourceEventMapper } from '../../eventMappers/resourceEventMapper';
import type { FirstPartyHost } from '../../types';

import { DistributedTracingSampling } from './distributedTracing/distributedTracingSampling';
Expand Down Expand Up @@ -40,10 +41,12 @@ class RumResourceTracking {
*/
startTracking({
resourceTraceSampleRate,
firstPartyHosts
firstPartyHosts,
resourceEventMapper
}: {
resourceTraceSampleRate: number;
firstPartyHosts: FirstPartyHost[];
resourceEventMapper?: ResourceEventMapper | null;
}): void {
// extra safety to avoid proxying the XHR class twice
if (this._isTracking) {
Expand All @@ -54,7 +57,8 @@ class RumResourceTracking {
return;
}

this._requestProxy = XHRProxy.createWithResourceReporter();
this._requestProxy =
XHRProxy.createWithResourceReporter(resourceEventMapper);
this._requestProxy.onTrackingStart({
tracingSamplingRate: resourceTraceSampleRate,
firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder(
Expand Down Expand Up @@ -99,6 +103,17 @@ class RumResourceTracking {
);
}

updateResourceEventMapper(
resourceEventMapper?: ResourceEventMapper | null
): void {
if (!this._isTracking || !this._requestProxy) {
return;
}
this._requestProxy.onTrackingUpdate({
resourceEventMapper
});
}

stopTracking(): void {
if (this._isTracking) {
this._isTracking = false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import { NativeModules } from 'react-native';

import { BufferSingleton } from '../../../../sdk/DatadogProvider/Buffer/BufferSingleton';
import { DdRum as DdRumWrapper } from '../../../DdRum';
import { PropagatorType } from '../../../types';
import { DdRumResourceTracking } from '../DdRumResourceTracking';
import { SAMPLING_PRIORITY_HEADER_KEY } from '../distributedTracing/headers';
Expand All @@ -26,6 +27,8 @@ beforeEach(() => {
});

afterEach(() => {
DdRumWrapper.unregisterResourceEventMapper();
DdRumResourceTracking.stopTracking();
global.XMLHttpRequest = undefined;
});

Expand Down Expand Up @@ -92,6 +95,67 @@ describe('DdRumResourceTracking', () => {
expect(DdRum.stopResource).not.toHaveBeenCalled();
});

it('applies a resource event mapper registered after tracking has started', async () => {
// GIVEN
DdRumResourceTracking.startTracking({
resourceTraceSampleRate: 100,
firstPartyHosts: []
});
DdRumWrapper.registerResourceEventMapper(resource => {
return {
...resource,
resourceContext: {
responseURL: 'https://sanitized.example.com/'
} as XMLHttpRequest
};
});

// WHEN
executeRequest('https://api.example.com/users/123');
await flushPromises();

// THEN
expect(DdRum.startResource).toHaveBeenCalledWith(
expect.anything(),
'GET',
'https://sanitized.example.com/',
expect.anything(),
expect.anything()
);
expect(DdRum.stopResource).toHaveBeenCalledTimes(1);
});

it('stops applying a resource event mapper after it is unregistered', async () => {
// GIVEN
DdRumResourceTracking.startTracking({
resourceTraceSampleRate: 100,
firstPartyHosts: []
});
DdRumWrapper.registerResourceEventMapper(resource => {
return {
...resource,
resourceContext: {
responseURL: 'https://sanitized.example.com/'
} as XMLHttpRequest
};
});
DdRumWrapper.unregisterResourceEventMapper();

// WHEN
executeRequest('https://api.example.com/users/123');
await flushPromises();

// THEN
expect(DdRum.startResource).toHaveBeenCalledWith(
expect.anything(),
'GET',
'https://api.example.com/users/123',
expect.anything(),
expect.anything()
);
expect(DdRum.stopResource).toHaveBeenCalledTimes(1);
});

describe('updateTrackingContext', () => {
beforeEach(() => {
DdRumResourceTracking.stopTracking();
Expand Down
Loading