Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
88727f8
retry event posts once
eli-darkly Aug 10, 2018
c7e40cf
linter
eli-darkly Aug 10, 2018
dc8fc3c
don't use jest-each, it causes strange problems in CI tests
eli-darkly Aug 10, 2018
c22d722
try to fix jest problem
eli-darkly Aug 10, 2018
5692c00
Revert "try to fix jest problem"
eli-darkly Aug 10, 2018
3f4ea95
document promise polyfill and make all polyfill sections consistent
apucacao Aug 13, 2018
d407bb9
fix typo
apucacao Aug 14, 2018
c9339ed
try to fix jdom error by setting jest url property
eli-darkly Aug 14, 2018
96a171e
bump jest version
eli-darkly Aug 14, 2018
8b23214
require earlier version of jsdom to avoid problem with mocking localS…
eli-darkly Aug 14, 2018
779bcc4
undo jest version change
eli-darkly Aug 14, 2018
4ebdb51
Merge pull request #90 from launchdarkly/eb/ch22174/jest-errors
eli-darkly Aug 14, 2018
3a90745
Merge branch 'master' into eb/ch15679/retry-events
eli-darkly Aug 14, 2018
bf35d1b
change identify() result back to a key-value map like it used to be
eli-darkly Aug 14, 2018
29c3df3
linter
eli-darkly Aug 14, 2018
22491fd
Merge pull request #91 from launchdarkly/eb/ch22186/identify-result
eli-darkly Aug 15, 2018
af2103c
Merge branch 'master' into ag/ch22054/readme-polyfills
eli-darkly Aug 15, 2018
b7a389f
Merge pull request #89 from launchdarkly/ag/ch22054/readme-polyfills
eli-darkly Aug 15, 2018
1cb4b79
Merge branch 'master' of github.com:launchdarkly/js-client
eli-darkly Aug 15, 2018
9182527
support extended bootstrap data format
eli-darkly Aug 16, 2018
e762629
linter
eli-darkly Aug 16, 2018
f2b20b2
add warning for bootstrap not having metadata
eli-darkly Aug 17, 2018
4a188b1
linter
eli-darkly Aug 17, 2018
aa78b6e
Merge pull request #92 from launchdarkly/eb/ch21387/bootstrap-metadata
eli-darkly Aug 22, 2018
2d07c91
Merge pull request #88 from launchdarkly/eb/ch15679/retry-events
eli-darkly Aug 23, 2018
b0dfc1d
add check for $valid property
eli-darkly Aug 24, 2018
0ccde84
linter
eli-darkly Aug 24, 2018
70a00c7
[ch22449] Make sure the TypeScript typings are included in our publis…
apucacao Aug 24, 2018
f4d5ff9
Merge pull request #93 from launchdarkly/eb/valid-flag
eli-darkly Aug 24, 2018
ecf7d0b
Merge branch 'all-flags-state'
eli-darkly Aug 25, 2018
bb20069
prepare 2.5.0 release
eli-darkly Aug 25, 2018
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
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.5.0] - 2018-08-27
### Changed:
- Starting in version 2.0.0, there was a problem where analytics events would not be generated correctly if you initialized the client with bootstrap data, because the bootstrap data did not include some flag metadata that the front end uses for events. The client now supports an extended format for bootstrap data that fixes this problem; this is generated by calling a new method that has been added to the server-side SDKs, `allFlagsState`/`all_flags_state` (previously `allFlags`/`all_flags`). Therefore, if you want analytics event data and you are using bootstrap data from the back end, you should upgrade both your JavaScript SDK and your server-side SDK, and use `allFlagsState` on the back end. This does not require any changes in your JavaScript code. If you use bootstrap data in the old format, the SDK will still be usable but events will not work correctly.
- When posting events to LaunchDarkly, if a request fails, it will be retried once.
- The TypeScript mappings for the SDK were omitted from the distribution in the previous release. They are now in the distribution again, in the root folder instead of in `src`, and have been renamed from `index.d.ts` to `typings.d.ts`.

## [2.4.1] - 2018-08-14
### Fixed:
- The result value of `identify()` (provided by either a promise or a callback, once the flag values for the new user have been retrieved) used to be a simple map of flag keys to values, until it was accidentally changed to an internal data structure in version 2.0.0. It is now a map of flag keys to values again, consistent with what is returned by `allFlags()`.
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.

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ldclient-js",
"version": "2.4.1",
"version": "2.5.0",
"description": "LaunchDarkly SDK for JavaScript",
"author": "LaunchDarkly <team@launchdarkly.com>",
"license": "Apache-2.0",
Expand All @@ -15,8 +15,10 @@
"ldclient.es.js",
"ldclient.es.js.map",
"ldclient.min.js",
"ldclient.min.js.map"
"ldclient.min.js.map",
"typings.d.ts"
],
"types": "./typings.d.ts",
"main": "dist/ldclient.cjs.js",
"module": "dist/ldclient.es.js",
"scripts": {
Expand All @@ -36,7 +38,6 @@
"clean": "rimraf dist/**",
"prepublishOnly": "npm run build:min"
},
"types": "./src/index.d.ts",
"devDependencies": {
"babel-core": "6.26.0",
"babel-eslint": "8.2.2",
Expand Down
24 changes: 18 additions & 6 deletions src/EventSender.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as errors from './errors';
import * as utils from './utils';

const MAX_URL_LENGTH = 2000;
Expand Down Expand Up @@ -30,23 +31,34 @@ export default function EventSender(eventsUrl, environmentId, forceHasCors, imag

function sendChunk(events, usePost, sync) {
const createImage = imageCreator || loadUrlUsingImage;
const jsonBody = JSON.stringify(events);
const send = onDone => {
if (usePost) {
function createRequest(canRetry) {
const xhr = new XMLHttpRequest();
xhr.open('POST', postUrl, !sync);
utils.addLDHeaders(xhr);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-LaunchDarkly-Event-Schema', '3');

if (!sync) {
xhr.addEventListener('load', () => {
onDone(getResponseInfo(xhr));
if (xhr.status >= 400 && errors.isHttpErrorRecoverable(xhr.status) && canRetry) {
createRequest(false).send(jsonBody);
} else {
onDone(getResponseInfo(xhr));
}
});
if (canRetry) {
xhr.addEventListener('error', () => {
createRequest(false).send(jsonBody);
});
}
}

xhr.send(JSON.stringify(events));
return xhr;
}
if (usePost) {
createRequest(true).send(jsonBody);
} else {
const src = imageUrl + '?d=' + utils.base64URLEncode(JSON.stringify(events));
const src = imageUrl + '?d=' + utils.base64URLEncode(jsonBody);
createImage(src, sync ? null : onDone);
}
};
Expand Down
48 changes: 45 additions & 3 deletions src/__tests__/EventSender-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,10 @@ describe('EventSender', () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'identify', key: 'userKey' };
sender.sendEvents([event], false);
lastRequest().respond();
expect(lastRequest().async).toEqual(true);
requests[0].respond();
expect(requests.length).toEqual(1);
expect(requests[0].async).toEqual(true);
expect(JSON.parse(requests[0].requestBody)).toEqual([event]);
});

it('should send synchronously', () => {
Expand Down Expand Up @@ -132,9 +134,49 @@ describe('EventSender', () => {
it('should send custom user-agent header', () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'identify', key: 'userKey' };
sender.sendEvents([event], true);
sender.sendEvents([event], false);
lastRequest().respond();
expect(lastRequest().requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString());
});

const retryableStatuses = [400, 408, 429, 500, 503];
for (const i in retryableStatuses) {
const status = retryableStatuses[i];
it('should retry on error ' + status, () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'false', key: 'userKey' };
sender.sendEvents([event], false);
requests[0].respond(status);
expect(requests.length).toEqual(2);
expect(JSON.parse(requests[1].requestBody)).toEqual([event]);
});
}

it('should not retry more than once', () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'false', key: 'userKey' };
sender.sendEvents([event], false);
requests[0].respond(503);
expect(requests.length).toEqual(2);
requests[1].respond(503);
expect(requests.length).toEqual(2);
});

it('should not retry on error 401', () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'false', key: 'userKey' };
sender.sendEvents([event], false);
requests[0].respond(401);
expect(requests.length).toEqual(1);
});

it('should retry on I/O error', () => {
const sender = EventSender(eventsUrl, envId, true);
const event = { kind: 'false', key: 'userKey' };
sender.sendEvents([event], false);
requests[0].error();
expect(requests.length).toEqual(2);
expect(JSON.parse(requests[1].requestBody)).toEqual([event]);
});
});
});
64 changes: 62 additions & 2 deletions src/__tests__/LDClient-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ describe('LDClient', () => {
requests[0].respond(404);
});

it('should not fetch flag settings since bootstrap is provided', () => {
it('should not fetch flag settings if bootstrap is provided', () => {
LDClient.initialize(envName, user, {
bootstrap: {},
});
Expand All @@ -167,6 +167,38 @@ describe('LDClient', () => {
expect(/sdk\/eval/.test(settingsRequest.url)).toEqual(false);
});

it('sets flag values from bootstrap object with old format', () => {
const client = LDClient.initialize(envName, user, {
bootstrap: { foo: 'bar' },
});

expect(client.variation('foo')).toEqual('bar');
});

it('logs warning when bootstrap object uses old format', () => {
LDClient.initialize(envName, user, {
bootstrap: { foo: 'bar' },
});

expect(warnSpy).toHaveBeenCalledWith(messages.bootstrapOldFormat());
});

it('sets flag values from bootstrap object with new format', () => {
const client = LDClient.initialize(envName, user, {
bootstrap: { foo: 'bar', $flagsState: { foo: { version: 1 } } },
});

expect(client.variation('foo')).toEqual('bar');
});

it('does not log warning when bootstrap object uses new format', () => {
LDClient.initialize(envName, user, {
bootstrap: { foo: 'bar', $flagsState: { foo: { version: 1 } } },
});

expect(warnSpy).not.toHaveBeenCalled();
});

it('should contain package version', () => {
// Arrange
const version = LDClient.version;
Expand Down Expand Up @@ -420,13 +452,15 @@ describe('LDClient', () => {
expect(e.user).toEqual(user);
}

function expectFeatureEvent(e, key, value, variation, version, defaultVal) {
function expectFeatureEvent(e, key, value, variation, version, defaultVal, trackEvents, debugEventsUntilDate) {
expect(e.kind).toEqual('feature');
expect(e.key).toEqual(key);
expect(e.value).toEqual(value);
expect(e.variation).toEqual(variation);
expect(e.version).toEqual(version);
expect(e.default).toEqual(defaultVal);
expect(e.trackEvents).toEqual(trackEvents);
expect(e.debugEventsUntilDate).toEqual(debugEventsUntilDate);
}

it('sends an identify event at startup', done => {
Expand Down Expand Up @@ -513,6 +547,32 @@ describe('LDClient', () => {

server.respond();
});

it('can get metadata for events from bootstrap object', done => {
const ep = stubEventProcessor();
const bootstrapData = {
foo: 'bar',
$flagsState: {
foo: {
variation: 1,
version: 2,
trackEvents: true,
debugEventsUntilDate: 1000,
},
},
};
const client = LDClient.initialize(envName, user, { eventProcessor: ep, bootstrap: bootstrapData });

client.on('ready', () => {
client.variation('foo', 'x');

expect(ep.events.length).toEqual(2);
expectIdentifyEvent(ep.events[0], user);
expectFeatureEvent(ep.events[1], 'foo', 'bar', 1, 2, 'x', true, 1000);

done();
});
});
});

describe('event listening', () => {
Expand Down
2 changes: 1 addition & 1 deletion src/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const LDFlagFetchError = createCustomError('LaunchDarklyFlagFetchError');

export function isHttpErrorRecoverable(status) {
if (status >= 400 && status < 500) {
return status === 408 || status === 429;
return status === 400 || status === 408 || status === 429;
}
return true;
}
31 changes: 30 additions & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,42 @@ export function initialize(env, user, options = {}) {
const events = options.eventProcessor || EventProcessor(eventsUrl, environment, options, emitter);
const requestor = Requestor(baseUrl, environment, options.useReport);
const seenRequests = {};
let flags = typeof options.bootstrap === 'object' ? utils.transformValuesToVersionedValues(options.bootstrap) : {};
let flags = typeof options.bootstrap === 'object' ? readFlagsFromBootstrap(options.bootstrap) : {};
let goalTracker;
let useLocalStorage;
let goals;
let subscribedToChangeEvents;
let firstEvent = true;

function readFlagsFromBootstrap(data) {
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
const keys = Object.keys(data);
const metadataKey = '$flagsState';
const validKey = '$valid';
const metadata = data[metadataKey];
if (!metadata && keys.length) {
console.warn(messages.bootstrapOldFormat());
}
if (data[validKey] === false) {
console.warn(messages.bootstrapInvalid());
}
const ret = {};
keys.forEach(key => {
if (key !== metadataKey && key !== validKey) {
let flag = { value: data[key] };
if (metadata && metadata[key]) {
flag = utils.extend(flag, metadata[key]);
} else {
flag.version = 0;
}
ret[key] = flag;
}
});
return ret;
}

function shouldEnqueueEvent() {
return sendEvents && !doNotTrack();
}
Expand Down
12 changes: 12 additions & 0 deletions src/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,18 @@ export const invalidUser = function() {
return 'Invalid user specified.' + docLink;
};

export const bootstrapOldFormat = function() {
return (
'LaunchDarkly client was initialized with bootstrap data that did not include flag metadata. ' +
'Events may not be sent correctly.' +
docLink
);
};

export const bootstrapInvalid = function() {
return 'LaunchDarkly bootstrap data is not available because the back end could not read the flags.';
};

export const deprecated = function(oldName, newName) {
return '[LaunchDarkly] "' + oldName + '" is deprecated, please use "' + newName + '"';
};
Expand Down
File renamed without changes.