Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit 62903ce

Browse files
authored
prepare 5.3.0 release (#113)
1 parent 2a5e6c7 commit 62903ce

File tree

7 files changed

+219
-16
lines changed

7 files changed

+219
-16
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
All notable changes to the LaunchDarkly Node.js SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
44

5+
## [5.3.0] - 2018-08-27
6+
### Added:
7+
- The new `LDClient` method `allFlagsState()` should be used instead of `allFlags()` if you are passing flag data to the front end for use with the JavaScript SDK. It preserves some flag metadata that the front end requires in order to send analytics events correctly. Versions 2.5.0 and above of the JavaScript SDK are able to use this metadata, but the output of `allFlagsState()` will still work with older versions.
8+
- The `allFlagsState()` method also allows you to select only client-side-enabled flags to pass to the front end, by using the option `clientSideOnly: true`.
9+
10+
### Deprecated:
11+
- `LDClient.allFlags()`
12+
513
## [5.2.1] - 2018-08-22
614

715
### Fixed:

flags_state.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
function FlagsStateBuilder(valid) {
3+
var builder = {};
4+
var flagValues = {};
5+
var flagMetadata = {};
6+
7+
builder.addFlag = function(flag, value, variation) {
8+
flagValues[flag.key] = value;
9+
var meta = {
10+
version: flag.version,
11+
trackEvents: flag.trackEvents
12+
};
13+
if (variation !== undefined && variation !== null) {
14+
meta.variation = variation;
15+
}
16+
if (flag.debugEventsUntilDate !== undefined && flag.debugEventsUntilDate !== null) {
17+
meta.debugEventsUntilDate = flag.debugEventsUntilDate;
18+
}
19+
flagMetadata[flag.key] = meta;
20+
};
21+
22+
builder.build = function() {
23+
return {
24+
valid: valid,
25+
allValues: function() { return flagValues; },
26+
getFlagValue: function(key) { return flagValues[key]; },
27+
toJSON: function() {
28+
return Object.assign({}, flagValues, { $flagsState: flagMetadata, $valid: valid });
29+
}
30+
};
31+
}
32+
33+
return builder;
34+
}
35+
36+
module.exports = FlagsStateBuilder;

index.d.ts

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,45 @@ declare module 'ldclient-node' {
4747
[key: string]: LDFlagValue;
4848
};
4949

50+
/**
51+
* An object that contains the state of all feature flags, generated by the client's
52+
* allFlagsState() method.
53+
*/
54+
export interface LDFlagsState = {
55+
/**
56+
* True if this object contains a valid snapshot of feature flag state, or false if the
57+
* state could not be computed (for instance, because the client was offline or there
58+
* was no user).
59+
*/
60+
valid: boolean;
61+
62+
/**
63+
* Returns the value of an individual feature flag at the time the state was recorded.
64+
* It will be null if the flag returned the default value, or if there was no such flag.
65+
* @param key the flag key
66+
*/
67+
getFlagValue: (key: string) => LDFlagValue;
68+
69+
/**
70+
* Returns a map of feature flag keys to values. If a flag would have evaluated to the
71+
* default value, its value will be null.
72+
*
73+
* Do not use this method if you are passing data to the front end to "bootstrap" the
74+
* JavaScript client. Instead, use toJson().
75+
*/
76+
allValues: () => LDFlagSet;
77+
78+
/**
79+
* Returns a Javascript representation of the entire state map, in the format used by
80+
* the Javascript SDK. Use this method if you are passing data to the front end in
81+
* order to "bootstrap" the JavaScript client.
82+
*
83+
* Do not rely on the exact shape of this data, as it may change in future to support
84+
* the needs of the JavaScript client.
85+
*/
86+
toJSON: () => object;
87+
};
88+
5089
/**
5190
* LaunchDarkly initialization options.
5291
*/
@@ -415,6 +454,17 @@ declare module 'ldclient-node' {
415454
requestAllData: (cb: (err: any, body: any) => void) => void;
416455
}
417456

457+
/**
458+
* Optional settings that can be passed to LDClient.allFlagsState().
459+
*/
460+
export type LDFlagsStateOptions = {
461+
/**
462+
* True if the state should include only flags that have been marked for use with the
463+
* client-side SDK. By default, all flags are included.
464+
*/
465+
clientSideOnly?: boolean;
466+
};
467+
418468
/**
419469
* The LaunchDarkly client's instance interface.
420470
*
@@ -484,8 +534,9 @@ declare module 'ldclient-node' {
484534
/**
485535
* Retrieves the set of all flag values for a user.
486536
*
487-
* @param key
488-
* The key of the flag for which to retrieve the corresponding value.
537+
* This method is deprecated; use allFlagsState() instead. Current versions of the client-side
538+
* SDK will not generate analytics events correctly if you pass the result of allFlags().
539+
*
489540
* @param user
490541
* @param callback
491542
* The node style callback to receive the variation result.
@@ -496,6 +547,27 @@ declare module 'ldclient-node' {
496547
callback?: (err: any, res: LDFlagSet) => void
497548
) => Promise<LDFlagSet>;
498549

550+
/**
551+
* Builds an object that encapsulates the state of all feature flags for a given user,
552+
* including the flag values and also metadata that can be used on the front end. This
553+
* method does not send analytics events back to LaunchDarkly.
554+
*
555+
* The most common use case for this method is to bootstrap a set of client-side
556+
* feature flags from a back-end service. Call the toJSON() method of the returned object
557+
* to convert it to the data structure used by the client-side SDK.
558+
*
559+
* @param user The end user requesting the feature flags.
560+
* @param options Optional object with properties that determine how the state is computed;
561+
* set `clientSideOnly: true` to include only client-side-enabled flags
562+
* @param callback The node-style callback to receive the state result.
563+
* @returns a Promise containing the state object
564+
*/
565+
allFlagsState: (
566+
user: LDUser,
567+
options?: LDFlagsStateOptions,
568+
callback?: (err: any, res: LDFlagsState) => void
569+
) => Promise<LDFlagsState>;
570+
499571
/**
500572
*
501573
* The secure_mode_hash method computes an HMAC signature of a user signed with the client's SDK key.

index.js

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ var EventEmitter = require('events').EventEmitter;
55
var EventProcessor = require('./event_processor');
66
var PollingProcessor = require('./polling');
77
var StreamingProcessor = require('./streaming');
8+
var FlagsStateBuilder = require('./flags_state');
89
var configuration = require('./configuration');
910
var evaluate = require('./evaluate_flag');
1011
var messages = require('./messages');
@@ -232,29 +233,44 @@ var newClient = function(sdkKey, config) {
232233
}
233234

234235
client.allFlags = function(user, callback) {
236+
config.logger.warn("allFlags() is deprecated. Call 'allFlagsState' instead and call toJSON() on the result");
237+
return wrapPromiseCallback(
238+
client.allFlagsState(user).then(function(state) {
239+
return state.allValues();
240+
}),
241+
callback);
242+
}
243+
244+
client.allFlagsState = function(user, options, callback) {
245+
options = options || {};
235246
return wrapPromiseCallback(new Promise(function(resolve, reject) {
236247
sanitizeUser(user);
237-
var results = {};
238-
248+
239249
if (this.isOffline()) {
240-
config.logger.info("allFlags() called in offline mode. Returning empty map.");
241-
return resolve({});
250+
config.logger.info("allFlagsState() called in offline mode. Returning empty state.");
251+
return resolve(FlagsStateBuilder(false).build());
242252
}
243253

244254
if (!user) {
245-
config.logger.info("allFlags() called without user. Returning empty map.");
246-
return resolve({});
255+
config.logger.info("allFlagsState() called without user. Returning empty state.");
256+
return resolve(FlagsStateBuilder(false).build());
247257
}
248258

259+
var builder = FlagsStateBuilder(true);
260+
var clientOnly = options.clientSideOnly;
249261
config.featureStore.all(dataKind.features, function(flags) {
250262
async.forEachOf(flags, function(flag, key, iterateeCb) {
251-
// At the moment, we don't send any events here
252-
evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) {
253-
results[key] = value;
263+
if (clientOnly && !flag.clientSide) {
254264
setImmediate(iterateeCb);
255-
})
265+
} else {
266+
// At the moment, we don't send any events here
267+
evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) {
268+
builder.addFlag(flag, value, variation);
269+
setImmediate(iterateeCb);
270+
});
271+
}
256272
}, function(err) {
257-
return err ? reject(err) : resolve(results);
273+
return err ? reject(err) : resolve(builder.build());
258274
});
259275
});
260276
}.bind(this)), callback);

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ldclient-node",
3-
"version": "5.2.1",
3+
"version": "5.3.0",
44
"description": "LaunchDarkly SDK for Node.js",
55
"main": "index.js",
66
"scripts": {

test/LDClient-test.js

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ describe('LDClient', function() {
2929
};
3030

3131
beforeEach(function() {
32+
logger.debug = jest.fn();
3233
logger.info = jest.fn();
3334
logger.warn = jest.fn();
35+
logger.error = jest.fn();
3436
eventProcessor.events = [];
3537
updateProcessor.error = null;
3638
});
@@ -83,6 +85,18 @@ describe('LDClient', function() {
8385
});
8486
});
8587

88+
it('returns empty state for allFlagsState in offline mode and logs a message', function(done) {
89+
var client = LDClient.init('secret', {offline: true, logger: logger});
90+
client.on('ready', function() {
91+
client.allFlagsState({key: 'user'}, {}, function(err, state) {
92+
expect(state.valid).toEqual(false);
93+
expect(state.allValues()).toEqual({});
94+
expect(logger.info).toHaveBeenCalledTimes(1);
95+
done();
96+
});
97+
});
98+
});
99+
86100
it('allows deprecated method all_flags', function(done) {
87101
var client = LDClient.init('secret', {offline: true, logger: logger});
88102
client.on('ready', function() {
@@ -103,7 +117,8 @@ describe('LDClient', function() {
103117
return LDClient.init('secret', {
104118
featureStore: store,
105119
eventProcessor: eventProcessor,
106-
updateProcessor: updateProcessor
120+
updateProcessor: updateProcessor,
121+
logger: logger
107122
});
108123
}
109124

@@ -257,6 +272,62 @@ describe('LDClient', function() {
257272
client.allFlags(user, function(err, results) {
258273
expect(err).toBeNull();
259274
expect(results).toEqual({feature: 'b'});
275+
expect(logger.warn).toHaveBeenCalledTimes(1); // deprecation warning
276+
done();
277+
});
278+
});
279+
});
280+
281+
it('captures flag state with allFlagsState()', function(done) {
282+
var flag = {
283+
key: 'feature',
284+
version: 100,
285+
on: true,
286+
targets: [],
287+
fallthrough: { variation: 1 },
288+
variations: ['a', 'b'],
289+
trackEvents: true,
290+
debugEventsUntilDate: 1000
291+
};
292+
var client = createOnlineClientWithFlags({ feature: flag });
293+
var user = { key: 'user' };
294+
client.on('ready', function() {
295+
client.allFlagsState(user, {}, function(err, state) {
296+
expect(err).toBeNull();
297+
expect(state.valid).toEqual(true);
298+
expect(state.allValues()).toEqual({feature: 'b'});
299+
expect(state.getFlagValue('feature')).toEqual('b');
300+
expect(state.toJSON()).toEqual({
301+
feature: 'b',
302+
$flagsState: {
303+
feature: {
304+
version: 100,
305+
variation: 1,
306+
trackEvents: true,
307+
debugEventsUntilDate: 1000
308+
}
309+
},
310+
$valid: true
311+
});
312+
done();
313+
});
314+
});
315+
});
316+
317+
it('can filter for only client-side flags with allFlagsState()', function(done) {
318+
var flag1 = { key: 'server-side-1', on: false, offVariation: 0, variations: ['a'], clientSide: false };
319+
var flag2 = { key: 'server-side-2', on: false, offVariation: 0, variations: ['b'], clientSide: false };
320+
var flag3 = { key: 'client-side-1', on: false, offVariation: 0, variations: ['value1'], clientSide: true };
321+
var flag4 = { key: 'client-side-2', on: false, offVariation: 0, variations: ['value2'], clientSide: true };
322+
var client = createOnlineClientWithFlags({
323+
'server-side-1': flag1, 'server-side-2': flag2, 'client-side-1': flag3, 'client-side-2': flag4
324+
});
325+
var user = { key: 'user' };
326+
client.on('ready', function() {
327+
client.allFlagsState(user, { clientSideOnly: true }, function(err, state) {
328+
expect(err).toBeNull();
329+
expect(state.valid).toEqual(true);
330+
expect(state.allValues()).toEqual({ 'client-side-1': 'value1', 'client-side-2': 'value2' });
260331
done();
261332
});
262333
});

0 commit comments

Comments
 (0)