Skip to content

Commit

Permalink
prepare 5.3.0 release (launchdarkly#113)
Browse files Browse the repository at this point in the history
  • Loading branch information
eli-darkly authored Aug 27, 2018
1 parent 2a5e6c7 commit 62903ce
Show file tree
Hide file tree
Showing 7 changed files with 219 additions and 16 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

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

## [5.3.0] - 2018-08-27
### Added:
- 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.
- 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`.

### Deprecated:
- `LDClient.allFlags()`

## [5.2.1] - 2018-08-22

### Fixed:
Expand Down
36 changes: 36 additions & 0 deletions flags_state.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

function FlagsStateBuilder(valid) {
var builder = {};
var flagValues = {};
var flagMetadata = {};

builder.addFlag = function(flag, value, variation) {
flagValues[flag.key] = value;
var meta = {
version: flag.version,
trackEvents: flag.trackEvents
};
if (variation !== undefined && variation !== null) {
meta.variation = variation;
}
if (flag.debugEventsUntilDate !== undefined && flag.debugEventsUntilDate !== null) {
meta.debugEventsUntilDate = flag.debugEventsUntilDate;
}
flagMetadata[flag.key] = meta;
};

builder.build = function() {
return {
valid: valid,
allValues: function() { return flagValues; },
getFlagValue: function(key) { return flagValues[key]; },
toJSON: function() {
return Object.assign({}, flagValues, { $flagsState: flagMetadata, $valid: valid });
}
};
}

return builder;
}

module.exports = FlagsStateBuilder;
76 changes: 74 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,45 @@ declare module 'ldclient-node' {
[key: string]: LDFlagValue;
};

/**
* An object that contains the state of all feature flags, generated by the client's
* allFlagsState() method.
*/
export interface LDFlagsState = {
/**
* True if this object contains a valid snapshot of feature flag state, or false if the
* state could not be computed (for instance, because the client was offline or there
* was no user).
*/
valid: boolean;

/**
* Returns the value of an individual feature flag at the time the state was recorded.
* It will be null if the flag returned the default value, or if there was no such flag.
* @param key the flag key
*/
getFlagValue: (key: string) => LDFlagValue;

/**
* Returns a map of feature flag keys to values. If a flag would have evaluated to the
* default value, its value will be null.
*
* Do not use this method if you are passing data to the front end to "bootstrap" the
* JavaScript client. Instead, use toJson().
*/
allValues: () => LDFlagSet;

/**
* Returns a Javascript representation of the entire state map, in the format used by
* the Javascript SDK. Use this method if you are passing data to the front end in
* order to "bootstrap" the JavaScript client.
*
* Do not rely on the exact shape of this data, as it may change in future to support
* the needs of the JavaScript client.
*/
toJSON: () => object;
};

/**
* LaunchDarkly initialization options.
*/
Expand Down Expand Up @@ -415,6 +454,17 @@ declare module 'ldclient-node' {
requestAllData: (cb: (err: any, body: any) => void) => void;
}

/**
* Optional settings that can be passed to LDClient.allFlagsState().
*/
export type LDFlagsStateOptions = {
/**
* True if the state should include only flags that have been marked for use with the
* client-side SDK. By default, all flags are included.
*/
clientSideOnly?: boolean;
};

/**
* The LaunchDarkly client's instance interface.
*
Expand Down Expand Up @@ -484,8 +534,9 @@ declare module 'ldclient-node' {
/**
* Retrieves the set of all flag values for a user.
*
* @param key
* The key of the flag for which to retrieve the corresponding value.
* This method is deprecated; use allFlagsState() instead. Current versions of the client-side
* SDK will not generate analytics events correctly if you pass the result of allFlags().
*
* @param user
* @param callback
* The node style callback to receive the variation result.
Expand All @@ -496,6 +547,27 @@ declare module 'ldclient-node' {
callback?: (err: any, res: LDFlagSet) => void
) => Promise<LDFlagSet>;

/**
* Builds an object that encapsulates the state of all feature flags for a given user,
* including the flag values and also metadata that can be used on the front end. This
* method does not send analytics events back to LaunchDarkly.
*
* The most common use case for this method is to bootstrap a set of client-side
* feature flags from a back-end service. Call the toJSON() method of the returned object
* to convert it to the data structure used by the client-side SDK.
*
* @param user The end user requesting the feature flags.
* @param options Optional object with properties that determine how the state is computed;
* set `clientSideOnly: true` to include only client-side-enabled flags
* @param callback The node-style callback to receive the state result.
* @returns a Promise containing the state object
*/
allFlagsState: (
user: LDUser,
options?: LDFlagsStateOptions,
callback?: (err: any, res: LDFlagsState) => void
) => Promise<LDFlagsState>;

/**
*
* The secure_mode_hash method computes an HMAC signature of a user signed with the client's SDK key.
Expand Down
38 changes: 27 additions & 11 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ var EventEmitter = require('events').EventEmitter;
var EventProcessor = require('./event_processor');
var PollingProcessor = require('./polling');
var StreamingProcessor = require('./streaming');
var FlagsStateBuilder = require('./flags_state');
var configuration = require('./configuration');
var evaluate = require('./evaluate_flag');
var messages = require('./messages');
Expand Down Expand Up @@ -232,29 +233,44 @@ var newClient = function(sdkKey, config) {
}

client.allFlags = function(user, callback) {
config.logger.warn("allFlags() is deprecated. Call 'allFlagsState' instead and call toJSON() on the result");
return wrapPromiseCallback(
client.allFlagsState(user).then(function(state) {
return state.allValues();
}),
callback);
}

client.allFlagsState = function(user, options, callback) {
options = options || {};
return wrapPromiseCallback(new Promise(function(resolve, reject) {
sanitizeUser(user);
var results = {};


if (this.isOffline()) {
config.logger.info("allFlags() called in offline mode. Returning empty map.");
return resolve({});
config.logger.info("allFlagsState() called in offline mode. Returning empty state.");
return resolve(FlagsStateBuilder(false).build());
}

if (!user) {
config.logger.info("allFlags() called without user. Returning empty map.");
return resolve({});
config.logger.info("allFlagsState() called without user. Returning empty state.");
return resolve(FlagsStateBuilder(false).build());
}

var builder = FlagsStateBuilder(true);
var clientOnly = options.clientSideOnly;
config.featureStore.all(dataKind.features, function(flags) {
async.forEachOf(flags, function(flag, key, iterateeCb) {
// At the moment, we don't send any events here
evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) {
results[key] = value;
if (clientOnly && !flag.clientSide) {
setImmediate(iterateeCb);
})
} else {
// At the moment, we don't send any events here
evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) {
builder.addFlag(flag, value, variation);
setImmediate(iterateeCb);
});
}
}, function(err) {
return err ? reject(err) : resolve(results);
return err ? reject(err) : resolve(builder.build());
});
});
}.bind(this)), callback);
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-node",
"version": "5.2.1",
"version": "5.3.0",
"description": "LaunchDarkly SDK for Node.js",
"main": "index.js",
"scripts": {
Expand Down
73 changes: 72 additions & 1 deletion test/LDClient-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ describe('LDClient', function() {
};

beforeEach(function() {
logger.debug = jest.fn();
logger.info = jest.fn();
logger.warn = jest.fn();
logger.error = jest.fn();
eventProcessor.events = [];
updateProcessor.error = null;
});
Expand Down Expand Up @@ -83,6 +85,18 @@ describe('LDClient', function() {
});
});

it('returns empty state for allFlagsState in offline mode and logs a message', function(done) {
var client = LDClient.init('secret', {offline: true, logger: logger});
client.on('ready', function() {
client.allFlagsState({key: 'user'}, {}, function(err, state) {
expect(state.valid).toEqual(false);
expect(state.allValues()).toEqual({});
expect(logger.info).toHaveBeenCalledTimes(1);
done();
});
});
});

it('allows deprecated method all_flags', function(done) {
var client = LDClient.init('secret', {offline: true, logger: logger});
client.on('ready', function() {
Expand All @@ -103,7 +117,8 @@ describe('LDClient', function() {
return LDClient.init('secret', {
featureStore: store,
eventProcessor: eventProcessor,
updateProcessor: updateProcessor
updateProcessor: updateProcessor,
logger: logger
});
}

Expand Down Expand Up @@ -257,6 +272,62 @@ describe('LDClient', function() {
client.allFlags(user, function(err, results) {
expect(err).toBeNull();
expect(results).toEqual({feature: 'b'});
expect(logger.warn).toHaveBeenCalledTimes(1); // deprecation warning
done();
});
});
});

it('captures flag state with allFlagsState()', function(done) {
var flag = {
key: 'feature',
version: 100,
on: true,
targets: [],
fallthrough: { variation: 1 },
variations: ['a', 'b'],
trackEvents: true,
debugEventsUntilDate: 1000
};
var client = createOnlineClientWithFlags({ feature: flag });
var user = { key: 'user' };
client.on('ready', function() {
client.allFlagsState(user, {}, function(err, state) {
expect(err).toBeNull();
expect(state.valid).toEqual(true);
expect(state.allValues()).toEqual({feature: 'b'});
expect(state.getFlagValue('feature')).toEqual('b');
expect(state.toJSON()).toEqual({
feature: 'b',
$flagsState: {
feature: {
version: 100,
variation: 1,
trackEvents: true,
debugEventsUntilDate: 1000
}
},
$valid: true
});
done();
});
});
});

it('can filter for only client-side flags with allFlagsState()', function(done) {
var flag1 = { key: 'server-side-1', on: false, offVariation: 0, variations: ['a'], clientSide: false };
var flag2 = { key: 'server-side-2', on: false, offVariation: 0, variations: ['b'], clientSide: false };
var flag3 = { key: 'client-side-1', on: false, offVariation: 0, variations: ['value1'], clientSide: true };
var flag4 = { key: 'client-side-2', on: false, offVariation: 0, variations: ['value2'], clientSide: true };
var client = createOnlineClientWithFlags({
'server-side-1': flag1, 'server-side-2': flag2, 'client-side-1': flag3, 'client-side-2': flag4
});
var user = { key: 'user' };
client.on('ready', function() {
client.allFlagsState(user, { clientSideOnly: true }, function(err, state) {
expect(err).toBeNull();
expect(state.valid).toEqual(true);
expect(state.allValues()).toEqual({ 'client-side-1': 'value1', 'client-side-2': 'value2' });
done();
});
});
Expand Down

0 comments on commit 62903ce

Please sign in to comment.