Skip to content

Commit 85940ba

Browse files
authored
prepare 2.5.0 release (#110)
1 parent bad2386 commit 85940ba

File tree

10 files changed

+179
-17
lines changed

10 files changed

+179
-17
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@
33
All notable changes to the LaunchDarkly client-side JavaScript SDK will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org).
55

6+
## [2.5.0] - 2018-08-27
7+
### Changed:
8+
- 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.
9+
- When posting events to LaunchDarkly, if a request fails, it will be retried once.
10+
- 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`.
11+
612
## [2.4.1] - 2018-08-14
713
### Fixed:
814
- 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()`.

package-lock.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ldclient-js",
3-
"version": "2.4.1",
3+
"version": "2.5.0",
44
"description": "LaunchDarkly SDK for JavaScript",
55
"author": "LaunchDarkly <team@launchdarkly.com>",
66
"license": "Apache-2.0",
@@ -15,8 +15,10 @@
1515
"ldclient.es.js",
1616
"ldclient.es.js.map",
1717
"ldclient.min.js",
18-
"ldclient.min.js.map"
18+
"ldclient.min.js.map",
19+
"typings.d.ts"
1920
],
21+
"types": "./typings.d.ts",
2022
"main": "dist/ldclient.cjs.js",
2123
"module": "dist/ldclient.es.js",
2224
"scripts": {
@@ -36,7 +38,6 @@
3638
"clean": "rimraf dist/**",
3739
"prepublishOnly": "npm run build:min"
3840
},
39-
"types": "./src/index.d.ts",
4041
"devDependencies": {
4142
"babel-core": "6.26.0",
4243
"babel-eslint": "8.2.2",

src/EventSender.js

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as errors from './errors';
12
import * as utils from './utils';
23

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

3132
function sendChunk(events, usePost, sync) {
3233
const createImage = imageCreator || loadUrlUsingImage;
34+
const jsonBody = JSON.stringify(events);
3335
const send = onDone => {
34-
if (usePost) {
36+
function createRequest(canRetry) {
3537
const xhr = new XMLHttpRequest();
3638
xhr.open('POST', postUrl, !sync);
3739
utils.addLDHeaders(xhr);
3840
xhr.setRequestHeader('Content-Type', 'application/json');
3941
xhr.setRequestHeader('X-LaunchDarkly-Event-Schema', '3');
40-
4142
if (!sync) {
4243
xhr.addEventListener('load', () => {
43-
onDone(getResponseInfo(xhr));
44+
if (xhr.status >= 400 && errors.isHttpErrorRecoverable(xhr.status) && canRetry) {
45+
createRequest(false).send(jsonBody);
46+
} else {
47+
onDone(getResponseInfo(xhr));
48+
}
4449
});
50+
if (canRetry) {
51+
xhr.addEventListener('error', () => {
52+
createRequest(false).send(jsonBody);
53+
});
54+
}
4555
}
46-
47-
xhr.send(JSON.stringify(events));
56+
return xhr;
57+
}
58+
if (usePost) {
59+
createRequest(true).send(jsonBody);
4860
} else {
49-
const src = imageUrl + '?d=' + utils.base64URLEncode(JSON.stringify(events));
61+
const src = imageUrl + '?d=' + utils.base64URLEncode(jsonBody);
5062
createImage(src, sync ? null : onDone);
5163
}
5264
};

src/__tests__/EventSender-test.js

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -103,8 +103,10 @@ describe('EventSender', () => {
103103
const sender = EventSender(eventsUrl, envId, true);
104104
const event = { kind: 'identify', key: 'userKey' };
105105
sender.sendEvents([event], false);
106-
lastRequest().respond();
107-
expect(lastRequest().async).toEqual(true);
106+
requests[0].respond();
107+
expect(requests.length).toEqual(1);
108+
expect(requests[0].async).toEqual(true);
109+
expect(JSON.parse(requests[0].requestBody)).toEqual([event]);
108110
});
109111

110112
it('should send synchronously', () => {
@@ -132,9 +134,49 @@ describe('EventSender', () => {
132134
it('should send custom user-agent header', () => {
133135
const sender = EventSender(eventsUrl, envId, true);
134136
const event = { kind: 'identify', key: 'userKey' };
135-
sender.sendEvents([event], true);
137+
sender.sendEvents([event], false);
136138
lastRequest().respond();
137139
expect(lastRequest().requestHeaders['X-LaunchDarkly-User-Agent']).toEqual(utils.getLDUserAgentString());
138140
});
141+
142+
const retryableStatuses = [400, 408, 429, 500, 503];
143+
for (const i in retryableStatuses) {
144+
const status = retryableStatuses[i];
145+
it('should retry on error ' + status, () => {
146+
const sender = EventSender(eventsUrl, envId, true);
147+
const event = { kind: 'false', key: 'userKey' };
148+
sender.sendEvents([event], false);
149+
requests[0].respond(status);
150+
expect(requests.length).toEqual(2);
151+
expect(JSON.parse(requests[1].requestBody)).toEqual([event]);
152+
});
153+
}
154+
155+
it('should not retry more than once', () => {
156+
const sender = EventSender(eventsUrl, envId, true);
157+
const event = { kind: 'false', key: 'userKey' };
158+
sender.sendEvents([event], false);
159+
requests[0].respond(503);
160+
expect(requests.length).toEqual(2);
161+
requests[1].respond(503);
162+
expect(requests.length).toEqual(2);
163+
});
164+
165+
it('should not retry on error 401', () => {
166+
const sender = EventSender(eventsUrl, envId, true);
167+
const event = { kind: 'false', key: 'userKey' };
168+
sender.sendEvents([event], false);
169+
requests[0].respond(401);
170+
expect(requests.length).toEqual(1);
171+
});
172+
173+
it('should retry on I/O error', () => {
174+
const sender = EventSender(eventsUrl, envId, true);
175+
const event = { kind: 'false', key: 'userKey' };
176+
sender.sendEvents([event], false);
177+
requests[0].error();
178+
expect(requests.length).toEqual(2);
179+
expect(JSON.parse(requests[1].requestBody)).toEqual([event]);
180+
});
139181
});
140182
});

src/__tests__/LDClient-test.js

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ describe('LDClient', () => {
158158
requests[0].respond(404);
159159
});
160160

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

170+
it('sets flag values from bootstrap object with old format', () => {
171+
const client = LDClient.initialize(envName, user, {
172+
bootstrap: { foo: 'bar' },
173+
});
174+
175+
expect(client.variation('foo')).toEqual('bar');
176+
});
177+
178+
it('logs warning when bootstrap object uses old format', () => {
179+
LDClient.initialize(envName, user, {
180+
bootstrap: { foo: 'bar' },
181+
});
182+
183+
expect(warnSpy).toHaveBeenCalledWith(messages.bootstrapOldFormat());
184+
});
185+
186+
it('sets flag values from bootstrap object with new format', () => {
187+
const client = LDClient.initialize(envName, user, {
188+
bootstrap: { foo: 'bar', $flagsState: { foo: { version: 1 } } },
189+
});
190+
191+
expect(client.variation('foo')).toEqual('bar');
192+
});
193+
194+
it('does not log warning when bootstrap object uses new format', () => {
195+
LDClient.initialize(envName, user, {
196+
bootstrap: { foo: 'bar', $flagsState: { foo: { version: 1 } } },
197+
});
198+
199+
expect(warnSpy).not.toHaveBeenCalled();
200+
});
201+
170202
it('should contain package version', () => {
171203
// Arrange
172204
const version = LDClient.version;
@@ -420,13 +452,15 @@ describe('LDClient', () => {
420452
expect(e.user).toEqual(user);
421453
}
422454

423-
function expectFeatureEvent(e, key, value, variation, version, defaultVal) {
455+
function expectFeatureEvent(e, key, value, variation, version, defaultVal, trackEvents, debugEventsUntilDate) {
424456
expect(e.kind).toEqual('feature');
425457
expect(e.key).toEqual(key);
426458
expect(e.value).toEqual(value);
427459
expect(e.variation).toEqual(variation);
428460
expect(e.version).toEqual(version);
429461
expect(e.default).toEqual(defaultVal);
462+
expect(e.trackEvents).toEqual(trackEvents);
463+
expect(e.debugEventsUntilDate).toEqual(debugEventsUntilDate);
430464
}
431465

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

514548
server.respond();
515549
});
550+
551+
it('can get metadata for events from bootstrap object', done => {
552+
const ep = stubEventProcessor();
553+
const bootstrapData = {
554+
foo: 'bar',
555+
$flagsState: {
556+
foo: {
557+
variation: 1,
558+
version: 2,
559+
trackEvents: true,
560+
debugEventsUntilDate: 1000,
561+
},
562+
},
563+
};
564+
const client = LDClient.initialize(envName, user, { eventProcessor: ep, bootstrap: bootstrapData });
565+
566+
client.on('ready', () => {
567+
client.variation('foo', 'x');
568+
569+
expect(ep.events.length).toEqual(2);
570+
expectIdentifyEvent(ep.events[0], user);
571+
expectFeatureEvent(ep.events[1], 'foo', 'bar', 1, 2, 'x', true, 1000);
572+
573+
done();
574+
});
575+
});
516576
});
517577

518578
describe('event listening', () => {

src/errors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const LDFlagFetchError = createCustomError('LaunchDarklyFlagFetchError');
2121

2222
export function isHttpErrorRecoverable(status) {
2323
if (status >= 400 && status < 500) {
24-
return status === 408 || status === 429;
24+
return status === 400 || status === 408 || status === 429;
2525
}
2626
return true;
2727
}

src/index.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,42 @@ export function initialize(env, user, options = {}) {
2828
const events = options.eventProcessor || EventProcessor(eventsUrl, environment, options, emitter);
2929
const requestor = Requestor(baseUrl, environment, options.useReport);
3030
const seenRequests = {};
31-
let flags = typeof options.bootstrap === 'object' ? utils.transformValuesToVersionedValues(options.bootstrap) : {};
31+
let flags = typeof options.bootstrap === 'object' ? readFlagsFromBootstrap(options.bootstrap) : {};
3232
let goalTracker;
3333
let useLocalStorage;
3434
let goals;
3535
let subscribedToChangeEvents;
3636
let firstEvent = true;
3737

38+
function readFlagsFromBootstrap(data) {
39+
// If the bootstrap data came from an older server-side SDK, we'll have just a map of keys to values.
40+
// Newer SDKs that have an allFlagsState method will provide an extra "$flagsState" key that contains
41+
// the rest of the metadata we want. We do it this way for backward compatibility with older JS SDKs.
42+
const keys = Object.keys(data);
43+
const metadataKey = '$flagsState';
44+
const validKey = '$valid';
45+
const metadata = data[metadataKey];
46+
if (!metadata && keys.length) {
47+
console.warn(messages.bootstrapOldFormat());
48+
}
49+
if (data[validKey] === false) {
50+
console.warn(messages.bootstrapInvalid());
51+
}
52+
const ret = {};
53+
keys.forEach(key => {
54+
if (key !== metadataKey && key !== validKey) {
55+
let flag = { value: data[key] };
56+
if (metadata && metadata[key]) {
57+
flag = utils.extend(flag, metadata[key]);
58+
} else {
59+
flag.version = 0;
60+
}
61+
ret[key] = flag;
62+
}
63+
});
64+
return ret;
65+
}
66+
3867
function shouldEnqueueEvent() {
3968
return sendEvents && !doNotTrack();
4069
}

src/messages.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ export const invalidUser = function() {
3737
return 'Invalid user specified.' + docLink;
3838
};
3939

40+
export const bootstrapOldFormat = function() {
41+
return (
42+
'LaunchDarkly client was initialized with bootstrap data that did not include flag metadata. ' +
43+
'Events may not be sent correctly.' +
44+
docLink
45+
);
46+
};
47+
48+
export const bootstrapInvalid = function() {
49+
return 'LaunchDarkly bootstrap data is not available because the back end could not read the flags.';
50+
};
51+
4052
export const deprecated = function(oldName, newName) {
4153
return '[LaunchDarkly] "' + oldName + '" is deprecated, please use "' + newName + '"';
4254
};
File renamed without changes.

0 commit comments

Comments
 (0)