Skip to content

Commit

Permalink
prepare 2.7.0 release (#114)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly authored Sep 25, 2018
1 parent 3160bd4 commit 1bf9087
Show file tree
Hide file tree
Showing 11 changed files with 273 additions and 54 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@
All notable changes to the LaunchDarkly client-side JavaScript SDK will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org).

## [2.7.0] - 2018-09-19
### Added:
- New client method `waitForInitialization` returns a Promise, like `waitUntilReady`; but while `waitUntilReady` will be resolved as soon as client initialization either succeeds or fails, `waitForInitialization` will be resolved only if initialization succeeds, and will be rejected (with an error object) if it fails.
- New config option `fetchGoals` (default: true) allows you to control whether the client will request A/B testing parameters from LaunchDarkly. If you do not use A/B testing, you may wish to disable this to reduce the number of HTTP requests.
- New config option `sendLDHeaders` (default: true) allows you to control whether the client will add a custom header to LaunchDarkly HTTP requests (to indicate the SDK version). You may wish to disable this behavior if you have performance concerns, as it causes browsers to make an additional CORS preflight check (since it is no longer a [simple request](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)).

## [2.6.0] - 2018-09-07
### Added:
- The new configuration option `evaluationReasons` causes LaunchDarkly to report information about how each feature flag value was determined; you can access this information with the new client method `variationDetail`. The new method returns an object that contains both the flag value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error.
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ldclient-js",
"version": "2.6.0",
"version": "2.7.0",
"description": "LaunchDarkly SDK for JavaScript",
"author": "LaunchDarkly <[email protected]>",
"license": "Apache-2.0",
Expand Down
11 changes: 9 additions & 2 deletions src/EventProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,16 @@ import * as errors from './errors';
import * as messages from './messages';
import * as utils from './utils';

export default function EventProcessor(eventsUrl, environmentId, options = {}, emitter = null, sender = null) {
export default function EventProcessor(
eventsUrl,
environmentId,
options = {},
emitter = null,
sender = null,
sendLDHeaders
) {
const processor = {};
const eventSender = sender || EventSender(eventsUrl, environmentId);
const eventSender = sender || EventSender(eventsUrl, environmentId, sendLDHeaders);
const summarizer = EventSummarizer();
const userFilter = UserFilter(options);
const inlineUsers = !!options.inlineUsersInEvents;
Expand Down
6 changes: 4 additions & 2 deletions src/EventSender.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as utils from './utils';

const MAX_URL_LENGTH = 2000;

export default function EventSender(eventsUrl, environmentId, forceHasCors, imageCreator) {
export default function EventSender(eventsUrl, environmentId, forceHasCors, imageCreator, sendLDHeaders = true) {
let hasCors;
const postUrl = eventsUrl + '/events/bulk/' + environmentId;
const imageUrl = eventsUrl + '/a/' + environmentId + '.gif';
Expand Down Expand Up @@ -36,7 +36,9 @@ export default function EventSender(eventsUrl, environmentId, forceHasCors, imag
function createRequest(canRetry) {
const xhr = new XMLHttpRequest();
xhr.open('POST', postUrl, !sync);
utils.addLDHeaders(xhr);
if (sendLDHeaders) {
utils.addLDHeaders(xhr);
}
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-LaunchDarkly-Event-Schema', '3');
if (!sync) {
Expand Down
18 changes: 11 additions & 7 deletions src/Requestor.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import * as messages from './messages';

const json = 'application/json';

function fetchJSON(endpoint, body, callback) {
function fetchJSON(endpoint, body, callback, sendLDHeaders) {
const xhr = new XMLHttpRequest();
let data = undefined;

xhr.addEventListener('load', () => {
if (
Expand All @@ -26,14 +27,17 @@ function fetchJSON(endpoint, body, callback) {
if (body) {
xhr.open('REPORT', endpoint);
xhr.setRequestHeader('Content-Type', 'application/json');
utils.addLDHeaders(xhr);
xhr.send(JSON.stringify(body));
data = JSON.stringify(body);
} else {
xhr.open('GET', endpoint);
}

if (sendLDHeaders) {
utils.addLDHeaders(xhr);
xhr.send();
}

xhr.send(data);

return xhr;
}

Expand All @@ -45,7 +49,7 @@ function getResponseError(xhr) {
}
}

export default function Requestor(baseUrl, environment, useReport, withReasons) {
export default function Requestor(baseUrl, environment, useReport, withReasons, sendLDHeaders) {
let flagSettingsRequest;
let lastFlagSettingsCallback;

Expand Down Expand Up @@ -94,12 +98,12 @@ export default function Requestor(baseUrl, environment, useReport, withReasons)
}

lastFlagSettingsCallback = cb;
flagSettingsRequest = fetchJSON(endpoint, body, cb);
flagSettingsRequest = fetchJSON(endpoint, body, cb, sendLDHeaders);
};

requestor.fetchGoals = function(callback) {
const endpoint = [baseUrl, '/sdk/goals/', environment].join('');
fetchJSON(endpoint, null, callback);
fetchJSON(endpoint, null, callback, sendLDHeaders);
};

return requestor;
Expand Down
13 changes: 12 additions & 1 deletion src/__tests__/EventSender-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,25 @@ describe('EventSender', () => {
expect(JSON.parse(r.requestBody)).toEqual(events);
});

it('should send custom user-agent header', () => {
it('should send custom user-agent header when sendLDHeaders is true', () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'identify', key: 'userKey' };
sender.sendEvents([event], false);
lastRequest().respond();
expect(lastRequest().requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString());
});

it('should not send custom user-agent header when sendLDHeaders is false', () => {
const forceHasCors = true;
const imageCreator = undefined;
const sendLDHeaders = false;
const sender = EventSender(eventsUrl, envId, forceHasCors, imageCreator, sendLDHeaders);
const event = { kind: 'identify', key: 'userKey' };
sender.sendEvents([event], false);
lastRequest().respond();
expect(lastRequest().requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(undefined);
});

const retryableStatuses = [400, 408, 429, 500, 503];
for (const i in retryableStatuses) {
const status = retryableStatuses[i];
Expand Down
81 changes: 77 additions & 4 deletions src/__tests__/LDClient-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,20 @@ describe('LDClient', () => {
}, 0);
});

it('should trigger the initialized event', done => {
const handleReady = jest.fn();
const client = LDClient.initialize(envName, user, {
bootstrap: {},
});

client.on('initialized', handleReady);

setTimeout(() => {
expect(handleReady).toHaveBeenCalled();
done();
}, 0);
});

it('should emit an error when an invalid samplingInterval is specified', done => {
const client = LDClient.initialize(envName, user, {
bootstrap: {},
Expand Down Expand Up @@ -82,6 +96,17 @@ describe('LDClient', () => {
expect(err.message).toEqual('Error fetching flag settings: ' + messages.environmentNotFound());
done();
});
client.waitForInitialization().catch(() => {}); // jest doesn't like unhandled rejections
requests[0].respond(404);
});

it('should emit a failure event when an invalid environment key is specified', done => {
const client = LDClient.initialize('abc', user);
client.on('failed', err => {
expect(err.message).toEqual('Error fetching flag settings: ' + messages.environmentNotFound());
done();
});
client.waitForInitialization().catch(() => {});
requests[0].respond(404);
});

Expand All @@ -91,6 +116,7 @@ describe('LDClient', () => {
expect(client.variation('flag-key', 1)).toEqual(1);
done();
});
client.waitForInitialization().catch(() => {});
requests[0].respond(404);
});

Expand All @@ -114,6 +140,21 @@ describe('LDClient', () => {
expect(/sdk\/eval/.test(requests[0].url)).toEqual(false); // it's the goals request
});

it('fetches goals if fetchGoals is unspecified', () => {
LDClient.initialize(envName, user);
expect(/sdk\/goals/.test(requests[1].url)).toEqual(true);
});

it('fetches goals if fetchGoals is true', () => {
LDClient.initialize(envName, user, { fetchGoals: true });
expect(/sdk\/goals/.test(requests[1].url)).toEqual(true);
});

it('does not fetch goals if fetchGoals is false', () => {
LDClient.initialize(envName, user, { fetchGoals: false });
expect(requests.length).toEqual(1);
});

it('logs warning when bootstrap object uses old format', () => {
LDClient.initialize(envName, user, {
bootstrap: { foo: 'bar' },
Expand Down Expand Up @@ -167,10 +208,13 @@ describe('LDClient', () => {
bootstrap: 'localstorage',
});

client.on('ready', () => {
expect(window.localStorage.getItem(lsKey)).toEqual(json);
done();
});
client
.waitForInitialization()
.then(() => {
expect(window.localStorage.getItem(lsKey)).toEqual(json);
done();
})
.catch(() => {});
});

it('should start with empty flags if we tried to use cached settings and there are none', done => {
Expand Down Expand Up @@ -349,6 +393,8 @@ describe('LDClient', () => {
client.on('error', handleError);
server.respond();

client.waitForInitialization().catch(() => {});

setTimeout(() => {
expect(handleError).toHaveBeenCalled();
done();
Expand Down Expand Up @@ -422,6 +468,33 @@ describe('LDClient', () => {
});
});

describe('waitForInitialization', () => {
it('resolves promise on successful init', done => {
const handleReady = jest.fn();
const client = LDClient.initialize(envName, user, {
bootstrap: {},
});

client.waitForInitialization().then(handleReady);

client.on('ready', () => {
setTimeout(() => {
expect(handleReady).toHaveBeenCalled();
done();
}, 0);
});
});

it('rejects promise if flags request fails', done => {
const client = LDClient.initialize('abc', user);
client.waitForInitialization().catch(err => {
expect(err.message).toEqual('Error fetching flag settings: ' + messages.environmentNotFound());
done();
});
requests[0].respond(404);
});
});

describe('variation', () => {
it('returns value for an existing flag - from bootstrap', () => {
const client = LDClient.initialize(envName, user, {
Expand Down
30 changes: 26 additions & 4 deletions src/__tests__/Requestor-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,21 +169,43 @@ describe('Requestor', () => {
expect(handleFive.calledOnce).toEqual(true);
});

it('should send custom user-agent header in GET mode', () => {
const requestor = Requestor('http://requestee', 'FAKE_ENV', false);
it('should send custom user-agent header in GET mode when sendLDHeaders is true', () => {
const useReport = false;
const withReasons = false;
const sendLDHeaders = true;
const requestor = Requestor('http://requestee', 'FAKE_ENV', useReport, withReasons, sendLDHeaders);
const user = { key: 'foo' };
requestor.fetchFlagSettings(user, 'hash1', sinon.spy());

expect(server.requests.length).toEqual(1);
expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString());
});

it('should send custom user-agent header in REPORT mode', () => {
const requestor = Requestor('http://requestee', 'FAKE_ENV', true);
it('should send custom user-agent header in REPORT mode when sendLDHeaders is true', () => {
const useReport = true;
const withReasons = false;
const sendLDHeaders = true;
const requestor = Requestor('http://requestee', 'FAKE_ENV', useReport, withReasons, sendLDHeaders);
const user = { key: 'foo' };
requestor.fetchFlagSettings(user, 'hash1', sinon.spy());

expect(server.requests.length).toEqual(1);
expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString());
});

it('should NOT send custom user-agent header when sendLDHeaders is false', () => {
const baseUrl = 'http://requestee';
const environment = 'FAKE_ENV';
const useReport = true;
const withReasons = false;
const sendLDHeaders = false;

const requestor = Requestor(baseUrl, environment, useReport, withReasons, sendLDHeaders);
const user = { key: 'foo' };

requestor.fetchFlagSettings(user, 'hash1', sinon.spy());

expect(server.requests.length).toEqual(1);
expect(server.requests[0].requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(undefined);
});
});
Loading

0 comments on commit 1bf9087

Please sign in to comment.