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
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { extractGraphQLErrors } from '../../graphql/graphqlUtils';
import { getErrorData } from '../XHRProxy/xhrUtils';
import { callOriginalFetch } from '../common/FetchProxyState';
import type { RequestContext } from '../common/RequestContext';
import { createRequestContext } from '../common/RequestContext';
import { createRequestContext, getResourceKey } from '../common/RequestContext';
import { ResourceReporter } from '../common/ResourceReporter';
import { filterDevResource } from '../common/internalDevResourceBlocklist';
import {
Expand Down Expand Up @@ -259,7 +259,7 @@ const reportFetch = async ({
}

resourceReporter.reportResource({
key: `${context.timer.startTime}/${context.method}`,
key: getResourceKey(context),
request: {
method: context.method,
url: context.url,
Expand Down Expand Up @@ -287,7 +287,7 @@ const reportFetchFailure = ({
resourceReporter: ResourceReporter;
}) => {
resourceReporter.reportResource({
key: `${context.timer.startTime}/${context.method}`,
key: getResourceKey(context),
request: {
method: context.method,
url: context.url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,35 @@ describe('FetchProxy', () => {
expect(outgoingHeaders.get(PARENT_ID_HEADER_KEY)).not.toBeNull();
});

it('reports distinct keys for same-method requests started in the same millisecond', async () => {
const now = Date.now();
const dateNowSpy = jest.spyOn(Date, 'now').mockReturnValue(now);
const originalFetch = jest
.fn()
.mockResolvedValue(createResponse()) as jest.MockedFunction<
typeof fetch
>;
const reportResource = jest.fn();
const { fetchGlobal, proxy } = createProxy({
originalFetch,
reportResource
});
proxy.onTrackingStart(getOptions());

await Promise.all([
fetchGlobal.fetch('https://api.example.com/users'),
fetchGlobal.fetch('https://api.example.com/settings')
]);
dateNowSpy.mockRestore();

expect(reportResource).toHaveBeenCalledTimes(2);
const [first, second] = reportResource.mock.calls.map(
call => call[0] as RUMResource
);
expect(first.timings.startTime).toBe(second.timings.startTime);
expect(first.key).not.toBe(second.key);
});

it('extracts GraphQL metadata and removes its internal header', async () => {
const originalFetch = jest
.fn()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SdkVerbosity } from '../../../../../config/types';
import { extractGraphQLErrors } from '../../graphql/graphqlUtils';
import { isRunningWithinFetchProxy } from '../common/FetchProxyState';
import type { RequestContext } from '../common/RequestContext';
import { createRequestContext } from '../common/RequestContext';
import { createRequestContext, getResourceKey } from '../common/RequestContext';
import { ResourceReporter } from '../common/ResourceReporter';
import { filterDevResource } from '../common/internalDevResourceBlocklist';
import {
Expand Down Expand Up @@ -189,7 +189,7 @@ const reportXhr = async (
return;
}

const key = `${context.timer.startTime}/${context.method}`;
const key = getResourceKey(context);

context.timer.stop();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,46 @@ describe('XHRProxy', () => {
expect(xhr.originalOnReadyStateChangeCalled).toBe(true);
});

it('reports distinct resources for same-method XHR requests started in the same millisecond', async () => {
// GIVEN
const now = Date.now();
(Date.now as jest.MockedFunction<
typeof Date.now
>).mockImplementation(() => now);
xhrProxy.onTrackingStart({
tracingSamplingRate: 100,
firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([])
});

// WHEN
const xhr1 = new XMLHttpRequestMock();
const xhr2 = new XMLHttpRequestMock();
xhr1.open('GET', 'https://api.example.com/v2/user');
xhr2.open('GET', 'https://api.example.com/v2/settings');
xhr1.send();
xhr2.send();
xhr1.notifyResponseArrived();
xhr2.notifyResponseArrived();
xhr1.complete(200, 'ok');
xhr2.complete(200, 'ok');
await flushPromises();

// THEN
expect(DdNativeRum.startResource.mock.calls.length).toBe(2);
expect(DdNativeRum.startResource.mock.calls[0][4]).toBe(
DdNativeRum.startResource.mock.calls[1][4]
);
expect(DdNativeRum.startResource.mock.calls[0][0]).not.toBe(
DdNativeRum.startResource.mock.calls[1][0]
);
expect(DdNativeRum.stopResource.mock.calls.length).toBe(2);
expect(
DdNativeRum.stopResource.mock.calls.map(call => call[0]).sort()
).toEqual(
DdNativeRum.startResource.mock.calls.map(call => call[0]).sort()
);
});

it('intercepts failing XHR request when startTracking() + XHR.open() + XHR.send()', async () => {
// GIVEN
const method = 'GET';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { RequestProxyOptions } from '../interfaces/RequestProxy';
import type { DdRumResourceGraphqlAttributes } from '../interfaces/RumResource';

export interface RequestContext {
resourceId: number;
graphql: DdRumResourceGraphqlAttributes & {
trackErrors?: boolean;
};
Expand All @@ -27,6 +28,10 @@ export interface RequestContext {
baggageHeaderEntries: Set<string>;
}

// Disambiguates resources sharing the same start millisecond and method,
// which would otherwise collide on the native side.
let resourceSequence = 0;

export const createRequestContext = ({
method,
url,
Expand All @@ -37,6 +42,7 @@ export const createRequestContext = ({
options: RequestProxyOptions;
}): RequestContext => {
return {
resourceId: ++resourceSequence,
method,
url,
timer: new Timer(),
Expand All @@ -52,3 +58,6 @@ export const createRequestContext = ({
baggageHeaderEntries: new Set<string>()
};
};

export const getResourceKey = (context: RequestContext): string =>
`${context.timer.startTime}/${context.method}/${context.resourceId}`;
Loading