diff --git a/packages/shared/common/src/internal/events/LDInternalOptions.ts b/packages/shared/common/src/internal/events/LDInternalOptions.ts index b852990a3e..54f6b91c54 100644 --- a/packages/shared/common/src/internal/events/LDInternalOptions.ts +++ b/packages/shared/common/src/internal/events/LDInternalOptions.ts @@ -12,6 +12,7 @@ export type LDInternalOptions = { analyticsEventPath?: string; diagnosticEventPath?: string; includeAuthorizationHeader?: boolean; + userAgentHeaderName?: 'user-agent' | 'x-launchdarkly-user-agent'; /** * In seconds. Log a warning if identifyTimeout is greater than this value. diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts index e91b93e169..7defe4afab 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts @@ -101,7 +101,6 @@ describe('given a stream processor with mock event source', () => { diagnosticsManager = new DiagnosticsManager(sdkKey, basicPlatform, {}); streamingProcessor = new StreamingProcessor( - sdkKey, { basicConfiguration: getBasicConfiguration(logger), platform: basicPlatform, @@ -109,6 +108,11 @@ describe('given a stream processor with mock event source', () => { '/all', [], listeners, + { + authorization: 'my-sdk-key', + 'user-agent': 'TestUserAgent/2.0.2', + 'x-launchdarkly-wrapper': 'Rapper/1.2.3', + }, diagnosticsManager, mockErrorHandler, ); @@ -137,7 +141,6 @@ describe('given a stream processor with mock event source', () => { it('sets streamInitialReconnectDelay correctly', () => { streamingProcessor = new StreamingProcessor( - sdkKey, { basicConfiguration: getBasicConfiguration(logger), platform: basicPlatform, @@ -145,6 +148,11 @@ describe('given a stream processor with mock event source', () => { '/all', [], listeners, + { + authorization: 'my-sdk-key', + 'user-agent': 'TestUserAgent/2.0.2', + 'x-launchdarkly-wrapper': 'Rapper/1.2.3', + }, diagnosticsManager, mockErrorHandler, 22, diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.ts index d9ccfaab45..8560fb89e7 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.ts @@ -10,7 +10,7 @@ import { LDStreamProcessor } from '../../api/subsystem'; import { LDStreamingError } from '../../errors'; import { ClientContext } from '../../options'; import { getStreamingUri } from '../../options/ServiceEndpoints'; -import { defaultHeaders, httpErrorMessage, shouldRetry } from '../../utils'; +import { defaultHeaders, httpErrorMessage, LDHeaders, shouldRetry } from '../../utils'; import { DiagnosticsManager } from '../diagnostics'; import { StreamingErrorHandler } from './types'; @@ -35,20 +35,20 @@ class StreamingProcessor implements LDStreamProcessor { private connectionAttemptStartTime?: number; constructor( - sdkKey: string, clientContext: ClientContext, streamUriPath: string, parameters: { key: string; value: string }[], private readonly listeners: Map, + baseHeaders: LDHeaders, private readonly diagnosticsManager?: DiagnosticsManager, private readonly errorHandler?: StreamingErrorHandler, private readonly streamInitialReconnectDelay = 1, ) { const { basicConfiguration, platform } = clientContext; - const { logger, tags } = basicConfiguration; - const { info, requests } = platform; + const { logger } = basicConfiguration; + const { requests } = platform; - this.headers = defaultHeaders(sdkKey, info, tags); + this.headers = { ...baseHeaders }; this.logger = logger; this.requests = requests; this.streamUri = getStreamingUri( diff --git a/packages/shared/common/src/utils/http.ts b/packages/shared/common/src/utils/http.ts index 0a9885780a..b7eebfd6ba 100644 --- a/packages/shared/common/src/utils/http.ts +++ b/packages/shared/common/src/utils/http.ts @@ -4,7 +4,8 @@ import { ApplicationTags } from '../options'; export type LDHeaders = { authorization?: string; - 'user-agent': string; + 'user-agent'?: string; + 'x-launchdarkly-user-agent'?: string; 'x-launchdarkly-wrapper'?: string; 'x-launchdarkly-tags'?: string; }; @@ -14,12 +15,13 @@ export function defaultHeaders( info: Info, tags?: ApplicationTags, includeAuthorizationHeader: boolean = true, + userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent' = 'user-agent', ): LDHeaders { const { userAgentBase, version, wrapperName, wrapperVersion } = info.sdkData(); - const headers: LDHeaders = { - 'user-agent': `${userAgentBase ?? 'NodeJSClient'}/${version}`, - }; + const headers: LDHeaders = {}; + + headers[userAgentHeaderName] = `${userAgentBase ?? 'NodeJSClient'}/${version}`; // edge sdks sets this to false because they use the clientSideID // and they don't need the authorization header diff --git a/packages/shared/mocks/src/streamingProcessor.ts b/packages/shared/mocks/src/streamingProcessor.ts index e596b443f1..cd038e7ef2 100644 --- a/packages/shared/mocks/src/streamingProcessor.ts +++ b/packages/shared/mocks/src/streamingProcessor.ts @@ -2,6 +2,7 @@ import type { ClientContext, EventName, internal, + LDHeaders, LDStreamingError, ProcessStreamResponse, } from '@common'; @@ -22,11 +23,11 @@ export const setupMockStreamingProcessor = ( MockStreamingProcessor.mockImplementation( ( - sdkKey: string, clientContext: ClientContext, streamUriPath: string, parameters: { key: string; value: string }[], listeners: Map, + baseHeaders: LDHeaders, diagnosticsManager: internal.DiagnosticsManager, errorHandler: internal.StreamingErrorHandler, _streamInitialReconnectDelay: number, diff --git a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index b0d404cab0..648be27661 100644 --- a/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -108,11 +108,11 @@ describe('sdk-client object', () => { 'dev-test-flag': false, }); expect(MockStreamingProcessor).toHaveBeenCalledWith( - expect.anything(), expect.anything(), '/stream/path', expect.anything(), expect.anything(), + expect.anything(), undefined, expect.anything(), ); @@ -129,11 +129,11 @@ describe('sdk-client object', () => { await ldc.identify(carContext); expect(MockStreamingProcessor).toHaveBeenCalledWith( - expect.anything(), expect.anything(), '/stream/path', [{ key: 'withReasons', value: 'true' }], expect.anything(), + expect.anything(), undefined, expect.anything(), ); diff --git a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts index 8e6a39c931..735629440b 100644 --- a/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessor.test.ts @@ -90,12 +90,11 @@ it('makes no requests until it is started', () => { const requests = makeRequests(); // eslint-disable-next-line no-new new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, (_flags) => {}, (_error) => {}, ); @@ -107,12 +106,11 @@ it('polls immediately when started', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, (_flags) => {}, (_error) => {}, ); @@ -128,12 +126,11 @@ it('calls callback on success', async () => { const errorCallback = jest.fn(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, dataCallback, errorCallback, ); @@ -150,12 +147,11 @@ it('polls repeatedly', async () => { requests.fetch = mockFetch('{ "flagA": true }', 200); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig({ pollInterval: 0.1 }), + {}, dataCallback, errorCallback, ); @@ -189,12 +185,11 @@ it('stops polling when stopped', (done) => { const errorCallback = jest.fn(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/stops', [], makeConfig({ pollInterval: 0.01 }), + {}, dataCallback, errorCallback, ); @@ -212,15 +207,14 @@ it('includes the correct headers on requests', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo({ - userAgentBase: 'AnSDK', - version: '42', - }), '/polling', [], makeConfig(), + { + authorization: 'the-sdk-key', + 'user-agent': 'AnSDK/42', + }, (_flags) => {}, (_error) => {}, ); @@ -242,12 +236,11 @@ it('defaults to using the "GET" verb', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig(), + {}, (_flags) => {}, (_error) => {}, ); @@ -266,12 +259,11 @@ it('can be configured to use the "REPORT" verb', () => { const requests = makeRequests(); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], makeConfig({ useReport: true }), + {}, (_flags) => {}, (_error) => {}, ); @@ -293,12 +285,11 @@ it('continues polling after receiving bad JSON', async () => { const config = makeConfig({ pollInterval: 0.1 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); @@ -322,12 +313,11 @@ it('continues polling after an exception thrown during a request', async () => { const config = makeConfig({ pollInterval: 0.1 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); @@ -354,12 +344,11 @@ it('can handle recoverable http errors', async () => { const config = makeConfig({ pollInterval: 0.1 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); @@ -384,12 +373,11 @@ it('stops polling on unrecoverable error codes', (done) => { const config = makeConfig({ pollInterval: 0.01 }); const polling = new PollingProcessor( - 'the-sdk-key', requests, - makeInfo(), '/polling', [], config, + {}, dataCallback, errorCallback, ); diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index beac5c1c03..d1c0ac331a 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -3,11 +3,13 @@ import { ClientContext, clone, Context, + defaultHeaders, internal, LDClientError, LDContext, LDFlagSet, LDFlagValue, + LDHeaders, LDLogger, Platform, ProcessStreamResponse, @@ -60,6 +62,7 @@ export default class LDClientImpl implements LDClient { private eventSendingEnabled: boolean = true; private networkAvailable: boolean = true; private connectionMode: ConnectionMode; + private baseHeaders: LDHeaders; /** * Creates the client object synchronously. No async, no network calls. @@ -109,6 +112,14 @@ export default class LDClientImpl implements LDClient { const ldContext = Context.toLDContext(context); this.emitter.emit('change', ldContext, flagKeys); }); + + this.baseHeaders = defaultHeaders( + this.sdkKey, + this.platform.info, + this.config.tags, + true, + 'x-launchdarkly-user-agent', + ); } /** @@ -407,12 +418,11 @@ export default class LDClientImpl implements LDClient { } this.updateProcessor = new PollingProcessor( - this.sdkKey, this.clientContext.platform.requests, - this.clientContext.platform.info, this.createPollUriPath(context), parameters, this.config, + this.baseHeaders, async (flags) => { this.logger.debug(`Handling polling result: ${Object.keys(flags)}`); @@ -446,11 +456,11 @@ export default class LDClientImpl implements LDClient { } this.updateProcessor = new internal.StreamingProcessor( - this.sdkKey, this.clientContext, this.createStreamUriPath(context), parameters, this.createStreamListeners(checkedContext, identifyResolve), + this.baseHeaders, this.diagnosticsManager, (e) => { identifyReject(e); diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index 55d87f879e..cef4678c93 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -64,6 +64,8 @@ export default class Configuration { public readonly pollInterval: number = DEFAULT_POLLING_INTERVAL; + public readonly userAgentHeaderName: 'user-agent' | 'x-launchdarkly-user-agent'; + // Allow indexing Configuration by a string [index: string]: any; @@ -81,6 +83,7 @@ export default class Configuration { pristineOptions.payloadFilterKey, ); this.tags = new ApplicationTags({ application: this.applicationInfo, logger: this.logger }); + this.userAgentHeaderName = internalOptions.userAgentHeaderName ?? 'user-agent'; } validateTypesAndNames(pristineOptions: LDOptions): string[] { diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 2b95f27fd2..05086685ca 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -1,10 +1,12 @@ import { ApplicationTags, + defaultHeaders, getPollingUri, httpErrorMessage, HttpErrorResponse, Info, isHttpRecoverable, + LDHeaders, LDLogger, LDPollingError, Requests, @@ -45,12 +47,11 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { private requestor: Requestor; constructor( - sdkKey: string, requests: Requests, - info: Info, uriPath: string, parameters: { key: string; value: string }[], config: PollingConfig, + baseHeaders: LDHeaders, private readonly dataHandler: (flags: Flags) => void, private readonly errorHandler?: PollingErrorHandler, ) { @@ -58,7 +59,7 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { this.logger = config.logger; this.pollInterval = config.pollInterval; - this.requestor = new Requestor(sdkKey, requests, info, uri, config.useReport, config.tags); + this.requestor = new Requestor(requests, uri, config.useReport, baseHeaders); } private async poll() { diff --git a/packages/shared/sdk-client/src/polling/Requestor.ts b/packages/shared/sdk-client/src/polling/Requestor.ts index 6a46dfcff2..bdbe449c46 100644 --- a/packages/shared/sdk-client/src/polling/Requestor.ts +++ b/packages/shared/sdk-client/src/polling/Requestor.ts @@ -4,6 +4,7 @@ import { defaultHeaders, HttpErrorResponse, Info, + LDHeaders, Requests, } from '@launchdarkly/js-sdk-common'; @@ -32,14 +33,12 @@ export default class Requestor { private verb: string; constructor( - sdkKey: string, private requests: Requests, - info: Info, private readonly uri: string, useReport: boolean, - tags: ApplicationTags, + baseHeaders: LDHeaders, ) { - this.headers = defaultHeaders(sdkKey, info, tags); + this.headers = { ...baseHeaders }; this.verb = useReport ? 'REPORT' : 'GET'; } diff --git a/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts b/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts index 46a0640ac8..77b549b992 100644 --- a/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts +++ b/packages/shared/sdk-server/__tests__/data_sources/Requestor.test.ts @@ -80,12 +80,9 @@ describe('given a requestor', () => { }, }; - requestor = new Requestor( - 'sdkKey', - new Configuration({}), - createBasicPlatform().info, - requests, - ); + requestor = new Requestor(new Configuration({}), requests, { + authorization: 'sdkKey', + }); }); it('gets data', (done) => { diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index ef6c85840f..5f2b177640 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -3,6 +3,7 @@ import { cancelableTimedPromise, ClientContext, Context, + defaultHeaders, internal, LDClientError, LDContext, @@ -207,6 +208,7 @@ export default class LDClientImpl implements LDClient { }, }; this.evaluator = new Evaluator(this.platform, queries); + const baseHeaders = defaultHeaders(sdkKey, platform.info, config.tags); const listeners = createStreamListeners(dataSourceUpdates, this.logger, { put: () => this.initSuccess(), @@ -214,18 +216,18 @@ export default class LDClientImpl implements LDClient { const makeDefaultProcessor = () => config.stream ? new internal.StreamingProcessor( - sdkKey, clientContext, '/all', [], listeners, + baseHeaders, this.diagnosticsManager, (e) => this.dataSourceErrorHandler(e), this.config.streamInitialReconnectDelay, ) : new PollingProcessor( config, - new Requestor(sdkKey, config, this.platform.info, this.platform.requests), + new Requestor(config, this.platform.requests, baseHeaders), dataSourceUpdates, () => this.initSuccess(), (e) => this.dataSourceErrorHandler(e), diff --git a/packages/shared/sdk-server/src/data_sources/Requestor.ts b/packages/shared/sdk-server/src/data_sources/Requestor.ts index d6498c6047..235e46f737 100644 --- a/packages/shared/sdk-server/src/data_sources/Requestor.ts +++ b/packages/shared/sdk-server/src/data_sources/Requestor.ts @@ -2,6 +2,7 @@ import { defaultHeaders, getPollingUri, Info, + LDHeaders, LDStreamingError, Options, Requests, @@ -28,12 +29,11 @@ export default class Requestor implements LDFeatureRequestor { > = {}; constructor( - sdkKey: string, config: Configuration, - info: Info, private readonly requests: Requests, + baseHeaders: LDHeaders, ) { - this.headers = defaultHeaders(sdkKey, info, config.tags); + this.headers = { ...baseHeaders }; this.uri = getPollingUri(config.serviceEndpoints, '/sdk/latest-all', []); }