diff --git a/packages/launchdarkly-js-client-sdk/jest.config.js b/packages/launchdarkly-js-client-sdk/jest.config.js index b091646b..aa175c59 100644 --- a/packages/launchdarkly-js-client-sdk/jest.config.js +++ b/packages/launchdarkly-js-client-sdk/jest.config.js @@ -13,4 +13,5 @@ module.exports = { window: true, VERSION: version, }, + testURL: 'https://mydomain.com/some/path', }; diff --git a/packages/launchdarkly-js-client-sdk/src/GoalManager.js b/packages/launchdarkly-js-client-sdk/src/GoalManager.js index 264f0f9c..2edee09d 100644 --- a/packages/launchdarkly-js-client-sdk/src/GoalManager.js +++ b/packages/launchdarkly-js-client-sdk/src/GoalManager.js @@ -21,6 +21,10 @@ export default function GoalManager(clientVars, readyCallback) { return false; }; + function getGoalsPath() { + return '/sdk/goals/' + clientVars.getEnvironmentId(); + } + function refreshGoalTracker() { if (goalTracker) { goalTracker.dispose(); @@ -77,7 +81,7 @@ export default function GoalManager(clientVars, readyCallback) { } clientVars.requestor - .fetchGoals() + .fetchJSON(getGoalsPath()) .then(g => { if (g && g.length > 0) { goals = g; diff --git a/packages/launchdarkly-js-client-sdk/src/__tests__/browserPlatform-test.js b/packages/launchdarkly-js-client-sdk/src/__tests__/browserPlatform-test.js index 2571ab48..795cf3e1 100644 --- a/packages/launchdarkly-js-client-sdk/src/__tests__/browserPlatform-test.js +++ b/packages/launchdarkly-js-client-sdk/src/__tests__/browserPlatform-test.js @@ -88,8 +88,15 @@ describe('browserPlatform', () => { }); describe('getCurrentUrl()', () => { + const expectedUrl = 'https://mydomain.com/some/path'; // this is set in jest.config.js + it('returns value of window.location.href', () => { - expect(platform.getCurrentUrl()).toEqual(window.location.href); + expect(platform.getCurrentUrl()).toEqual(expectedUrl); + }); + + it('calls URL transformer if specified', () => { + const p = browserPlatform({ eventUrlTransformer: url => url + '/x' }); + expect(p.getCurrentUrl()).toEqual(expectedUrl + '/x'); }); }); diff --git a/packages/launchdarkly-js-client-sdk/src/browserPlatform.js b/packages/launchdarkly-js-client-sdk/src/browserPlatform.js index 5f8ee22f..b53ab2d2 100644 --- a/packages/launchdarkly-js-client-sdk/src/browserPlatform.js +++ b/packages/launchdarkly-js-client-sdk/src/browserPlatform.js @@ -1,6 +1,6 @@ import newHttpRequest from './httpRequest'; -export default function makeBrowserPlatform() { +export default function makeBrowserPlatform(options) { const ret = {}; ret.pageIsClosing = false; // this will be set to true by index.js if the page is closing @@ -19,7 +19,8 @@ export default function makeBrowserPlatform() { return hasCors; }; - ret.getCurrentUrl = () => window.location.href; + const eventUrlTransformer = options && options.eventUrlTransformer; + ret.getCurrentUrl = () => (eventUrlTransformer ? eventUrlTransformer(window.location.href) : window.location.href); ret.isDoNotTrack = () => { let flag; diff --git a/packages/launchdarkly-js-client-sdk/test-types.ts b/packages/launchdarkly-js-client-sdk/test-types.ts index 4dad93c7..7285905c 100644 --- a/packages/launchdarkly-js-client-sdk/test-types.ts +++ b/packages/launchdarkly-js-client-sdk/test-types.ts @@ -27,6 +27,7 @@ var allOptions: ld.LDOptions = { flushInterval: 1, samplingInterval: 1, streamReconnectDelay: 1, + eventUrlTransformer: url => url + 'x', logger: logger }; var userWithKeyOnly: ld.LDUser = { key: 'user' }; diff --git a/packages/launchdarkly-js-client-sdk/typings.d.ts b/packages/launchdarkly-js-client-sdk/typings.d.ts index ab970c1f..df233999 100644 --- a/packages/launchdarkly-js-client-sdk/typings.d.ts +++ b/packages/launchdarkly-js-client-sdk/typings.d.ts @@ -72,6 +72,13 @@ declare module 'launchdarkly-js-client-sdk' { * Set it to false if you are not using A/B testing and want to skip the request. */ fetchGoals?: boolean; + + /** + * A function which, if present, can change the URL in analytics events to something other + * than the actual browser URL. It will be called with the current browser URL as a parameter, + * and returns the value that should be stored in the event's `url` property. + */ + eventUrlTransformer?: (url: string) => string; } /** diff --git a/packages/launchdarkly-js-sdk-common/src/Requestor.js b/packages/launchdarkly-js-sdk-common/src/Requestor.js index a4b8c6a7..159f846d 100644 --- a/packages/launchdarkly-js-sdk-common/src/Requestor.js +++ b/packages/launchdarkly-js-sdk-common/src/Requestor.js @@ -48,12 +48,13 @@ export default function Requestor(platform, options, environment, logger) { const req = platform.httpRequest(method, endpoint, headers, body); const p = req.promise.then( result => { - if ( - result.status === 200 && - result.header('content-type') && - result.header('content-type').lastIndexOf(json) === 0 - ) { - return JSON.parse(result.body); + if (result.status === 200) { + if (result.header('content-type') && result.header('content-type').lastIndexOf(json) === 0) { + return JSON.parse(result.body); + } else { + const message = messages.invalidContentType(result.header('content-type') || ''); + return Promise.reject(new errors.LDFlagFetchError(message)); + } } else { return Promise.reject(getResponseError(result)); } @@ -67,8 +68,14 @@ export default function Requestor(platform, options, environment, logger) { return coalescer.resultPromise; } - // Returns a Promise which will resolve with the parsed JSON response, or will be - // rejected if the request failed. + // Performs a GET request to an arbitrary path under baseUrl. Returns a Promise which will resolve + // with the parsed JSON response, or will be rejected if the request failed. + requestor.fetchJSON = function(path) { + return fetchJSON(baseUrl + path, null); + }; + + // Requests the current state of all flags for the given user from LaunchDarkly. Returns a Promise + // which will resolve with the parsed JSON response, or will be rejected if the request failed. requestor.fetchFlagSettings = function(user, hash) { let data; let endpoint; @@ -94,12 +101,5 @@ export default function Requestor(platform, options, environment, logger) { return fetchJSON(endpoint, body); }; - // Returns a Promise which will resolve with the parsed JSON response, or will be - // rejected if the request failed. - requestor.fetchGoals = function() { - const endpoint = [baseUrl, '/sdk/goals/', environment].join(''); - return fetchJSON(endpoint, null); - }; - return requestor; } diff --git a/packages/launchdarkly-js-sdk-common/src/__mocks__/Requestor.js b/packages/launchdarkly-js-sdk-common/src/__mocks__/Requestor.js index 436c284c..b4110396 100644 --- a/packages/launchdarkly-js-sdk-common/src/__mocks__/Requestor.js +++ b/packages/launchdarkly-js-sdk-common/src/__mocks__/Requestor.js @@ -1,4 +1,3 @@ export default () => ({ fetchFlagSettings: jest.fn().mockImplementation((user, hash, callback) => callback(null, {})), - fetchGoals: jest.fn().mockImplementation(callback => callback(null, {})), }); diff --git a/packages/launchdarkly-js-sdk-common/src/__tests__/LDClient-localstorage-test.js b/packages/launchdarkly-js-sdk-common/src/__tests__/LDClient-localstorage-test.js index d17ce56e..4b0da643 100644 --- a/packages/launchdarkly-js-sdk-common/src/__tests__/LDClient-localstorage-test.js +++ b/packages/launchdarkly-js-sdk-common/src/__tests__/LDClient-localstorage-test.js @@ -22,7 +22,7 @@ describe('LDClient local storage', () => { const platform = stubPlatform.defaults(); platform.localStorage = null; - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); await client.waitForInitialization(); // should see a flag request to the server right away, as if bootstrap was not specified @@ -36,7 +36,7 @@ describe('LDClient local storage', () => { const json = '{"flag-key": 1}'; platform.testing.setLocalStorageImmediately(lsKey, json); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); await client.waitForInitialization(); expect(client.variation('flag-key')).toEqual(1); @@ -47,7 +47,7 @@ describe('LDClient local storage', () => { const platform = stubPlatform.defaults(); server.respondWith(jsonResponse({ 'flag-key': { value: 1 } })); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); // don't wait for ready event - verifying that variation() doesn't throw an error if called before ready expect(client.variation('flag-key', 0)).toEqual(0); @@ -62,7 +62,7 @@ describe('LDClient local storage', () => { platform.localStorage.get = () => Promise.reject(new Error()); server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); await client.waitForInitialization(); expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); @@ -73,7 +73,7 @@ describe('LDClient local storage', () => { platform.localStorage.set = () => Promise.reject(new Error()); server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); await client.waitForInitialization(); await asyncSleep(0); // allow any pending async tasks to complete @@ -87,7 +87,7 @@ describe('LDClient local storage', () => { server.respondWith(errorResponse(503)); platform.testing.setLocalStorageImmediately(lsKey, json); - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); await client.waitForInitialization(); await asyncSleep(0); // allow any pending async tasks to complete @@ -103,7 +103,6 @@ describe('LDClient local storage', () => { const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', hash: 'totallyLegitHash', - fetchGoals: false, }); await client.waitForInitialization(); @@ -119,7 +118,7 @@ describe('LDClient local storage', () => { const lsKey2 = 'ld:UNKNOWN_ENVIRONMENT_ID:' + utils.btoa('{"key":"user2"}'); const user2 = { key: 'user2' }; - const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + const client = platform.testing.makeClient(envName, user, { bootstrap: 'localstorage' }); server.respondWith(jsonResponse({ 'enable-foo': { value: true } })); diff --git a/packages/launchdarkly-js-sdk-common/src/__tests__/Requestor-test.js b/packages/launchdarkly-js-sdk-common/src/__tests__/Requestor-test.js index 858c0624..918b34e4 100644 --- a/packages/launchdarkly-js-sdk-common/src/__tests__/Requestor-test.js +++ b/packages/launchdarkly-js-sdk-common/src/__tests__/Requestor-test.js @@ -172,6 +172,24 @@ describe('Requestor', () => { expect(result).toEqual(data); }); + it('returns error for non-JSON content type', async () => { + const requestor = Requestor(platform, defaultConfig, env, logger); + + server.respondWith([200, { 'Content-Type': 'text/html' }, '']); + + const err = new errors.LDFlagFetchError(messages.invalidContentType('text/html')); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); + + it('returns error for unspecified content type', async () => { + const requestor = Requestor(platform, defaultConfig, env, logger); + + server.respondWith([200, {}, '{}']); + + const err = new errors.LDFlagFetchError(messages.invalidContentType('')); + await expect(requestor.fetchFlagSettings(user)).rejects.toThrow(err); + }); + it('signals specific error for 404 response', async () => { const requestor = Requestor(platform, defaultConfig, env, logger); diff --git a/packages/launchdarkly-js-sdk-common/src/index.js b/packages/launchdarkly-js-sdk-common/src/index.js index 96573718..2c68cf9b 100644 --- a/packages/launchdarkly-js-sdk-common/src/index.js +++ b/packages/launchdarkly-js-sdk-common/src/index.js @@ -671,6 +671,7 @@ export function initialize(env, user, specifiedOptions, platform, extraDefaults) start: start, // Starts the client once the environment is ready. enqueueEvent: enqueueEvent, // Puts an analytics event in the queue, if event sending is enabled. getFlagsInternal: getFlagsInternal, // Returns flag data structure with all details. + getEnvironmentId: () => environment, // Gets the environment ID (this may have changed since initialization, if we have a state provider) internalChangeEventName: internalChangeEvent, // This event is triggered whenever we have new flag state. }; } diff --git a/packages/launchdarkly-js-sdk-common/src/messages.js b/packages/launchdarkly-js-sdk-common/src/messages.js index 973a206d..5a2bb02f 100644 --- a/packages/launchdarkly-js-sdk-common/src/messages.js +++ b/packages/launchdarkly-js-sdk-common/src/messages.js @@ -15,6 +15,10 @@ export const eventWithoutUser = function() { return 'Be sure to call `identify` in the LaunchDarkly client: http://docs.launchdarkly.com/docs/running-an-ab-test#include-the-client-side-snippet'; }; +export const invalidContentType = function(contentType) { + return 'Expected application/json content type but got "' + contentType + '"'; +}; + export const invalidKey = function() { return 'Event key must be a string'; };