diff --git a/docs/typedoc.js b/docs/typedoc.js deleted file mode 100644 index 254c9ee..0000000 --- a/docs/typedoc.js +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - out: './docs/build/html', - exclude: [ - '**/node_modules/**', - 'test-types.ts' - ], - name: "LaunchDarkly Javascript SDK Core Components (3.3.4)", - readme: 'none', // don't add a home page with a copy of README.md - mode: 'file', // don't treat "index.d.ts" itself as a parent module - includeDeclarations: true, // allows it to process a .d.ts file instead of actual TS code - entryPoint: '"launchdarkly-js-sdk-common"' // note extra quotes - workaround for a TypeDoc bug -}; diff --git a/src/EventSender.js b/src/EventSender.js index 6a0a2e3..a1cfb7e 100644 --- a/src/EventSender.js +++ b/src/EventSender.js @@ -34,7 +34,7 @@ export default function EventSender(platform, environmentId, options) { 'X-LaunchDarkly-Payload-ID': payloadId, }); return platform - .httpRequest('POST', url, headers, jsonBody) + .httpRequest('POST', url, utils.transformHeaders(headers, options), jsonBody) .promise.then(result => { if (!result) { // This was a response from a fire-and-forget request, so we won't have a status. diff --git a/src/Requestor.js b/src/Requestor.js index f692906..f4db4be 100644 --- a/src/Requestor.js +++ b/src/Requestor.js @@ -45,7 +45,7 @@ export default function Requestor(platform, options, environment) { activeRequests[endpoint] = coalescer; } - const req = platform.httpRequest(method, endpoint, headers, body); + const req = platform.httpRequest(method, endpoint, utils.transformHeaders(headers, options), body); const p = req.promise.then( result => { if (result.status === 200) { diff --git a/src/Stream.js b/src/Stream.js index 70a9862..9f7b727 100644 --- a/src/Stream.js +++ b/src/Stream.js @@ -1,5 +1,5 @@ import * as messages from './messages'; -import { base64URLEncode, getLDHeaders, objectHasOwnProperty } from './utils'; +import { base64URLEncode, getLDHeaders, transformHeaders, objectHasOwnProperty } from './utils'; // The underlying event source implementation is abstracted via the platform object, which should // have these three properties: @@ -104,6 +104,7 @@ export default function Stream(platform, config, environment, diagnosticsAccumul } else { url = evalUrlPrefix + '/' + base64URLEncode(JSON.stringify(user)); } + options.headers = transformHeaders(options.headers, config); if (withReasons) { query = query + (query ? '&' : '') + 'withReasons=true'; } diff --git a/src/__tests__/EventSender-test.js b/src/__tests__/EventSender-test.js index fe9ab35..9bda1f4 100644 --- a/src/__tests__/EventSender-test.js +++ b/src/__tests__/EventSender-test.js @@ -149,6 +149,23 @@ describe('EventSender', () => { expect(r.headers['x-launchdarkly-wrapper']).toEqual('FakeSDK'); }); + it('should send transformed headers if requestHeaderTransform function is provided', async () => { + const headerTransform = input => { + const output = { ...input }; + output['c'] = '30'; + return output; + }; + const options = { requestHeaderTransform: headerTransform }; + const server = platform.testing.http.newServer(); + server.byDefault(respond(202)); + const sender = EventSender(platform, envId, options); + const event = { kind: 'identify', key: 'userKey' }; + await sender.sendEvents([event], server.url); + + const r = await server.nextRequest(); + expect(r.headers['c']).toEqual('30'); + }); + describe('retry on recoverable HTTP error', () => { const retryableStatuses = [400, 408, 429, 500, 503]; for (const i in retryableStatuses) { diff --git a/src/__tests__/Requestor-test.js b/src/__tests__/Requestor-test.js index e9c6db1..474515d 100644 --- a/src/__tests__/Requestor-test.js +++ b/src/__tests__/Requestor-test.js @@ -227,6 +227,24 @@ describe('Requestor', () => { }); }); + it('sends transformed headers if requestHeaderTransform function is provided', async () => { + await withServer(async (baseConfig, server) => { + const headerTransform = input => { + const output = { ...input }; + output['b'] = '20'; + return output; + }; + const config = { ...baseConfig, requestHeaderTransform: headerTransform }; + const requestor = Requestor(platform, config, env); + + await requestor.fetchFlagSettings(user); + + expect(server.requests.length()).toEqual(1); + const req = await server.requests.take(); + expect(req.headers['b']).toEqual('20'); + }); + }); + it('returns parsed JSON response on success', async () => { const data = { foo: 'bar' }; await withServer(async (baseConfig, server) => { diff --git a/src/__tests__/Stream-test.js b/src/__tests__/Stream-test.js index fa8a6c7..a392d7b 100644 --- a/src/__tests__/Stream-test.js +++ b/src/__tests__/Stream-test.js @@ -120,6 +120,20 @@ describe('Stream', () => { expect(created.options.headers).toEqual({}); }); + it('sends transformed headers if requestHeaderTransform function is provided', async () => { + const headerTransform = input => { + const output = { ...input }; + output['a'] = '10'; + return output; + }; + const config = { ...defaultConfig, requestHeaderTransform: headerTransform }; + const stream = new Stream(platform, config, envName); + stream.connect(user, null, {}); + + const created = await platform.testing.expectStream(makeExpectedStreamUrl(encodedUser)); + expect(created.options.headers).toEqual({ ...baseHeaders, a: '10' }); + }); + it('sets event listeners', async () => { const stream = new Stream(platform, defaultConfig, envName); const fn1 = jest.fn(); diff --git a/src/__tests__/utils-test.js b/src/__tests__/utils-test.js index f47f980..0f95b9b 100644 --- a/src/__tests__/utils-test.js +++ b/src/__tests__/utils-test.js @@ -1,6 +1,7 @@ import { base64URLEncode, getLDHeaders, + transformHeaders, getLDUserAgentString, wrapPromiseCallback, chunkUserEventsForUrl, @@ -79,6 +80,38 @@ describe('utils', () => { }); }); + describe('transformHeaders', () => { + it('does not modify the headers if the option is not available', () => { + const inputHeaders = { a: '1', b: '2' }; + const headers = transformHeaders(inputHeaders, {}); + expect(headers).toEqual(inputHeaders); + }); + + it('modifies the headers if the option has a transform', () => { + const inputHeaders = { c: '3', d: '4' }; + const outputHeaders = { c: '9', d: '4', e: '5' }; + const headerTransform = input => { + const output = { ...input }; + output['c'] = '9'; + output['e'] = '5'; + return output; + }; + const headers = transformHeaders(inputHeaders, { requestHeaderTransform: headerTransform }); + expect(headers).toEqual(outputHeaders); + }); + + it('cannot mutate the input header object', () => { + const inputHeaders = { f: '6' }; + const expectedInputHeaders = { f: '6' }; + const headerMutate = input => { + input['f'] = '7'; // eslint-disable-line no-param-reassign + return input; + }; + transformHeaders(inputHeaders, { requestHeaderTransform: headerMutate }); + expect(inputHeaders).toEqual(expectedInputHeaders); + }); + }); + describe('getLDUserAgentString', () => { it('uses platform user-agent and package version by default', () => { const platform = stubPlatform.defaults(); diff --git a/src/configuration.js b/src/configuration.js index c595ce6..d779f10 100644 --- a/src/configuration.js +++ b/src/configuration.js @@ -18,6 +18,7 @@ export const baseOptionDefs = { sendEvents: { default: true }, streaming: { type: 'boolean' }, // default for this is undefined, which is different from false sendLDHeaders: { default: true }, + requestHeaderTransform: { type: 'function' }, inlineUsersInEvents: { default: false }, allowFrequentDuplicateEvents: { default: false }, sendEventsOnlyForVariation: { default: false }, diff --git a/src/utils.js b/src/utils.js index 37f6b1a..ad9a40c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -165,6 +165,13 @@ export function getLDHeaders(platform, options) { return h; } +export function transformHeaders(headers, options) { + if (!options || !options.requestHeaderTransform) { + return headers; + } + return options.requestHeaderTransform({ ...headers }); +} + export function extend(...objects) { return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {}); } diff --git a/test-types.ts b/test-types.ts index bf63737..9c5bac5 100644 --- a/test-types.ts +++ b/test-types.ts @@ -39,6 +39,7 @@ var allBaseOptions: ld.LDOptionsBase = { streaming: true, useReport: true, sendLDHeaders: true, + requestHeaderTransform: (x) => x, evaluationReasons: true, sendEvents: true, allAttributesPrivate: true, diff --git a/typings.d.ts b/typings.d.ts index 731d283..812d3b5 100644 --- a/typings.d.ts +++ b/typings.d.ts @@ -139,6 +139,14 @@ declare module 'launchdarkly-js-sdk-common' { */ sendLDHeaders?: boolean; + /** + * A transform function for dynamic configuration of HTTP headers. + * + * This method will run last in the header generation sequence, so the function should have + * all system generated headers in case those also need to be modified. + */ + requestHeaderTransform?: (headers: Map) => Map; + /** * Whether LaunchDarkly should provide additional information about how flag values were * calculated.