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

prepare 5.4.0 release #118

Merged
merged 61 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
68d3556
Make things prettier, add RedisFeatureStore to toplevel ts declaratio…
Jul 11, 2018
c5c665f
Add modules for ldclient StreamProcessor, Requestor, and FeatureStore
Jul 11, 2018
407e6ce
address review
Jul 11, 2018
bef407a
Merge pull request #86 from launchdarkly/at/ch20232/add-flag-util-met…
Jul 19, 2018
762b1c8
fix: package.json to reduce vulnerabilities
snyk-bot Jul 19, 2018
69d7219
Merge pull request #87 from launchdarkly/snyk-fix-0c23hh
eli-darkly Jul 19, 2018
d2c6c18
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Jul 19, 2018
640703c
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Jul 19, 2018
c45414e
remove npm dependency on "crypto", use built-in version instead
eli-darkly Jul 26, 2018
b4fe5f4
update package-lock
eli-darkly Jul 26, 2018
04bc0e8
Merge pull request #88 from launchdarkly/eb/ch17629/built-in-crypto
eli-darkly Jul 26, 2018
d4e15b1
treat HTTP 400 as a recoverable error
eli-darkly Aug 1, 2018
0484c64
Merge pull request #89 from launchdarkly/eb/19705/400-error
eli-darkly Aug 1, 2018
797b4a8
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 1, 2018
ccbb753
update doc comment for new waitForInitialization behavior
eli-darkly Aug 1, 2018
bcb4f0e
Merge pull request #90 from launchdarkly/eb/wait-for-init-docs
eli-darkly Aug 1, 2018
018c6f2
fix waitForInitialization to always resolve with a value
eli-darkly Aug 1, 2018
44f2d4a
Merge pull request #91 from launchdarkly/eb/wait-for-init-value
eli-darkly Aug 1, 2018
1664025
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 1, 2018
9595181
Update feature store type for versioned data kind
Aug 17, 2018
94a7cc6
Merge pull request #92 from launchdarkly/at/update-types
Aug 17, 2018
e51d554
add new version of allFlags() that captures more metadata
eli-darkly Aug 20, 2018
38616c8
add deprecation warning for allFlags
eli-darkly Aug 20, 2018
90e1c1c
typo
eli-darkly Aug 21, 2018
2b672fb
name function toJSON so JSON.stringify will use it
eli-darkly Aug 21, 2018
1a35b79
comment edits
eli-darkly Aug 21, 2018
72e2e24
fix test
eli-darkly Aug 21, 2018
ba0d334
add $valid property in case we care about that on the front end
eli-darkly Aug 21, 2018
db69f7a
Merge pull request #93 from launchdarkly/eb/ch22308/all-flags-state
eli-darkly Aug 21, 2018
a9a5c2f
add ability to filter for only client-side flags
eli-darkly Aug 21, 2018
06bdb46
fix comment
eli-darkly Aug 22, 2018
37c1e82
Merge pull request #94 from launchdarkly/eb/ch12124/client-side-filter
eli-darkly Aug 22, 2018
e7f92fc
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 22, 2018
aa39f0e
add npm audit to build
eli-darkly Aug 22, 2018
2dd2edc
run npm audit fix
eli-darkly Aug 22, 2018
511e783
run npm audit only in current Node
eli-darkly Aug 22, 2018
fd8da86
Merge pull request #95 from launchdarkly/eb/ch21851/npm-audit
eli-darkly Aug 22, 2018
01d415e
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 22, 2018
00e08c8
implement evaluation with explanation
eli-darkly Aug 23, 2018
bece5f2
add doc comment
eli-darkly Aug 23, 2018
69f27da
uncomment test
eli-darkly Aug 23, 2018
ad940ea
tests for default logic
eli-darkly Aug 24, 2018
2e44cff
Merge branch 'master' into all-flags-state
eli-darkly Aug 24, 2018
45dad9d
typo
eli-darkly Aug 24, 2018
d15fef4
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 27, 2018
277c77c
fix allFlagsState behavior when options are omitted
eli-darkly Aug 27, 2018
9317316
Merge pull request #97 from launchdarkly/eb/ch22880/optional-options
eli-darkly Aug 27, 2018
727ff9b
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 27, 2018
1217a54
fix for ch22995 - send event for prerequisite even if it's off
eli-darkly Aug 29, 2018
071f96d
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 29, 2018
07b6eb4
validate TypeScript definitions in CI build
eli-darkly Aug 29, 2018
01c8371
add tsc build step
eli-darkly Aug 29, 2018
a15959c
fix tsc command
eli-darkly Aug 29, 2018
aad1895
add npm script
eli-darkly Aug 29, 2018
aef2575
Merge pull request #98 from launchdarkly/eb/ch22998/check-typescript
eli-darkly Aug 29, 2018
623956a
typo
eli-darkly Aug 29, 2018
de65a33
Merge branch 'master' of github.com:launchdarkly/node-client
eli-darkly Aug 29, 2018
35f97cc
Merge branch 'explanation' into eb/ch19976/explanations
eli-darkly Aug 29, 2018
48e750b
TS syntax error
eli-darkly Aug 29, 2018
85dde8d
Merge pull request #96 from launchdarkly/eb/ch19976/explanations
eli-darkly Aug 30, 2018
9fd7aef
version 5.4.0
eli-darkly Aug 30, 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

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.4.0] - 2018-08-30
### Added:
- The new `LDClient` method `variationDetail` allows you to evaluate a feature flag (using the same parameters as you would for `variation`) and receive more information about how the value was calculated. This information is returned in an object that contains both the result 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.

### Fixed:
- Evaluating a prerequisite feature flag did not produce an analytics event if the prerequisite flag was off.

## [5.3.2] - 2018-08-29
### Fixed:
- Fixed TypeScript syntax errors in `index.d.ts`. We are now running the TypeScript compiler in our automated builds to avoid such problems. (Thanks, [PsychicCat](https://github.com/launchdarkly/node-client/pull/116)!)
Expand Down
146 changes: 87 additions & 59 deletions evaluate_flag.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,90 +8,93 @@ var builtins = ['key', 'ip', 'country', 'email', 'firstName', 'lastName', 'avata

var noop = function(){};

// Callback receives (err, detail, events) where detail has the properties "value", "variationIndex", and "reason";
// detail will never be null even if there's an error.
function evaluate(flag, user, featureStore, cb) {
cb = cb || noop;
if (!user || user.key === null || user.key === undefined) {
cb(null, null, null, null);
cb(null, errorResult('USER_NOT_SPECIFIED'), []);
return;
}

if (!flag) {
cb(null, null, null, null);
cb(null, errorResult('FLAG_NOT_FOUND'), []);
return;
}

var events = [];
evalInternal(flag, user, featureStore, events, function(err, detail) {
cb(err, detail, events);
});
}

function evalInternal(flag, user, featureStore, events, cb) {
// If flag is off, return the off variation
if (!flag.on) {
// Return the off variation if defined and valid
cb(null, flag.offVariation, getVariation(flag, flag.offVariation), null);
getOffResult(flag, { kind: 'OFF' }, function(err, detail) {
cb(err, detail);
});
return;
}

evalInternal(flag, user, featureStore, [], function(err, variation, value, events) {
if (err) {
cb(err, variation, value, events);
return;
}

if (variation === null) {
// Return the off variation if defined and valid
cb(null, flag.offVariation, getVariation(flag, flag.offVariation), events);
checkPrerequisites(flag, user, featureStore, events, function(err, failureReason) {
if (err != null || failureReason != null) {
getOffResult(flag, failureReason, cb);
} else {
cb(err, variation, value, events);
evalRules(flag, user, featureStore, cb);
}
});
return;
}

function evalInternal(flag, user, featureStore, events, cb) {
// Evaluate prerequisites, if any
// Callback receives (err, reason) where reason is null if successful, or a "prerequisite failed" reason
function checkPrerequisites(flag, user, featureStore, events, cb) {
if (flag.prerequisites) {
async.mapSeries(flag.prerequisites,
function(prereq, callback) {
featureStore.get(dataKind.features, prereq.key, function(f) {
// If the flag does not exist in the store or is not on, the prerequisite
// is not satisfied
if (!f || !f.on) {
callback(new Error("Unsatisfied prerequisite"), null);
if (!f) {
callback({ key: prereq.key, err: new Error("Could not retrieve prerequisite feature flag \"" + prereq.key + "\"") });
return;
}
evalInternal(f, user, featureStore, events, function(err, variation, value) {
evalInternal(f, user, featureStore, events, function(err, detail) {
// If there was an error, the value is null, the variation index is out of range,
// or the value does not match the indexed variation the prerequisite is not satisfied
events.push(createFlagEvent(f.key, f, user, variation, value, null, flag.key));
if (err || value === null || variation != prereq.variation) {
callback(new Error("Unsatisfied prerequisite"), null)
events.push(createFlagEvent(f.key, f, user, detail, null, flag.key, true));
if (err) {
callback({ key: prereq.key, err: err });
} else if (!f.on || detail.variationIndex != prereq.variation) {
// Note that if the prerequisite flag is off, we don't consider it a match no matter what its
// off variation was. But we still evaluate it and generate an event.
callback({ key: prereq.key });
} else {
// The prerequisite was satisfied
callback(null, null);
callback(null);
}
});
});
},
function(err, results) {
// If the error is that prerequisites weren't satisfied, we don't return an error,
// because we want to serve the 'offVariation'
if (err) {
cb(null, null, null, events);
return;
}
evalRules(flag, user, featureStore, function(e, variation, value) {
cb(e, variation, value, events);
});
})
function(errInfo) {
if (errInfo) {
cb(errInfo.err, { 'kind': 'PREREQUISITE_FAILED', 'prerequisiteKey': errInfo.key });
} else {
cb(null, null);
}
});
} else {
evalRules(flag, user, featureStore, function(e, variation, value) {
cb(e, variation, value, events);
});
cb(null, null);
}
}

// Callback receives (err, detail)
function evalRules(flag, user, featureStore, cb) {
var i, j;
var target;
var variation;
var rule;
// Check target matches
for (i = 0; i < flag.targets.length; i++) {
for (i = 0; i < (flag.targets || []).length; i++) {
target = flag.targets[i];

if (!target.values) {
Expand All @@ -100,32 +103,30 @@ function evalRules(flag, user, featureStore, cb) {

for (j = 0; j < target.values.length; j++) {
if (user.key === target.values[j]) {
value = getVariation(flag, target.variation);
cb(value === null ? new Error("Undefined variation for flag " + flag.key) : null,
target.variation, value);
getVariation(flag, target.variation, { kind: 'TARGET_MATCH' }, cb);
return;
}
}
}

async.mapSeries(flag.rules,
i = 0;
async.mapSeries(flag.rules || [],
function(rule, callback) {
ruleMatchUser(rule, user, featureStore, function(matched) {
setImmediate(callback, matched ? rule : null, null);
var match = matched ? { index: i, rule: rule } : null;
setImmediate(callback, match, null);
});
},
function(err, results) {
// we use the "error" value to indicate that a rule was successfully matched (since we only care
// about the first match, and mapSeries terminates on the first "error")
if (err) {
var rule = err;
variation = variationForUser(rule, user, flag);
var reason = { kind: 'RULE_MATCH', ruleIndex: err.index, ruleId: err.rule.id };
getResultForVariationOrRollout(err.rule, user, flag, reason, cb);
} else {
// no rule matched; check the fallthrough
variation = variationForUser(flag.fallthrough, user, flag);
getResultForVariationOrRollout(flag.fallthrough, user, flag, { kind: 'FALLTHROUGH' }, cb);
}
cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null,
variation, getVariation(flag, variation));
}
);
}
Expand Down Expand Up @@ -255,16 +256,39 @@ function matchAny(matchFn, value, values) {
return false;
}

// Given an index, return the variation value, or null if
// the index is invalid
function getVariation(flag, index) {
if (index === null || index === undefined || index >= flag.variations.length) {
return null;
function getVariation(flag, index, reason, cb) {
if (index === null || index === undefined || index < 0 || index >= flag.variations.length) {
cb(new Error('Invalid variation index in flag', errResult('MALFORMED_FLAG')));
} else {
return flag.variations[index];
cb(null, { value: flag.variations[index], variationIndex: index, reason: reason });
}
}

function getOffResult(flag, reason, cb) {
if (flag.offVariation === null || flag.offVariation === undefined) {
cb(null, { value: null, variationIndex: null, reason: reason });
} else {
getVariation(flag, flag.offVariation, reason, cb);
}
}

function getResultForVariationOrRollout(r, user, flag, reason, cb) {
if (!r) {
cb(new Error('Fallthrough variation undefined'), errResult('MALFORMED_FLAG'));
} else {
var index = variationForUser(r, user, flag);
if (index === null) {
cb(new Error('Variation/rollout object with no variation or rollout'), errResult('MALFORMED_FLAG'));
} else {
getVariation(flag, index, reason, cb);
}
}
}

function errorResult(errorKind) {
return { value: null, variationIndex: null, reason: { kind: 'ERROR', errorKind: errorKind }};
}

// Given a variation or rollout 'r', select
// the variation for the given user
function variationForUser(r, user, flag) {
Expand Down Expand Up @@ -337,20 +361,24 @@ function bucketableStringValue(value) {
return null;
}

function createFlagEvent(key, flag, user, variation, value, defaultVal, prereqOf) {
return {
function createFlagEvent(key, flag, user, detail, defaultVal, prereqOf, includeReason) {
var e = {
"kind": "feature",
"key": key,
"user": user,
"variation": variation,
"value": value,
"variation": detail.variationIndex,
"value": detail.value,
"default": defaultVal,
"creationDate": new Date().getTime(),
"version": flag ? flag.version : null,
"prereqOf": prereqOf,
"trackEvents": flag ? flag.trackEvents : null,
"debugEventsUntilDate": flag ? flag.debugEventsUntilDate : null
};
if (includeReason) {
e['reason'] = detail.reason;
}
return e;
}

module.exports = {evaluate: evaluate, bucketUser: bucketUser, createFlagEvent: createFlagEvent};
3 changes: 3 additions & 0 deletions event_processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ function EventProcessor(sdkKey, config, errorReporter) {
if (event.version) {
out.version = event.version;
}
if (event.reason) {
out.reason = event.reason;
}
if (config.inlineUsersInEvents || debug) {
out.user = userFilter.filterUser(event.user);
} else {
Expand Down
8 changes: 7 additions & 1 deletion flags_state.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ function FlagsStateBuilder(valid) {
var flagValues = {};
var flagMetadata = {};

builder.addFlag = function(flag, value, variation) {
builder.addFlag = function(flag, value, variation, reason) {
flagValues[flag.key] = value;
var meta = {
version: flag.version,
Expand All @@ -16,6 +16,9 @@ function FlagsStateBuilder(valid) {
if (flag.debugEventsUntilDate !== undefined && flag.debugEventsUntilDate !== null) {
meta.debugEventsUntilDate = flag.debugEventsUntilDate;
}
if (reason) {
meta.reason = reason;
}
flagMetadata[flag.key] = meta;
};

Expand All @@ -24,6 +27,9 @@ function FlagsStateBuilder(valid) {
valid: valid,
allValues: function() { return flagValues; },
getFlagValue: function(key) { return flagValues[key]; },
getFlagReason: function(key) {
return flagMetadata[key] ? flagMetadata[key].reason : null;
},
toJSON: function() {
return Object.assign({}, flagValues, { $flagsState: flagMetadata, $valid: valid });
}
Expand Down
Loading