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
95 changes: 93 additions & 2 deletions packages/core/src/rum/__tests__/DdRum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ import { DdRum } from '../DdRum';
import type { ActionEventMapper } from '../eventMappers/actionEventMapper';
import type { ErrorEventMapper } from '../eventMappers/errorEventMapper';
import type { ResourceEventMapper } from '../eventMappers/resourceEventMapper';
import { setCachedSessionId } from '../helper';
import {
clearCachedAccountId,
clearCachedSessionId,
clearCachedUserId,
setCachedAccountId,
setCachedSessionId,
setCachedUserId
} from '../helper';
import { DatadogTracingContext } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingContext';
import { DatadogTracingIdentifier } from '../instrumentation/resourceTracking/distributedTracing/DatadogTracingIdentifier';
import { TracingIdFormat } from '../instrumentation/resourceTracking/distributedTracing/TracingIdentifier';
Expand Down Expand Up @@ -48,7 +55,9 @@ describe('DdRum', () => {
beforeEach(() => {
jest.clearAllMocks();
BufferSingleton.onInitialization();
setCachedSessionId(undefined as any);
clearCachedSessionId();
clearCachedUserId();
clearCachedAccountId();
});

describe('Context validation', () => {
Expand Down Expand Up @@ -1009,6 +1018,88 @@ describe('DdRum', () => {
}
});

it('tracing context contains User ID in baggage when User ID is cached', () => {
for (let i = 0; i < 100; i++) {
const randomUserId = `test-${Math.random()}`;

setCachedUserId(randomUserId);
const tracingContext = DdRum.getTracingContextForPropagators(
[
PropagatorType.DATADOG,
PropagatorType.TRACECONTEXT,
PropagatorType.B3MULTI,
PropagatorType.B3
],
100
);

const requestHeaders = tracingContext.getHeadersForRequest();
expect(requestHeaders).toHaveProperty('baggage');
expect(requestHeaders['baggage']).toBe(
`user.id=${randomUserId}`
);

const resourceContext = tracingContext.getRumResourceContext();
expect(resourceContext['baggage']).toBeUndefined();
}
});

it('tracing context contains Account ID in baggage when Account ID is cached', () => {
for (let i = 0; i < 100; i++) {
const randomAccountId = `test-${Math.random()}`;

setCachedAccountId(randomAccountId);
const tracingContext = DdRum.getTracingContextForPropagators(
[
PropagatorType.DATADOG,
PropagatorType.TRACECONTEXT,
PropagatorType.B3MULTI,
PropagatorType.B3
],
100
);

const requestHeaders = tracingContext.getHeadersForRequest();
expect(requestHeaders).toHaveProperty('baggage');
expect(requestHeaders['baggage']).toBe(
`account.id=${randomAccountId}`
);

const resourceContext = tracingContext.getRumResourceContext();
expect(resourceContext['baggage']).toBeUndefined();
}
});

it('tracing context contains User ID, Account ID and Session ID in baggage when all session info is cached', () => {
for (let i = 0; i < 100; i++) {
const randomSessionId = `session-${Math.random()}`;
const randomUserId = `user-${Math.random()}`;
const randomAccountId = `account-${Math.random()}`;

setCachedSessionId(randomSessionId);
setCachedUserId(randomUserId);
setCachedAccountId(randomAccountId);
const tracingContext = DdRum.getTracingContextForPropagators(
[
PropagatorType.DATADOG,
PropagatorType.TRACECONTEXT,
PropagatorType.B3MULTI,
PropagatorType.B3
],
100
);

const requestHeaders = tracingContext.getHeadersForRequest();
expect(requestHeaders).toHaveProperty('baggage');
expect(requestHeaders['baggage']).toBe(
`session.id=${randomSessionId},user.id=${randomUserId},account.id=${randomAccountId}`
);

const resourceContext = tracingContext.getRumResourceContext();
expect(resourceContext['baggage']).toBeUndefined();
}
});

it('injects headers and context correctly with all propagators and sampling rate (50% 0, 50% 100)', () => {
for (let i = 0; i < 100; i++) {
const tracingSamplingRate =
Expand Down
46 changes: 36 additions & 10 deletions packages/core/src/rum/helper.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,62 @@
import { getGlobalInstance } from '../utils/singletonUtils';

/*
* Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0.
* This product includes software developed at Datadog (https://www.datadoghq.com/).
* Copyright 2016-Present Datadog, Inc.
*/
let _cachedSessionId: string | undefined;
let _cachedUserId: string | undefined;
let _cachedAccountId: string | undefined;
const SESSION_INFO_MODULE = 'com.datadog.reactnative.sdk.session_info';

class _SessionInfo {
sessionId: string | undefined = undefined;
userId: string | undefined = undefined;
accountId: string | undefined = undefined;

_reset() {
this.sessionId = undefined;
this.userId = undefined;
this.accountId = undefined;
}
}

const SessionInfo = getGlobalInstance(
SESSION_INFO_MODULE,
() => new _SessionInfo()
);

// Helper functions to interact with the SessionInfo singleton
export const getCachedSessionId = () => {
return _cachedSessionId;
return SessionInfo.sessionId;
};

export const setCachedSessionId = (sessionId: string) => {
_cachedSessionId = sessionId;
SessionInfo.sessionId = sessionId;
};

export const clearCachedSessionId = () => {
_cachedSessionId = undefined;
SessionInfo.sessionId = undefined;
};

export const getCachedUserId = () => {
return _cachedUserId;
return SessionInfo.userId;
};

export const setCachedUserId = (userId: string) => {
_cachedUserId = userId;
SessionInfo.userId = userId;
};

export const clearCachedUserId = () => {
SessionInfo.userId = undefined;
};

export const getCachedAccountId = () => {
return _cachedAccountId;
return SessionInfo.accountId;
};

export const setCachedAccountId = (accountId: string) => {
_cachedAccountId = accountId;
SessionInfo.accountId = accountId;
};

export const clearCachedAccountId = () => {
SessionInfo.accountId = undefined;
};
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
* Copyright 2016-Present Datadog, Inc.
*/

import { formatBaggageHeader } from '../requestProxy/XHRProxy/baggageHeaderUtils';

import { DatadogTracingIdentifier } from './DatadogTracingIdentifier';
import type { SpanId, TraceId } from './TracingIdentifier';
import { BAGGAGE_HEADER_KEY } from './distributedTracingHeaders';

/**
* An object that contains the tracing attributes as headers for network requests and attributes
Expand All @@ -23,7 +26,7 @@ export class DatadogTracingContext {
traceId: TraceId | undefined | void,
spanId: SpanId | undefined | void
) {
this.headersForRequest = requestHeaders;
this.headersForRequest = this.processHeaders(requestHeaders);
this.rumResourceContext = resourceContext;
this.traceId = traceId
? new DatadogTracingIdentifier(traceId)
Expand Down Expand Up @@ -78,4 +81,24 @@ export class DatadogTracingContext {
inject(key, this.rumResourceContext[key]);
});
}

private processHeaders(
requestHeaders: { header: string; value: string }[]
) {
const baggageHeaderEntries = requestHeaders
.filter(
({ header, value }) => header === BAGGAGE_HEADER_KEY && value
)
.map(({ value }) => value);
const baggageHeader = formatBaggageHeader(
new Set(baggageHeaderEntries)
);
if (!baggageHeader) {
return requestHeaders;
}

return requestHeaders
.filter(({ header }) => header !== BAGGAGE_HEADER_KEY)
.concat({ header: BAGGAGE_HEADER_KEY, value: baggageHeader });
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,13 +167,6 @@ export const getTracingHeadersFromAttributes = (
}
}

if (hasDatadogOrW3CPropagator && tracingAttributes.rumSessionId) {
headers.push({
header: BAGGAGE_HEADER_KEY,
value: `${DD_RUM_SESSION_ID_TAG}=${tracingAttributes.rumSessionId}`
});
}

return headers;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,32 @@ describe('formatBaggageHeader', () => {
expect(logSpy).not.toHaveBeenCalled();
});

it('should percent-encode spaces and non-ASCII characters in values', () => {
it('should not encode non-datadog-specific property values and log a warning', () => {
const entries = new Set(['user=Amélie', 'region=us east']);
const result = formatBaggageHeader(entries);
expect(result).toBe('user=Am%C3%A9lie,region=us%20east');
expect(result).toBe('user=Amélie,region=us east');
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('invalid baggage header value detected'),
SdkVerbosity.WARN
);
});

it('should only encode datadog-specific property values', () => {
const entries = new Set([
'user=Amélie',
'session.id=example session id',
'user.id=example user id',
'account.id=example account id',
'region=us east'
]);
const result = formatBaggageHeader(entries);
expect(result).toBe(
'user=Amélie,session.id=example%20session%20id,user.id=example%20user%20id,account.id=example%20account%20id,region=us east'
);
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('invalid baggage header value detected'),
SdkVerbosity.WARN
);
});

it('should support properties with and without values', () => {
Expand All @@ -38,32 +60,32 @@ describe('formatBaggageHeader', () => {
expect(result).toBe('foo=bar;p1=one;p2');
});

it('should skip invalid entries without crashing', () => {
it('should warn of invalid entries without crashing', () => {
const entries = new Set(['valid=ok', 'invalidEntry']);
const result = formatBaggageHeader(entries);
expect(result).toBe('valid=ok');
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('Dropped invalid baggage header entry'),
SdkVerbosity.WARN
SdkVerbosity.ERROR
);
});

it('should skip entries with invalid key (non-token)', () => {
it('should warn of entries with invalid key (non-token)', () => {
const entries = new Set(['in valid=value', 'user=ok']);
const result = formatBaggageHeader(entries);
expect(result).toBe('user=ok');
expect(result).toBe('in valid=value,user=ok');
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('key not compliant'),
expect.stringContaining('invalid baggage header keys detected'),
SdkVerbosity.WARN
);
});

it('should skip invalid properties (bad property key)', () => {
it('should warn of invalid properties (bad property key)', () => {
const entries = new Set(['user=ok;invalid key=value;good=yes']);
const result = formatBaggageHeader(entries);
expect(result).toBe('user=ok;good=yes');
expect(result).toBe('user=ok;invalid key=value;good=yes');
expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('property key not compliant'),
expect.stringContaining('invalid baggage header key-value'),
SdkVerbosity.WARN
);
});
Expand Down Expand Up @@ -106,12 +128,62 @@ describe('formatBaggageHeader', () => {

it('should trim keys and values', () => {
const entries = new Set([
'traceId=abc123;sampled=true;debug',
'traceId=abc123; sampled=true; debug',
'test1 = this is a test'
]);
const result = formatBaggageHeader(entries);
expect(result).toBe(
'traceId=abc123;sampled=true;debug,test1=this%20is%20a%20test'
'traceId=abc123;sampled=true;debug,test1=this is a test'
);
});

it('should not double encode non-datadog-specific percent-encoded values', () => {
const entries = new Set([
'user=foo%20bar',
'name=Am%C3%A9lie',
'path=%2Fapi%2Fv1%2Fusers'
]);

const result = formatBaggageHeader(entries);

expect(result).toBe(
'user=foo%20bar,name=Am%C3%A9lie,path=%2Fapi%2Fv1%2Fusers'
);
});

it('should not double encode non-datadog-specific percent-encoded property values', () => {
const entries = new Set([
'traceId=abc123;user=Am%C3%A9lie;note=hello%20world'
]);

const result = formatBaggageHeader(entries);

expect(result).toBe(
'traceId=abc123;user=Am%C3%A9lie;note=hello%20world'
);
});

it('should re-encode mixed encoded/decoded datadog-specific values only once', () => {
const entries = new Set([
// should not be encoded because "user" is not datadog-specific
'user=hello%20world test',
// partially encoded: "%25" + literal space
'session.id=example%20session id',
// contains a literal "%", not a valid escape sequence
'user.id=example%user id',
// not encoded
'account.id=example account id'
]);

const result = formatBaggageHeader(entries);

expect(result).toBe(
'user=hello%20world test,session.id=example%20session%20id,user.id=example%user%20id,account.id=example%20account%20id'
);

expect(logSpy).toHaveBeenCalledWith(
expect.stringContaining('invalid baggage header value detected'),
SdkVerbosity.WARN
);
});
});
Loading