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
7 changes: 6 additions & 1 deletion packages/core/src/DdSdkReactNative.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,12 @@ export class DdSdkReactNative {
if (trackResources) {
DdRumResourceTracking.startTracking({
resourceTraceSampleRate,
firstPartyHosts
firstPartyHosts,
resourceEventMapper,
resourceReporters: {
startResource: DdRum.startResource,
stopResource: DdRum.stopResource
}
});
}

Expand Down
24 changes: 22 additions & 2 deletions packages/core/src/__tests__/DdSdkReactNative.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ jest.mock(
() => {
return {
DdRumResourceTracking: {
startTracking: jest.fn().mockImplementation(() => {})
startTracking: jest.fn().mockImplementation(() => {}),
updateResourceEventMapper: jest
.fn()
.mockImplementation(() => {})
}
};
}
Expand Down Expand Up @@ -75,6 +78,9 @@ beforeEach(async () => {
(DdRumResourceTracking.startTracking as jest.MockedFunction<
typeof DdRumResourceTracking.startTracking
>).mockClear();
(DdRumResourceTracking.updateResourceEventMapper as jest.MockedFunction<
typeof DdRumResourceTracking.updateResourceEventMapper
>).mockClear();
(DdRumErrorTracking.startTracking as jest.MockedFunction<
typeof DdRumErrorTracking.startTracking
>).mockClear();
Expand Down Expand Up @@ -676,7 +682,12 @@ describe('DdSdkReactNative', () => {
match: 'something.fr',
propagatorTypes: ['datadog']
}
]
],
resourceEventMapper: null,
resourceReporters: {
startResource: DdRum.startResource,
stopResource: DdRum.stopResource
}
});
});

Expand Down Expand Up @@ -835,6 +846,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
91 changes: 63 additions & 28 deletions packages/core/src/rum/DdRum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getCachedUserId,
setCachedSessionId
} from './helper';
import { DdRumResourceTracking } from './instrumentation/resourceTracking/DdRumResourceTracking';
import type { DatadogTracingContext } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingContext';
import { DatadogTracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier';
import { TracingIdentifier } from './instrumentation/resourceTracking/distributedTracing/TracingIdentifier';
Expand All @@ -49,6 +50,7 @@ import type {
} from './types';

const RUM_MODULE = 'com.datadog.reactnative.rum';
const DROP_RESOURCE_ATTRIBUTE = '_dd.resource.drop_resource';

const generateEmptyPromise = () => new Promise<void>(resolve => resolve());

Expand Down Expand Up @@ -85,6 +87,7 @@ class DdRumWrapper implements DdRumType {
private resourceEventMapper = generateResourceEventMapper(undefined);
private actionEventMapper = generateActionEventMapper(undefined);
private timeProvider: TimeProvider = new DefaultTimeProvider();
private startedResourceKeys = new Set<string>();

startView = (
key: string,
Expand Down Expand Up @@ -251,19 +254,51 @@ class DdRumWrapper implements DdRumType {
method: string,
url: string,
context: object = {},
timestampMs: number = this.timeProvider.now()
timestampMs: number = this.timeProvider.now(),
kind: ResourceKind = 'other',
resourceContext?: XMLHttpRequest
): Promise<void> => {
const resourceContextForMapper = resourceContext
? { ...resourceContext, responseURL: url }
: ({ responseURL: url } as XMLHttpRequest);

const mappedEvent = this.resourceEventMapper.applyEventMapper({
key,
statusCode: 0,
kind,
size: -1,
context,
timestampMs,
resourceContext: resourceContextForMapper
});
/**
* The keep/drop decision is made here, where the request URL is
* available through `resourceContext.responseURL` for both XHR and
* manual resources. 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) {
this.startedResourceKeys.delete(key);
return generateEmptyPromise();
}

const startUrl = mappedEvent.resourceContext?.responseURL || url;
this.startedResourceKeys.add(key);

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 thread
teplymax marked this conversation as resolved.
encodeAttributes(mappedEvent.context),
timestampMs
)
);
Expand All @@ -287,38 +322,36 @@ class DdRumWrapper implements DdRumType {
timestampMs,
resourceContext
});
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
)
);
const wasStarted = this.startedResourceKeys.delete(key);

if (!wasStarted) {
return generateEmptyPromise();
}

const nativeResource = mappedEvent ?? {
key,
statusCode,
kind,
size,
context: {
[DROP_RESOURCE_ATTRIBUTE]: true
},
timestampMs,
resourceContext
};

InternalLog.log(
`Stopping RUM Resource #${key} status:${statusCode}`,
SdkVerbosity.DEBUG
);
return bufferVoidNativeCall(() =>
this.nativeRum.stopResource(
mappedEvent.key,
mappedEvent.statusCode,
mappedEvent.kind,
mappedEvent.size,
encodeAttributes(mappedEvent.context),
mappedEvent.timestampMs
nativeResource.key,
nativeResource.statusCode,
nativeResource.kind,
nativeResource.size,
encodeAttributes(nativeResource.context),
nativeResource.timestampMs
)
);
};
Expand Down Expand Up @@ -507,10 +540,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
79 changes: 77 additions & 2 deletions packages/core/src/rum/__tests__/DdRum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,53 @@ describe('DdRum', () => {
expect.anything()
);
});

test('passes the provided resource kind to the event mapper', async () => {
const resourceEventMapper: jest.MockedFunction<ResourceEventMapper> = jest.fn(
resource => resource
);
DdRum.registerResourceEventMapper(resourceEventMapper);

await DdRum.startResource(
'key',
'GET',
'https://my-api.com/',
{},
234,
'fetch'
);

expect(resourceEventMapper).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'fetch'
})
);

DdRum.unregisterResourceEventMapper();
});

test('uses other as the default resource kind for event mapper', async () => {
const resourceEventMapper: jest.MockedFunction<ResourceEventMapper> = jest.fn(
resource => resource
);
DdRum.registerResourceEventMapper(resourceEventMapper);

await DdRum.startResource(
'key',
'GET',
'https://my-api.com/',
{},
234
);

expect(resourceEventMapper).toHaveBeenCalledWith(
expect.objectContaining({
kind: 'other'
})
);

DdRum.unregisterResourceEventMapper();
});
});

describe('DdRum.stopResource', () => {
Expand Down Expand Up @@ -1673,7 +1720,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,12 +1743,40 @@ describe('DdRum', () => {
245
);

expect(NativeModules.DdRum.startResource).not.toHaveBeenCalled();
expect(NativeModules.DdRum.stopResource).not.toHaveBeenCalled();
});

it('stops a started native resource when the mapper returns null at stop', async () => {
let shouldDropResource = false;
const resourceEventMapper: ResourceEventMapper = resource => {
if (shouldDropResource) {
return null;
}
return resource;
};
DdRum.registerResourceEventMapper(resourceEventMapper);

await DdRum.startResource(
'key',
'GET',
'https://my-api.com/',
{ retry: false },
234
);

shouldDropResource = true;
await DdRum.stopResource('key', 200, 'xhr', 302, {}, 245);

expect(NativeModules.DdRum.startResource).toHaveBeenCalled();
expect(NativeModules.DdRum.stopResource).toHaveBeenCalledWith(
'key',
200,
'xhr',
302,
{ '_dd.resource.drop_resource': true },
{
'_dd.resource.drop_resource': true
},
245
);
});
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
Loading