diff --git a/.circleci/config.yml b/.circleci/config.yml index 1661560..9cc27c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,17 +1,35 @@ version: 2 + +workflows: + version: 2 + test: + jobs: + - oldest-long-term-support-release + - current-release + +node-template: &node-template + steps: + - checkout + - run: echo "Node version:" `node --version` + - run: npm install + - run: + command: npm test + environment: + JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml" + - store_test_results: + path: reports/junit + - store_artifacts: + path: reports/junit + jobs: - build: + oldest-long-term-support-release: + <<: *node-template docker: - image: circleci/node:6 - image: redis - steps: - - checkout - - run: npm install - - run: - command: npm test - environment: - JEST_JUNIT_OUTPUT: "reports/junit/js-test-results.xml" - - store_test_results: - path: reports/junit - - store_artifacts: - path: reports/junit + + current-release: + <<: *node-template + docker: + - image: circleci/node:latest + - image: redis diff --git a/configuration.js b/configuration.js new file mode 100644 index 0000000..6750fc7 --- /dev/null +++ b/configuration.js @@ -0,0 +1,103 @@ +var winston = require('winston'); +var InMemoryFeatureStore = require('./feature_store'); +var messages = require('./messages'); +var package_json = require('./package.json'); + +module.exports = (function() { + var defaults = { + baseUri: 'https://app.launchdarkly.com', + streamUri: 'https://stream.launchdarkly.com', + eventsUri: 'https://events.launchdarkly.com', + stream: true, + sendEvents: true, + timeout: 5, + capacity: 1000, + flushInterval: 5, + pollInterval: 30, + offline: false, + useLdd: false, + allAttributesPrivate: false, + privateAttributeNames: [], + userKeysCapacity: 1000, + userKeysFlushInterval: 300, + featureStore: InMemoryFeatureStore() + }; + + var deprecatedOptions = { + base_uri: 'baseUri', + stream_uri: 'streamUri', + events_uri: 'eventsUri', + send_events: 'sendEvents', + flush_interval: 'flushInterval', + poll_interval: 'pollInterval', + proxy_host: 'proxyHost', + proxy_port: 'proxyPort', + proxy_auth: 'proxyAuth', + feature_store: 'featureStore', + use_ldd: 'useLdd', + all_attributes_private: 'allAttributesPrivate', + private_attribute_names: 'privateAttributeNames' + }; + + function checkDeprecatedOptions(config) { + Object.keys(deprecatedOptions).forEach(function(oldName) { + if (config[oldName] !== undefined) { + var newName = deprecatedOptions[oldName]; + config.logger.warn(messages.deprecated(oldName, newName)); + if (config[newName] === undefined) { + config[newName] = config[oldName]; + } + delete config[oldName]; + } + }); + } + + function applyDefaults(config, defaults) { + // This works differently from Object.assign() in that it will *not* override a default value + // if the provided value is explicitly set to null. + var ret = Object.assign({}, config); + Object.keys(defaults).forEach(function(name) { + if (ret[name] === undefined || ret[name] === null) { + ret[name] = defaults[name]; + } + }); + return ret; + } + + function canonicalizeUri(uri) { + return uri.replace(/\/+$/, ""); + } + + function validate(options) { + var config = Object.assign({}, options || {}); + + config.userAgent = 'NodeJSClient/' + package_json.version; + config.logger = (config.logger || + new winston.Logger({ + level: 'info', + transports: [ + new (winston.transports.Console)(({ + formatter: function(options) { + return '[LaunchDarkly] ' + (options.message ? options.message : ''); + } + })), + ] + }) + ); + + checkDeprecatedOptions(config); + + config = applyDefaults(config, defaults); + + config.baseUri = canonicalizeUri(config.baseUri); + config.streamUri = canonicalizeUri(config.streamUri); + config.eventsUri = canonicalizeUri(config.eventsUri); + config.pollInterval = config.pollInterval > 30 ? config.pollInterval : 30; + + return config; + } + + return { + validate: validate + }; +})(); diff --git a/evaluate_flag.js b/evaluate_flag.js index 94b2974..c89e418 100644 --- a/evaluate_flag.js +++ b/evaluate_flag.js @@ -11,50 +11,38 @@ var noop = function(){}; function evaluate(flag, user, featureStore, cb) { cb = cb || noop; if (!user || user.key === null || user.key === undefined) { - cb(null, null, null); + cb(null, null, null, null); return; } if (!flag) { - cb(null, null, null); + cb(null, null, null, null); return; } if (!flag.on) { // Return the off variation if defined and valid - if (flag.offVariation != null) { - cb(null, get_variation(flag, flag.offVariation), null); - } - // Otherwise, return the default variation - else { - cb(null, null, null); - } + cb(null, flag.offVariation, getVariation(flag, flag.offVariation), null); return; } - eval_internal(flag, user, featureStore, [], function(err, result, events) { + evalInternal(flag, user, featureStore, [], function(err, variation, value, events) { if (err) { - cb(err, result, events); + cb(err, variation, value, events); return; } - if (result === null) { + if (variation === null) { // Return the off variation if defined and valid - if (flag.offVariation != null) { - cb(null, get_variation(flag, flag.offVariation), events); - } - // Otherwise, return the default variation - else { - cb(null, null, events); - } + cb(null, flag.offVariation, getVariation(flag, flag.offVariation), events); } else { - cb(err, result, events); + cb(err, variation, value, events); } }); return; } -function eval_internal(flag, user, featureStore, events, cb) { +function evalInternal(flag, user, featureStore, events, cb) { // Evaluate prerequisites, if any if (flag.prerequisites) { async.mapSeries(flag.prerequisites, @@ -66,12 +54,11 @@ function eval_internal(flag, user, featureStore, events, cb) { callback(new Error("Unsatisfied prerequisite"), null); return; } - eval_internal(f, user, featureStore, events, function(err, value) { + evalInternal(f, user, featureStore, events, function(err, variation, value) { // 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 - var variation = get_variation(f, prereq.variation); - events.push(create_flag_event(f.key, user, value, null, f.version, flag.key)); - if (err || value === null || variation === null || value != variation) { + 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) } else { // The prerequisite was satisfied @@ -84,16 +71,16 @@ function eval_internal(flag, user, featureStore, events, cb) { // 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, events); + cb(null, null, null, events); return; } - evalRules(flag, user, featureStore, function(e, variation) { - cb(e, variation, events); + evalRules(flag, user, featureStore, function(e, variation, value) { + cb(e, variation, value, events); }); }) } else { - evalRules(flag, user, featureStore, function(e, variation) { - cb(e, variation, events); + evalRules(flag, user, featureStore, function(e, variation, value) { + cb(e, variation, value, events); }); } } @@ -113,8 +100,9 @@ function evalRules(flag, user, featureStore, cb) { for (j = 0; j < target.values.length; j++) { if (user.key === target.values[j]) { - variation = get_variation(flag, target.variation); - cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null, variation); + value = getVariation(flag, target.variation); + cb(value === null ? new Error("Undefined variation for flag " + flag.key) : null, + target.variation, value); return; } } @@ -122,7 +110,7 @@ function evalRules(flag, user, featureStore, cb) { async.mapSeries(flag.rules, function(rule, callback) { - rule_match_user(rule, user, featureStore, function(matched) { + ruleMatchUser(rule, user, featureStore, function(matched) { setImmediate(callback, matched ? rule : null, null); }); }, @@ -131,18 +119,18 @@ function evalRules(flag, user, featureStore, cb) { // about the first match, and mapSeries terminates on the first "error") if (err) { var rule = err; - variation = variation_for_user(rule, user, flag); - cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null, variation); + variation = variationForUser(rule, user, flag); } else { // no rule matched; check the fallthrough - variation = variation_for_user(flag.fallthrough, user, flag); - cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null, variation); + variation = variationForUser(flag.fallthrough, user, flag); } + cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null, + variation, getVariation(flag, variation)); } ); } -function rule_match_user(r, user, featureStore, cb) { +function ruleMatchUser(r, user, featureStore, cb) { var i; if (!r.clauses) { @@ -152,7 +140,7 @@ function rule_match_user(r, user, featureStore, cb) { // A rule matches if all its clauses match async.mapSeries(r.clauses, function(clause, callback) { - clause_match_user(clause, user, featureStore, function(matched) { + clauseMatchUser(clause, user, featureStore, function(matched) { // on the first clause that does *not* match, we raise an "error" to stop the loop setImmediate(callback, matched ? null : clause, null); }); @@ -163,12 +151,12 @@ function rule_match_user(r, user, featureStore, cb) { ); } -function clause_match_user(c, user, featureStore, cb) { +function clauseMatchUser(c, user, featureStore, cb) { if (c.op == 'segmentMatch') { async.mapSeries(c.values, function(value, callback) { featureStore.get(dataKind.segments, value, function(segment) { - if (segment && segment_match_user(segment, user)) { + if (segment && segmentMatchUser(segment, user)) { // on the first segment that matches, we raise an "error" to stop the loop callback(segment, null); } else { @@ -178,20 +166,20 @@ function clause_match_user(c, user, featureStore, cb) { }, function(err, results) { // an "error" indicates that a segment *did* match - cb(maybe_negate(c, !!err)); + cb(maybeNegate(c, !!err)); } ); } else { - cb(clause_match_user_no_segments(c, user)); + cb(clauseMatchUserNoSegments(c, user)); } } -function clause_match_user_no_segments(c, user) { +function clauseMatchUserNoSegments(c, user) { var uValue; var matchFn; var i; - uValue = user_value(user, c.attribute); + uValue = userValue(user, c.attribute); if (uValue === null || uValue === undefined) { return false; @@ -202,17 +190,17 @@ function clause_match_user_no_segments(c, user) { // The user's value is an array if (Array === uValue.constructor) { for (i = 0; i < uValue.length; i++) { - if (match_any(matchFn, uValue[i], c.values)) { - return maybe_negate(c, true); + if (matchAny(matchFn, uValue[i], c.values)) { + return maybeNegate(c, true); } } - return maybe_negate(c, false); + return maybeNegate(c, false); } - return maybe_negate(c, match_any(matchFn, uValue, c.values)); + return maybeNegate(c, matchAny(matchFn, uValue, c.values)); } -function segment_match_user(segment, user) { +function segmentMatchUser(segment, user) { if (user.key) { if ((segment.included || []).indexOf(user.key) >= 0) { return true; @@ -221,7 +209,7 @@ function segment_match_user(segment, user) { return false; } for (var i = 0; i < (segment.rules || []).length; i++) { - if (segment_rule_match_user(segment.rules[i], user, segment.key, segment.salt)) { + if (segmentRuleMatchUser(segment.rules[i], user, segment.key, segment.salt)) { return true; } } @@ -229,9 +217,9 @@ function segment_match_user(segment, user) { return false; } -function segment_rule_match_user(rule, user, segmentKey, salt) { +function segmentRuleMatchUser(rule, user, segmentKey, salt) { for (var i = 0; i < (rule.clauses || []).length; i++) { - if (!clause_match_user_no_segments(rule.clauses[i], user)) { + if (!clauseMatchUserNoSegments(rule.clauses[i], user)) { return false; } } @@ -242,12 +230,12 @@ function segment_rule_match_user(rule, user, segmentKey, salt) { } // All of the clauses are met. See if the user buckets in - var bucket = bucket_user(user, segmentKey, rule.bucketBy || "key", salt); + var bucket = bucketUser(user, segmentKey, rule.bucketBy || "key", salt); var weight = rule.weight / 100000.0; return bucket < weight; } -function maybe_negate(c, b) { +function maybeNegate(c, b) { if (c.negate) { return !b; } else { @@ -255,7 +243,7 @@ function maybe_negate(c, b) { } } -function match_any(matchFn, value, values) { +function matchAny(matchFn, value, values) { var i = 0; for (i = 0; i < values.length; i++) { @@ -269,8 +257,8 @@ function match_any(matchFn, value, values) { // Given an index, return the variation value, or null if // the index is invalid -function get_variation(flag, index) { - if (index >= flag.variations.length) { +function getVariation(flag, index) { + if (index === null || index === undefined || index >= flag.variations.length) { return null; } else { return flag.variations[index]; @@ -279,7 +267,7 @@ function get_variation(flag, index) { // Given a variation or rollout 'r', select // the variation for the given user -function variation_for_user(r, user, flag) { +function variationForUser(r, user, flag) { var bucketBy; var bucket; var sum = 0; @@ -287,17 +275,17 @@ function variation_for_user(r, user, flag) { var variation; if (r.variation != null) { // This represets a fixed variation; return it - return get_variation(flag, r.variation); + return r.variation; } else if (r.rollout != null) { // This represents a percentage rollout. Assume // we're rolling out by key bucketBy = r.rollout.bucketBy != null ? r.rollout.bucketBy : "key"; - bucket = bucket_user(user, flag.key, bucketBy, flag.salt); + bucket = bucketUser(user, flag.key, bucketBy, flag.salt); for (i = 0; i < r.rollout.variations.length; i++) { variate = r.rollout.variations[i]; sum += variate.weight / 100000.0; if (bucket < sum) { - return get_variation(flag, variate.variation); + return variate.variation; } } } @@ -307,7 +295,7 @@ function variation_for_user(r, user, flag) { // Fetch an attribute value from a user object. Automatically // navigates into the custom array when necessary -function user_value(user, attr) { +function userValue(user, attr) { if (builtins.indexOf(attr) >= 0 && user.hasOwnProperty(attr)) { return user[attr]; } @@ -318,11 +306,11 @@ function user_value(user, attr) { } // Compute a percentile for a user -function bucket_user(user, key, attr, salt) { +function bucketUser(user, key, attr, salt) { var uValue; var idHash; - idHash = bucketable_string_value(user_value(user, attr)); + idHash = bucketableStringValue(userValue(user, attr)); if (idHash === null) { return 0; @@ -339,7 +327,7 @@ function bucket_user(user, key, attr, salt) { return result; } -function bucketable_string_value(value) { +function bucketableStringValue(value) { if (typeof(value) === 'string') { return value; } @@ -349,17 +337,20 @@ function bucketable_string_value(value) { return null; } -function create_flag_event(key, user, value, default_val, version, prereqOf) { +function createFlagEvent(key, flag, user, variation, value, defaultVal, prereqOf) { return { "kind": "feature", "key": key, "user": user, + "variation": variation, "value": value, - "default": default_val, + "default": defaultVal, "creationDate": new Date().getTime(), - "version": version, - "prereqOf": prereqOf + "version": flag ? flag.version : null, + "prereqOf": prereqOf, + "trackEvents": flag ? flag.trackEvents : null, + "debugEventsUntilDate": flag ? flag.debugEventsUntilDate : null }; } -module.exports = {evaluate: evaluate, bucket_user: bucket_user, create_flag_event: create_flag_event}; \ No newline at end of file +module.exports = {evaluate: evaluate, bucketUser: bucketUser, createFlagEvent: createFlagEvent}; \ No newline at end of file diff --git a/event_processor.js b/event_processor.js new file mode 100644 index 0000000..ec98690 --- /dev/null +++ b/event_processor.js @@ -0,0 +1,238 @@ +var LRUCache = require('lrucache'); +var request = require('request'); +var EventSummarizer = require('./event_summarizer'); +var UserFilter = require('./user_filter'); +var errors = require('./errors'); +var wrapPromiseCallback = require('./utils/wrapPromiseCallback'); + +function EventProcessor(sdkKey, config, errorReporter) { + var ep = {}; + + var userFilter = UserFilter(config), + summarizer = EventSummarizer(config), + userKeysCache = LRUCache(config.userKeysCapacity), + queue = [], + lastKnownPastTime = 0, + exceededCapacity = false, + shutdown = false, + flushTimer, + flushUsersTimer; + + function enqueue(event) { + if (queue.length < config.capacity) { + queue.push(event); + exceededCapacity = false; + } else { + if (!exceededCapacity) { + exceededCapacity = true; + config.logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); + } + } + } + + function shouldDebugEvent(event) { + if (event.debugEventsUntilDate) { + if (event.debugEventsUntilDate > lastKnownPastTime && + event.debugEventsUntilDate > new Date().getTime()) { + return true; + } + } + return false; + } + + function makeOutputEvent(event) { + switch (event.kind) { + case 'feature': + debug = !!event.debug; + var out = { + kind: debug ? 'debug' : 'feature', + creationDate: event.creationDate, + key: event.key, + value: event.value, + default: event.default, + prereqOf: event.prereqOf + }; + if (event.variation !== undefined && event.variation !== null) { + out.variation = event.variation; + } + if (event.version) { + out.version = event.version; + } + if (config.inlineUsersInEvents || debug) { + out.user = userFilter.filterUser(event.user); + } else { + out.userKey = event.user && event.user.key; + } + return out; + case 'identify': + return { + kind: 'identify', + creationDate: event.creationDate, + key: event.user && event.user.key, + user: userFilter.filterUser(event.user) + }; + case 'custom': + var out = { + kind: 'custom', + creationDate: event.creationDate, + key: event.key, + data: event.data + }; + if (config.inlineUsersInEvents) { + out.user = userFilter.filterUser(event.user); + } else { + out.userKey = event.user && event.user.key; + } + return out; + default: + return event; + } + } + + ep.sendEvent = function(event) { + var addIndexEvent = false, + addFullEvent = false, + addDebugEvent = false; + + if (shutdown) { + return; + } + config.logger.debug("Sending event", JSON.stringify(event)); + + // Always record the event in the summarizer. + summarizer.summarizeEvent(event); + + // Decide whether to add the event to the payload. Feature events may be added twice, once for + // the event (if tracked) and once for debugging. + if (event.kind === 'feature') { + addFullEvent = event.trackEvents; + addDebugEvent = shouldDebugEvent(event); + } else { + addFullEvent = true; + } + + // For each user we haven't seen before, we add an index event - unless this is already + // an identify event for that user. + if (!addFullEvent || !config.inlineUsersInEvents) { + if (event.user && !userKeysCache.get(event.user.key)) { + userKeysCache.set(event.user.key, true); + if (event.kind != 'identify') { + addIndexEvent = true; + } + } + } + + if (addIndexEvent) { + enqueue({ + kind: 'index', + creationDate: event.creationDate, + user: userFilter.filterUser(event.user) + }); + } + if (addFullEvent) { + enqueue(makeOutputEvent(event)); + } + if (addDebugEvent) { + var debugEvent = Object.assign({}, event, { debug: true }); + enqueue(makeOutputEvent(debugEvent)); + } + } + + ep.flush = function(callback) { + return wrapPromiseCallback(new Promise(function(resolve, reject) { + var worklist; + var summary; + + if (shutdown) { + var err = new errors.LDInvalidSDKKeyError("Events cannot be posted because SDK key is invalid"); + reject(err); + return; + } + + worklist = queue; + queue = []; + summary = summarizer.getSummary(); + summarizer.clearSummary(); + if (Object.keys(summary.features).length) { + summary.kind = 'summary'; + worklist.push(summary); + } + + if (!worklist.length) { + resolve(); + return; + } + + config.logger.debug("Flushing %d events", worklist.length); + + tryPostingEvents(worklist, resolve, reject, true); + }.bind(this)), callback); + } + + function tryPostingEvents(events, resolve, reject, canRetry) { + var retryOrReject = function(err) { + if (canRetry) { + config.logger && config.logger.warn("Will retry posting events after 1 second"); + setTimeout(function() { + tryPostingEvents(events, resolve, reject, false); + }, 1000); + } else { + reject(err); + } + } + + request({ + method: "POST", + url: config.eventsUri + '/bulk', + headers: { + 'Authorization': sdkKey, + 'User-Agent': config.userAgent, + 'X-LaunchDarkly-Event-Schema': '3' + }, + json: true, + body: events, + timeout: config.timeout * 1000, + agent: config.proxyAgent + }).on('response', function(resp, body) { + if (resp.headers['date']) { + var date = Date.parse(resp.headers['date']); + if (date) { + lastKnownPastTime = date; + } + } + if (resp.statusCode > 204) { + var err = new errors.LDUnexpectedResponseError("Unexpected status code " + resp.statusCode + "; events may not have been processed", + resp.statusCode); + errorReporter && errorReporter(err); + if (resp.statusCode === 401) { + reject(err); + var err1 = new errors.LDInvalidSDKKeyError("Received 401 error, no further events will be posted since SDK key is invalid"); + errorReporter && errorReporter(err1); + shutdown = true; + } else if (resp.statusCode >= 500) { + retryOrReject(err); + } + } else { + resolve(resp, body); + } + }).on('error', function(err) { + retryOrReject(err); + }); + } + + ep.close = function() { + clearInterval(flushTimer); + clearInterval(flushUsersTimer); + } + + flushTimer = setInterval(function() { + ep.flush(); + }, config.flushInterval * 1000); + flushUsersTimer = setInterval(function() { + userKeysCache.removeAll(); + }, config.userKeysFlushInterval * 1000); + + return ep; +} + +module.exports = EventProcessor; diff --git a/event_summarizer.js b/event_summarizer.js new file mode 100644 index 0000000..aab679d --- /dev/null +++ b/event_summarizer.js @@ -0,0 +1,76 @@ + +function EventSummarizer(config) { + var es = {}; + + var startDate = 0, + endDate = 0, + counters = {}; + + es.summarizeEvent = function(event) { + if (event.kind === 'feature') { + var counterKey = event.key + ':' + (event.variation || '') + (event.version || ''); + var counterVal = counters[counterKey]; + if (counterVal) { + counterVal.count = counterVal.count + 1; + } else { + counters[counterKey] = { + count: 1, + key: event.key, + version: event.version, + variation: event.variation, + value: event.value, + default: event.default + }; + } + if (startDate === 0 || event.creationDate < startDate) { + startDate = event.creationDate; + } + if (event.creationDate > endDate) { + endDate = event.creationDate; + } + } + } + + es.getSummary = function() { + var flagsOut = {}; + for (var i in counters) { + var c = counters[i]; + var flag = flagsOut[c.key]; + if (!flag) { + flag = { + default: c.default, + counters: [] + }; + flagsOut[c.key] = flag; + } + counterOut = { + value: c.value, + count: c.count + }; + if (c.variation !== undefined && c.variation !== null) { + counterOut.variation = c.variation; + } + if (c.version) { + counterOut.version = c.version; + } else { + counterOut.unknown = true; + } + flag.counters.push(counterOut); + } + return { + startDate: startDate, + endDate: endDate, + features: flagsOut + }; + } + + es.clearSummary = function() { + startDate = 0; + endDate = 0; + counters = {}; + } + + return es; +} + +module.exports = EventSummarizer; diff --git a/feature_store.js b/feature_store.js index 35eebb0..c65a3d6 100644 --- a/feature_store.js +++ b/feature_store.js @@ -45,7 +45,7 @@ function InMemoryFeatureStore() { store.init = function(allData, cb) { this.allData = allData; - this.init_called = true; + this.initCalled = true; callbackResult(cb); } @@ -89,7 +89,7 @@ function InMemoryFeatureStore() { } store.initialized = function(cb) { - callbackResult(cb, this.init_called === true); + callbackResult(cb, this.initCalled === true); } store.close = function() { diff --git a/index.d.ts b/index.d.ts index 286a68b..6703d27 100644 --- a/index.d.ts +++ b/index.d.ts @@ -44,7 +44,7 @@ declare module "ldclient-node" { * This is used for enterprise customers with their own LaunchDarkly instances. * Most users should use the default value. */ - base_uri?: string; + baseUri?: string; /** * The stream uri for the LaunchDarkly server. @@ -52,7 +52,7 @@ declare module "ldclient-node" { * This is used for enterprise customers with their own LaunchDarkly instances. * Most users should use the default value. */ - stream_uri?: string; + streamUri?: string; /** * The events uri for the LaunchDarkly server. @@ -60,7 +60,7 @@ declare module "ldclient-node" { * This is used for enterprise customers with their own LaunchDarkly instances. * Most users should use the default value. */ - events_uri?: string; + eventsUri?: string; /** * In seconds, controls the request timeout to LaunchDarkly. @@ -85,34 +85,34 @@ declare module "ldclient-node" { * * The SDK provides an in memory feature store as well as a redis feature store. */ - feature_store?: LDFeatureStore; + featureStore?: LDFeatureStore; /** * In seconds, controls how long LaunchDarkly buffers events before sending them back to our server. */ - flush_interval?: number; + flushInterval?: number; /** * In seconds, controls the time between polling requests. */ - poll_interval?: number; + pollInterval?: number; /** * Allows you to specify a host for an optional HTTP proxy. */ - proxy_host?: string; + proxyHost?: string; /** * Allows you to specify a port for an optional HTTP proxy. * Both the host and port must be specified to enable proxy support. */ - proxy_port?: string; + proxyPort?: string; /** * Allows you to specify basic authentication parameters for an optional HTTP proxy. * Usually of the form username:password. */ - proxy_auth?: string; + proxyAuth?: string; /** * Whether the client should be initialized in offline mode. @@ -127,12 +127,12 @@ declare module "ldclient-node" { /** * Whether to rely on LDD for feature updates. */ - use_ldd?: boolean; + useLdd?: boolean; /** * Whether to send events back to LaunchDarkly */ - send_events?: boolean; + sendEvents?: boolean; } /** @@ -380,7 +380,7 @@ declare module "ldclient-node" { * The node style callback to receive the variation result. * @returns a Promise containing the set of all flag values for a user */ - all_flags: (user: LDUser, callback?: (err: any, res: LDFlagSet) => void) => Promise; + allFlags: (user: LDUser, callback?: (err: any, res: LDFlagSet) => void) => Promise; /** * @@ -393,7 +393,7 @@ declare module "ldclient-node" { * * @returns The hash. */ - secure_mode_hash: (user: LDUser) => string; + secureModeHash: (user: LDUser) => string; /** * Close the update processor as well as the attached feature store. @@ -405,7 +405,7 @@ declare module "ldclient-node" { * * @returns Whether the client is configured in offline mode. */ - is_offline: () => boolean; + isOffline: () => boolean; /** * Track page events to use in goals or A/B tests. @@ -440,7 +440,7 @@ declare module "ldclient-node" { * Flush the queue * * Internally, the LaunchDarkly SDK keeps an event queue for track and identify calls. - * These are flushed periodically (see configuration option: flush_interval) + * These are flushed periodically (see configuration option: flushInterval) * and when the queue size limit (see configuration option: capacity) is reached. * * @returns a Promise which resolves once flushing is finished diff --git a/index.js b/index.js index 6eda8dc..59c22e0 100644 --- a/index.js +++ b/index.js @@ -1,19 +1,17 @@ -var request = require('request'); var FeatureStoreEventWrapper = require('./feature_store_event_wrapper'); -var InMemoryFeatureStore = require('./feature_store'); var RedisFeatureStore = require('./redis_feature_store'); var Requestor = require('./requestor'); var EventEmitter = require('events').EventEmitter; -var EventSerializer = require('./event_serializer'); +var EventProcessor = require('./event_processor'); var PollingProcessor = require('./polling'); var StreamingProcessor = require('./streaming'); +var configuration = require('./configuration'); var evaluate = require('./evaluate_flag'); +var messages = require('./messages'); var tunnel = require('tunnel'); -var winston = require('winston'); var crypto = require('crypto'); var async = require('async'); var errors = require('./errors'); -var package_json = require('./package.json'); var wrapPromiseCallback = require('./utils/wrapPromiseCallback'); var dataKind = require('./versioned_data_kind'); @@ -33,95 +31,96 @@ function createErrorReporter(emitter, logger) { global.setImmediate = global.setImmediate || process.nextTick.bind(process); -var new_client = function(sdk_key, config) { +function NullEventProcessor() { + return { + sendEvent: function() {}, + flush: function(callback) { + return wrapPromiseCallback(Promise.resolve(), callback); + }, + close: function() {} + }; +} + +function NullUpdateProcessor() { + return { + start: function(callback) { + setImmediate(callback, null); + } + }; +} + +var newClient = function(sdkKey, config) { var client = new EventEmitter(), - init_complete = false, + initComplete = false, queue = [], requestor, - update_processor, - event_queue_shutdown = false, - flush_timer; - - config = Object.assign({}, config || {}); - config.user_agent = 'NodeJSClient/' + package_json.version; - - config.base_uri = (config.base_uri || 'https://app.launchdarkly.com').replace(/\/+$/, ""); - config.stream_uri = (config.stream_uri || 'https://stream.launchdarkly.com').replace(/\/+$/, ""); - config.events_uri = (config.events_uri || 'https://events.launchdarkly.com').replace(/\/+$/, ""); - config.stream = (typeof config.stream === 'undefined') ? true : config.stream; - config.send_events = (typeof config.send_events === 'undefined') ? true : config.send_events; - config.timeout = config.timeout || 5; - config.capacity = config.capacity || 1000; - config.flush_interval = config.flush_interval || 5; - config.poll_interval = config.poll_interval > 30 ? config.poll_interval : 30; + updateProcessor, + eventProcessor, + flushTimer; + + config = configuration.validate(config); + // Initialize global tunnel if proxy options are set - if (config.proxy_host && config.proxy_port ) { - config.proxy_agent = create_proxy_agent(config); + if (config.proxyHost && config.proxyPort ) { + config.proxyAgent = createProxyAgent(config); } - config.logger = (config.logger || - new winston.Logger({ - level: 'info', - transports: [ - new (winston.transports.Console)(({ - formatter: function(options) { - return '[LaunchDarkly] ' + (options.message ? options.message : ''); - } - })), - ] - }) - ); - config.private_attr_names = config.private_attr_names || []; - var featureStore = config.feature_store || InMemoryFeatureStore(); - config.feature_store = FeatureStoreEventWrapper(featureStore, client); + config.featureStore = FeatureStoreEventWrapper(config.featureStore, client); - var eventSerializer = EventSerializer(config); - var maybeReportError = createErrorReporter(client, config.logger); - if (!sdk_key && !config.offline) { + if (config.eventProcessor) { + eventProcessor = config.eventProcessor; + } else { + if (config.offline || !config.sendEvents) { + eventProcessor = NullEventProcessor(); + } else { + eventProcessor = EventProcessor(sdkKey, config, maybeReportError); + } + } + + if (!sdkKey && !config.offline) { throw new Error("You must configure the client with an SDK key"); } - if (!config.use_ldd && !config.offline) { - requestor = Requestor(sdk_key, config); + if (config.updateProcessor) { + updateProcessor = config.updateProcessor; + } else if (config.useLdd || config.offline) { + updateProcessor = NullUpdateProcessor(); + } else { + requestor = Requestor(sdkKey, config); if (config.stream) { config.logger.info("Initializing stream processor to receive feature flag updates"); - update_processor = StreamingProcessor(sdk_key, config, requestor); + updateProcessor = StreamingProcessor(sdkKey, config, requestor); } else { config.logger.info("Initializing polling processor to receive feature flag updates"); config.logger.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support"); - update_processor = PollingProcessor(config, requestor); + updateProcessor = PollingProcessor(config, requestor); } - update_processor.start(function(err) { - if (err) { - var error; - if ((err.status && err.status === 401) || (err.code && err.code === 401)) { - error = new Error("Authentication failed. Double check your SDK key."); - } else { - error = err; - } - - maybeReportError(error); - } else if (!init_complete) { - init_complete = true; - client.emit('ready'); - } - }); - } else { - process.nextTick(function() { - init_complete = true; - client.emit('ready'); - }); } + updateProcessor.start(function(err) { + if (err) { + var error; + if ((err.status && err.status === 401) || (err.code && err.code === 401)) { + error = new Error("Authentication failed. Double check your SDK key."); + } else { + error = err; + } + maybeReportError(error); + } else if (!initComplete) { + initComplete = true; + client.emit('ready'); + } + }); + client.initialized = function() { - return init_complete; - } + return initComplete; + }; client.waitUntilReady = function() { - if (init_complete){ + if (initComplete) { return Promise.resolve(); } @@ -130,55 +129,56 @@ var new_client = function(sdk_key, config) { }); }; - client.variation = function(key, user, default_val, callback) { + client.variation = function(key, user, defaultVal, callback) { return wrapPromiseCallback(new Promise(function(resolve, reject) { - sanitize_user(user); + sanitizeUser(user); var variationErr; - if (this.is_offline()) { + if (this.isOffline()) { config.logger.info("Variation called in offline mode. Returning default value."); - return resolve(default_val); + return resolve(defaultVal); } else if (!key) { variationErr = new errors.LDClientError('No feature flag key specified. Returning default value.'); maybeReportError(variationError); - send_flag_event(key, user, default_val, default_val); - return resolve(default_val); + sendFlagEvent(key, null, user, null, defaultVal, defaultVal); + return resolve(defaultVal); } - else if (!user) { - variationErr = new errors.LDClientError('No user specified. Returning default value.'); - maybeReportError(variationErr); - send_flag_event(key, user, default_val, default_val); - return resolve(default_val); - } - - else if (user.key === "") { + else if (user && user.key === "") { config.logger.warn("User key is blank. Flag evaluation will proceed, but the user will not be stored in LaunchDarkly"); } - if (!init_complete) { - config.feature_store.initialized(function(storeInited) { - if (config.feature_store.initialized()) { + if (!initComplete) { + config.featureStore.initialized(function(storeInited) { + if (storeInited) { config.logger.warn("Variation called before LaunchDarkly client initialization completed (did you wait for the 'ready' event?) - using last known values from feature store") - variationInternal(key, user, default_val, resolve, reject); + variationInternal(key, user, defaultVal, resolve, reject); } else { variationErr = new errors.LDClientError("Variation called before LaunchDarkly client initialization completed (did you wait for the 'ready' event?) - using default value"); maybeReportError(variationErr); - send_flag_event(key, user, default_val, default_val); - return resolve(default_val); + sendFlagEvent(key, null, user, null, defaultVal, defaultVal); + return resolve(defaultVal); } }); + return; } - variationInternal(key, user, default_val, resolve, reject); + variationInternal(key, user, defaultVal, resolve, reject); }.bind(this)), callback); } - function variationInternal(key, user, default_val, resolve, reject) { - config.feature_store.get(dataKind.features, key, function(flag) { - evaluate.evaluate(flag, user, config.feature_store, function(err, result, events) { + function variationInternal(key, user, defaultVal, resolve, reject) { + config.featureStore.get(dataKind.features, key, function(flag) { + if (!user) { + variationErr = new errors.LDClientError('No user specified. Returning default value.'); + maybeReportError(variationErr); + sendFlagEvent(key, flag, user, null, defaultVal, defaultVal); + return resolve(defaultVal); + } + + evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) { var i; var version = flag ? flag.version : null; @@ -190,43 +190,43 @@ var new_client = function(sdk_key, config) { // have already been constructed, so we just have to push them onto the queue. if (events) { for (i = 0; i < events.length; i++) { - enqueue(events[i]); + eventProcessor.sendEvent(events[i]); } } - if (result === null) { + if (value === null) { config.logger.debug("Result value is null in variation"); - send_flag_event(key, user, default_val, default_val, version); - return resolve(default_val); + sendFlagEvent(key, flag, user, null, defaultVal, defaultVal); + return resolve(defaultVal); } else { - send_flag_event(key, user, result, default_val, version); - return resolve(result); + sendFlagEvent(key, flag, user, variation, value, defaultVal); + return resolve(value); } }); }); } - client.toggle = function(key, user, default_val, callback) { + client.toggle = function(key, user, defaultVal, callback) { config.logger.warn("toggle() is deprecated. Call 'variation' instead"); - return client.variation(key, user, default_val, callback); + return client.variation(key, user, defaultVal, callback); } - client.all_flags = function(user, callback) { + client.allFlags = function(user, callback) { return wrapPromiseCallback(new Promise(function(resolve, reject) { - sanitize_user(user); + sanitizeUser(user); var results = {}; - if (this.is_offline() || !user) { - config.logger.info("all_flags() called in offline mode. Returning empty map."); + if (this.isOffline() || !user) { + config.logger.info("allFlags() called in offline mode. Returning empty map."); return resolve({}); } - config.feature_store.all(dataKind.features, function(flags) { - async.forEachOf(flags, function(flag, key, iteratee_cb) { + 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.feature_store, function(err, result, events) { - results[key] = result; - setImmediate(iteratee_cb); + evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) { + results[key] = value; + setImmediate(iterateeCb); }) }, function(err) { return err ? reject(err) : resolve(results); @@ -235,26 +235,27 @@ var new_client = function(sdk_key, config) { }.bind(this)), callback); } - client.secure_mode_hash = function(user) { - var hmac = crypto.createHmac('sha256', sdk_key); + client.secureModeHash = function(user) { + var hmac = crypto.createHmac('sha256', sdkKey); hmac.update(user.key); return hmac.digest('hex'); } client.close = function() { - if (update_processor) { - update_processor.close(); + eventProcessor.close(); + if (updateProcessor) { + updateProcessor.close(); } - config.feature_store.close(); - clearInterval(flush_timer); + config.featureStore.close(); + clearInterval(flushTimer); } - client.is_offline = function() { + client.isOffline = function() { return config.offline; } client.track = function(eventName, user, data) { - sanitize_user(user); + sanitizeUser(user); var event = {"key": eventName, "user": user, "kind": "custom", @@ -264,114 +265,70 @@ var new_client = function(sdk_key, config) { event.data = data; } - enqueue(event); + eventProcessor.sendEvent(event); }; client.identify = function(user) { - sanitize_user(user); + sanitizeUser(user); var event = {"key": user.key, "kind": "identify", "user": user, "creationDate": new Date().getTime()}; - enqueue(event); + eventProcessor.sendEvent(event); }; client.flush = function(callback) { - return wrapPromiseCallback(new Promise(function(resolve, reject) { - var worklist; - if (event_queue_shutdown) { - var err = new errors.LDInvalidSDKKeyError("Events cannot be posted because SDK key is invalid"); - reject(err); - return; - } else if (!queue.length) { - resolve(); - return; - } - - worklist = eventSerializer.serialize_events(queue.slice(0)); - queue = []; - - config.logger.debug("Flushing %d events", worklist.length); - - request({ - method: "POST", - url: config.events_uri + '/bulk', - headers: { - 'Authorization': sdk_key, - 'User-Agent': config.user_agent - }, - json: true, - body: worklist, - timeout: config.timeout * 1000, - agent: config.proxy_agent - }).on('response', function(resp, body) { - if (resp.statusCode > 204) { - var err = new errors.LDUnexpectedResponseError("Unexpected status code " + resp.statusCode + "; events may not have been processed", - resp.statusCode); - maybeReportError(err); - reject(err); - if (resp.statusCode === 401) { - var err1 = new errors.LDInvalidSDKKeyError("Received 401 error, no further events will be posted since SDK key is invalid"); - maybeReportError(err1); - event_queue_shutdown = true; - } - } else { - resolve(resp, body); - } - }).on('error', reject); - }.bind(this)), callback); + return eventProcessor.flush(callback); }; - function enqueue(event) { - if (config.offline || !config.send_events || event_queue_shutdown) { - return; - } - - config.logger.debug("Sending flag event", JSON.stringify(event)); - queue.push(event); - - if (queue.length >= config.capacity) { - client.flush(); - } + function sendFlagEvent(key, flag, user, variation, value, defaultVal) { + var event = evaluate.createFlagEvent(key, flag, user, variation, value, defaultVal); + eventProcessor.sendEvent(event); } - function send_flag_event(key, user, value, default_val, version) { - var event = evaluate.create_flag_event(key, user, value, default_val, version); - enqueue(event); + function backgroundFlush() { + client.flush().then(function() {}, function() {}); } - function background_flush() { - client.flush().then(function() {}, function() {}); + function deprecatedMethod(oldName, newName) { + client[oldName] = function() { + config.logger.warn(messages.deprecated(oldName, newName)); + return client[newName].apply(client, arguments); + }; } + + deprecatedMethod('all_flags', 'allFlags'); + deprecatedMethod('is_offline', 'isOffline'); + deprecatedMethod('secure_mode_hash', 'secureModeHash'); - flush_timer = setInterval(background_flush, config.flush_interval * 1000); + flushTimer = setInterval(backgroundFlush, config.flushInterval * 1000); return client; }; module.exports = { - init: new_client, + init: newClient, RedisFeatureStore: RedisFeatureStore, errors: errors }; -function create_proxy_agent(config) { +function createProxyAgent(config) { var options = { proxy: { - host: config.proxy_host, - port: config.proxy_port, - proxyAuth: config.proxy_auth + host: config.proxyHost, + port: config.proxyPort, + proxyAuth: config.proxyAuth } }; - if (config.proxy_scheme === 'https') { - if (!config.base_uri || config.base_uri.startsWith('https')) { + if (config.proxyScheme === 'https') { + if (!config.baseUri || config.baseUri.startsWith('https')) { return tunnel.httpsOverHttps(options); } else { return tunnel.httpOverHttps(options); } - } else if (!config.base_uri || config.base_uri.startsWith('https')) { + } else if (!config.baseUri || config.baseUri.startsWith('https')) { return tunnel.httpsOverHttp(options); } else { return tunnel.httpOverHttp(options); @@ -379,7 +336,7 @@ function create_proxy_agent(config) { } -function sanitize_user(u) { +function sanitizeUser(u) { if (!u) { return; } diff --git a/messages.js b/messages.js new file mode 100644 index 0000000..95ae5e9 --- /dev/null +++ b/messages.js @@ -0,0 +1,4 @@ + +exports.deprecated = function(oldName, newName) { + return '"' + oldName + '" is deprecated, please use "' + newName + '"'; +} diff --git a/operators.js b/operators.js index 439cc39..6032ad2 100644 --- a/operators.js +++ b/operators.js @@ -89,12 +89,12 @@ var operators = { "semVerGreaterThan": semVerOperator(function(a, b) { return a.compare(b) > 0; }) }; -var operator_none = function(a, b) { +var operatorNone = function(a, b) { return false; } function fn(op) { - return operators[op] || operator_none; + return operators[op] || operatorNone; } module.exports = {operators: operators, fn: fn}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index a984ea8..5dc67a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,13 @@ "integrity": "sha1-X6rZwsB/YN12dw9xzwJbYqY8/U4=", "dev": true }, + "abbrev": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.0.tgz", + "integrity": "sha1-0FVMIlZjbi9W58LlrRg/hZQo2B8=", + "dev": true, + "optional": true + }, "acorn": { "version": "5.5.3", "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", @@ -82,8 +89,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "ansi-styles": { "version": "3.2.1", @@ -371,6 +377,24 @@ "default-require-extensions": "1.0.0" } }, + "aproba": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.1.1.tgz", + "integrity": "sha1-ldNgDwdxCqDpKYxyatXs8urLq6s=", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true, + "requires": { + "delegates": "1.0.0", + "readable-stream": "2.3.5" + } + }, "argparse": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", @@ -429,6 +453,12 @@ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -442,9 +472,9 @@ "dev": true }, "async": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/async/-/async-2.5.0.tgz", - "integrity": "sha512-e+lJAJeNWuPCNyxZKOBdaJGyLGHugXVQtrAwtuAe2vhxTYxFTKE73p8JuTmdH0qdQZtDvI4dhJwjZc5zsfIsYw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.0.tgz", + "integrity": "sha512-xAfGg1/NTLBBKlHFmnd7PlmUW9KhVQIUuSrYem9xzFUZy13ScvtyGGejaae9iAVRiRq9+Cx7DPFaAAhCpyxyPw==", "requires": { "lodash": "4.17.4" } @@ -759,12 +789,21 @@ "tweetnacl": "0.14.5" } }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "dev": true, + "requires": { + "inherits": "2.0.3" + } + }, "boom": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/boom/-/boom-4.3.1.tgz", "integrity": "sha1-T4owBctKfjiJ90kDD9JbluAdLjE=", "requires": { - "hoek": "4.2.0" + "hoek": "4.2.1" } }, "brace-expansion": { @@ -872,6 +911,20 @@ "lazy-cache": "1.0.4" } }, + "chai": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", + "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", + "dev": true, + "requires": { + "assertion-error": "1.1.0", + "check-error": "1.0.2", + "deep-eql": "3.0.1", + "get-func-name": "2.0.0", + "pathval": "1.1.0", + "type-detect": "4.0.8" + } + }, "chalk": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.2.tgz", @@ -883,6 +936,12 @@ "supports-color": "5.3.0" } }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "ci-info": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.3.tgz", @@ -1009,8 +1068,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collection-visit": { "version": "1.0.0", @@ -1068,6 +1126,12 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, "content-type-parser": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/content-type-parser/-/content-type-parser-1.0.2.tgz", @@ -1121,7 +1185,7 @@ "resolved": "https://registry.npmjs.org/boom/-/boom-5.2.0.tgz", "integrity": "sha512-Z5BTk6ZRe4tXXQlkqftmsAUANpXmuwlsF5Oov8ThoMbQRzdGTA1ngYRW160GexgOgjsFOKJz0LYhoNi+2AMBUw==", "requires": { - "hoek": "4.2.0" + "hoek": "4.2.1" } } } @@ -1180,6 +1244,28 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=", + "dev": true + }, + "deep-extend": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.4.2.tgz", + "integrity": "sha1-SLaZwn4zS/ifEIkr5DL25MfTSn8=", + "dev": true, + "optional": true + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1228,6 +1314,13 @@ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, "detect-indent": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-4.0.0.tgz", @@ -1237,6 +1330,13 @@ "repeating": "2.0.1" } }, + "detect-libc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.2.tgz", + "integrity": "sha1-ca1dIEvxempsqPRQxhRUBm70YeE=", + "dev": true, + "optional": true + }, "detect-newline": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", @@ -1584,355 +1684,79 @@ "node-pre-gyp": "0.6.39" }, "dependencies": { - "abbrev": { - "version": "1.1.0", - "bundled": true, - "dev": true, - "optional": true - }, "ajv": { "version": "4.11.8", - "bundled": true, - "dev": true, - "optional": true, + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", "requires": { "co": "4.6.0", "json-stable-stringify": "1.0.1" } }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "dev": true - }, - "aproba": { - "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "delegates": "1.0.0", - "readable-stream": "2.2.9" - } - }, - "asn1": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, "assert-plus": { "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "asynckit": { - "version": "0.4.0", - "bundled": true, - "dev": true, - "optional": true + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=" }, "aws-sign2": { "version": "0.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "aws4": { - "version": "1.6.0", - "bundled": true, - "dev": true, - "optional": true + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=" }, "balanced-match": { "version": "0.4.2", - "bundled": true, - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "0.14.5" - } - }, - "block-stream": { - "version": "0.0.9", - "bundled": true, - "dev": true, - "requires": { - "inherits": "2.0.3" - } + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" }, "boom": { "version": "2.10.1", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", "requires": { "hoek": "2.16.3" } }, - "brace-expansion": { - "version": "1.1.7", - "bundled": true, - "dev": true, - "requires": { - "balanced-match": "0.4.2", - "concat-map": "0.0.1" - } - }, - "buffer-shims": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "caseless": { - "version": "0.12.0", - "bundled": true, - "dev": true, - "optional": true - }, - "co": { - "version": "4.6.0", - "bundled": true, - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "combined-stream": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "requires": { - "delayed-stream": "1.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "dev": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "dev": true - }, "cryptiles": { "version": "2.0.5", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", "requires": { "boom": "2.10.1" } }, - "dashdash": { - "version": "1.14.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "debug": { - "version": "2.6.8", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "ms": "2.0.0" - } - }, - "deep-extend": { - "version": "0.4.2", - "bundled": true, - "dev": true, - "optional": true - }, - "delayed-stream": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "ecc-jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "extend": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, "extsprintf": { "version": "1.0.2", - "bundled": true, - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "bundled": true, - "dev": true, - "optional": true + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz", + "integrity": "sha1-4QgOBljjALBilJkMxw4VAiNf1VA=" }, "form-data": { "version": "2.1.4", - "bundled": true, - "dev": true, - "optional": true, + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", "requires": { "asynckit": "0.4.0", "combined-stream": "1.0.5", - "mime-types": "2.1.15" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "dev": true - }, - "fstream": { - "version": "1.0.11", - "bundled": true, - "dev": true, - "requires": { - "graceful-fs": "4.1.11", - "inherits": "2.0.3", - "mkdirp": "0.5.1", - "rimraf": "2.6.1" - } - }, - "fstream-ignore": { - "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "fstream": "1.0.11", - "inherits": "2.0.3", - "minimatch": "3.0.4" - } - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "aproba": "1.1.1", - "console-control-strings": "1.1.0", - "has-unicode": "2.0.1", - "object-assign": "4.1.1", - "signal-exit": "3.0.2", - "string-width": "1.0.2", - "strip-ansi": "3.0.1", - "wide-align": "1.1.2" - } - }, - "getpass": { - "version": "0.1.7", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "glob": { - "version": "7.1.2", - "bundled": true, - "dev": true, - "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.3", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "mime-types": "2.1.17" } }, - "graceful-fs": { - "version": "4.1.11", - "bundled": true, - "dev": true - }, "har-schema": { "version": "1.0.5", - "bundled": true, - "dev": true, - "optional": true + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=" }, "har-validator": { "version": "4.2.1", - "bundled": true, - "dev": true, - "optional": true, + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", "requires": { "ajv": "4.11.8", "har-schema": "1.0.5" } }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, "hawk": { "version": "3.1.3", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", "requires": { "boom": "2.10.1", "cryptiles": "2.0.5", @@ -1942,322 +1766,51 @@ }, "hoek": { "version": "2.16.3", - "bundled": true, - "dev": true + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=" }, "http-signature": { "version": "1.1.1", - "bundled": true, - "dev": true, - "optional": true, + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", "requires": { "assert-plus": "0.2.0", - "jsprim": "1.4.0", - "sshpk": "1.13.0" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "dev": true, - "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "jsprim": "1.4.1", + "sshpk": "1.13.1" } }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "dev": true - }, - "ini": { - "version": "1.3.4", - "bundled": true, - "dev": true, - "optional": true - }, "is-fullwidth-code-point": { "version": "1.0.0", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "requires": { "number-is-nan": "1.0.1" } }, - "is-typedarray": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true + "mime-db": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.27.0.tgz", + "integrity": "sha1-gg9XIpa70g7CXtVeW13oaeVDbrE=" }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "dev": true + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" }, - "isstream": { - "version": "0.1.2", - "bundled": true, - "dev": true, - "optional": true + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, - "jodid25519": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsbn": "0.1.1" - } - }, - "jsbn": { - "version": "0.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "bundled": true, - "dev": true, - "optional": true - }, - "json-stable-stringify": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "jsonify": "0.0.0" - } - }, - "json-stringify-safe": { - "version": "5.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "jsonify": { - "version": "0.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "jsprim": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.0.2", - "json-schema": "0.2.3", - "verror": "1.3.6" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "mime-db": { - "version": "1.27.0", - "bundled": true, - "dev": true - }, - "mime-types": { - "version": "2.1.15", - "bundled": true, - "dev": true, - "requires": { - "mime-db": "1.27.0" - } - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "dev": true, - "requires": { - "brace-expansion": "1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "node-pre-gyp": { - "version": "0.6.39", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "detect-libc": "1.0.2", - "hawk": "3.1.3", - "mkdirp": "0.5.1", - "nopt": "4.0.1", - "npmlog": "4.1.0", - "rc": "1.2.1", - "request": "2.81.0", - "rimraf": "2.6.1", - "semver": "5.3.0", - "tar": "2.2.1", - "tar-pack": "3.4.0" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "abbrev": "1.1.0", - "osenv": "0.1.4" - } - }, - "npmlog": { - "version": "4.1.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "are-we-there-yet": "1.1.4", - "console-control-strings": "1.1.0", - "gauge": "2.7.4", - "set-blocking": "2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "oauth-sign": { - "version": "0.8.2", - "bundled": true, - "dev": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "dev": true, - "requires": { - "wrappy": "1.0.2" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.4", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "os-homedir": "1.0.2", - "os-tmpdir": "1.0.2" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "dev": true - }, - "performance-now": { - "version": "0.2.0", - "bundled": true, - "dev": true, - "optional": true - }, - "process-nextick-args": { - "version": "1.0.7", - "bundled": true, - "dev": true - }, - "punycode": { - "version": "1.4.1", - "bundled": true, - "dev": true, - "optional": true - }, - "qs": { - "version": "6.4.0", - "bundled": true, - "dev": true, - "optional": true - }, - "rc": { - "version": "1.2.1", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "deep-extend": "0.4.2", - "ini": "1.3.4", - "minimist": "1.2.0", - "strip-json-comments": "2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.2.9", - "bundled": true, - "dev": true, - "requires": { - "buffer-shims": "1.0.0", - "core-util-is": "1.0.2", - "inherits": "2.0.3", - "isarray": "1.0.0", - "process-nextick-args": "1.0.7", - "string_decoder": "1.0.1", - "util-deprecate": "1.0.2" - } + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=" }, "request": { "version": "2.81.0", - "bundled": true, - "dev": true, - "optional": true, + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", "requires": { "aws-sign2": "0.6.0", "aws4": "1.6.0", @@ -2272,223 +1825,150 @@ "is-typedarray": "1.0.0", "isstream": "0.1.2", "json-stringify-safe": "5.0.1", - "mime-types": "2.1.15", + "mime-types": "2.1.17", "oauth-sign": "0.8.2", "performance-now": "0.2.0", "qs": "6.4.0", - "safe-buffer": "5.0.1", + "safe-buffer": "5.1.1", "stringstream": "0.0.5", - "tough-cookie": "2.3.2", + "tough-cookie": "2.3.3", "tunnel-agent": "0.6.0", - "uuid": "3.0.1" - } - }, - "rimraf": { - "version": "2.6.1", - "bundled": true, - "dev": true, - "requires": { - "glob": "7.1.2" + "uuid": "3.1.0" } }, - "safe-buffer": { - "version": "5.0.1", - "bundled": true, - "dev": true - }, "semver": { "version": "5.3.0", - "bundled": true, - "dev": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "dev": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "dev": true, - "optional": true + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" }, "sntp": { "version": "1.0.9", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", "requires": { "hoek": "2.16.3" } }, - "sshpk": { - "version": "1.13.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "asn1": "0.2.3", - "assert-plus": "1.0.0", - "bcrypt-pbkdf": "1.0.1", - "dashdash": "1.14.1", - "ecc-jsbn": "0.1.1", - "getpass": "0.1.7", - "jodid25519": "1.0.2", - "jsbn": "0.1.1", - "tweetnacl": "0.14.5" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "bundled": true, - "dev": true, - "optional": true - } - } - }, "string-width": { "version": "1.0.2", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "requires": { "code-point-at": "1.1.0", "is-fullwidth-code-point": "1.0.0", "strip-ansi": "3.0.1" } }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, - "stringstream": { - "version": "0.0.5", - "bundled": true, - "dev": true, - "optional": true - }, "strip-ansi": { "version": "3.0.1", - "bundled": true, - "dev": true, + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { "ansi-regex": "2.1.1" } }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "tar": { - "version": "2.2.1", - "bundled": true, - "dev": true, - "requires": { - "block-stream": "0.0.9", - "fstream": "1.0.11", - "inherits": "2.0.3" - } - }, - "tar-pack": { - "version": "3.4.0", - "bundled": true, - "dev": true, - "optional": true, - "requires": { - "debug": "2.6.8", - "fstream": "1.0.11", - "fstream-ignore": "1.0.5", - "once": "1.4.0", - "readable-stream": "2.2.9", - "rimraf": "2.6.1", - "tar": "2.2.1", - "uid-number": "0.0.6" - } - }, - "tough-cookie": { - "version": "2.3.2", - "bundled": true, - "dev": true, - "optional": true, + "verror": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz", + "integrity": "sha1-z/XfEpRtKX0rqu+qJoniW+AcAFw=", "requires": { - "punycode": "1.4.1" + "extsprintf": "1.0.2" } - }, - "tunnel-agent": { - "version": "0.6.0", - "bundled": true, + } + } + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "inherits": "2.0.3", + "mkdirp": "0.5.1", + "rimraf": "2.6.2" + } + }, + "fstream-ignore": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fstream-ignore/-/fstream-ignore-1.0.5.tgz", + "integrity": "sha1-nDHa40dnAY/h0kmyTa2mfQktoQU=", + "dev": true, + "optional": true, + "requires": { + "fstream": "1.0.11", + "inherits": "2.0.3", + "minimatch": "3.0.4" + } + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "1.1.1", + "console-control-strings": "1.1.0", + "has-unicode": "2.0.1", + "object-assign": "4.1.1", + "signal-exit": "3.0.2", + "string-width": "1.0.2", + "strip-ansi": "3.0.1", + "wide-align": "1.1.2" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "optional": true, "requires": { - "safe-buffer": "5.0.1" + "number-is-nan": "1.0.1" } }, - "tweetnacl": { - "version": "0.14.5", - "bundled": true, - "dev": true, - "optional": true - }, - "uid-number": { - "version": "0.0.6", - "bundled": true, - "dev": true, - "optional": true - }, - "util-deprecate": { + "string-width": { "version": "1.0.2", - "bundled": true, - "dev": true - }, - "uuid": { - "version": "3.0.1", - "bundled": true, - "dev": true, - "optional": true - }, - "verror": { - "version": "1.3.6", - "bundled": true, + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "optional": true, "requires": { - "extsprintf": "1.0.2" + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" } }, - "wide-align": { - "version": "1.1.2", - "bundled": true, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { - "string-width": "1.0.2" + "ansi-regex": "2.1.1" } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "dev": true } } }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, "get-caller-file": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.2.tgz", "integrity": "sha1-9wLmMSfn4jHBYKgMFVSstw1QR+U=", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", @@ -2627,6 +2107,13 @@ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2694,14 +2181,14 @@ "requires": { "boom": "4.3.1", "cryptiles": "3.1.2", - "hoek": "4.2.0", + "hoek": "4.2.1", "sntp": "2.0.2" } }, "hoek": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.0.tgz", - "integrity": "sha512-v0XCLxICi9nPfYrS9RL8HbYnXi9obYAeLbSP00BmnZwCK9+Ih9WOjoZ8YoHCoav2csqn4FOz4Orldsy2dmDwmQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.2.1.tgz", + "integrity": "sha512-QLg82fGkfnJ/4iy1xZ81/9SIJiq1NGFUMGs6ParyjBZr6jW2Ufj/snDqTHixNlHdPNwN2RLVD0Pi3igeK9+JfA==" }, "home-or-tmp": { "version": "2.0.0", @@ -2776,6 +2263,13 @@ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true }, + "ini": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.4.tgz", + "integrity": "sha1-BTfLedr1m1mhpRff9wbIbsA5Fi4=", + "dev": true, + "optional": true + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -3068,7 +2562,7 @@ "integrity": "sha512-duj6AlLcsWNwUpfyfHt0nWIeRiZpuShnP40YTxOGQgtaN8fd6JYSxsvxUphTDy8V5MfDXo4s/xVCIIvVCO808g==", "dev": true, "requires": { - "async": "2.5.0", + "async": "2.6.0", "compare-versions": "3.1.0", "fileset": "2.0.3", "istanbul-lib-coverage": "1.2.0", @@ -3133,7 +2627,7 @@ "babel-types": "6.26.0", "babylon": "6.18.0", "istanbul-lib-coverage": "1.2.0", - "semver": "5.4.1" + "semver": "5.5.0" } }, "istanbul-lib-report": { @@ -3600,7 +3094,7 @@ "nwmatcher": "1.4.4", "parse5": "4.0.0", "pn": "1.1.0", - "request": "2.83.0", + "request": "2.85.0", "request-promise-native": "1.0.5", "sax": "1.2.4", "symbol-tree": "3.2.2", @@ -3774,12 +3268,16 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.2.tgz", "integrity": "sha512-wgeVXhrDwAWnIF/yZARsFnMBtdFXOg1b8RIrhilp+0iDYN4mdQcNZElDZ0e4B64BhaxeQ5zN7PMyvu7we1kPeQ==", - "dev": true, "requires": { "pseudomap": "1.0.2", "yallist": "2.1.2" } }, + "lrucache": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/lrucache/-/lrucache-1.0.3.tgz", + "integrity": "sha1-Ox3tDRuoLhiLm9q6nu5khvhkpDQ=" + }, "makeerror": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", @@ -3958,20 +3456,54 @@ "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", + "dev": true + } + } + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, + "nock": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/nock/-/nock-9.2.3.tgz", + "integrity": "sha512-4XYNSJDJ/PvNoH+cCRWcGOOFsq3jtZdNTRIlPIBA7CopGWJO56m5OaPEjjJ3WddxNYfe5HL9sQQAtMt8oyR9AA==", + "dev": true, + "requires": { + "chai": "4.1.2", + "debug": "3.1.0", + "deep-equal": "1.0.1", + "json-stringify-safe": "5.0.1", + "lodash": "4.17.10", + "mkdirp": "0.5.1", + "propagate": "1.0.0", + "qs": "6.5.1", + "semver": "5.5.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==", "dev": true } } }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, "node-cache": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-3.2.1.tgz", @@ -3994,16 +3526,206 @@ "dev": true, "requires": { "growly": "1.3.0", - "semver": "5.4.1", + "semver": "5.5.0", "shellwords": "0.1.1", "which": "1.3.0" } }, + "node-pre-gyp": { + "version": "0.6.39", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.6.39.tgz", + "integrity": "sha512-OsJV74qxnvz/AMGgcfZoDaeDXKD3oY3QVIbBmwszTFkRisTSXbMQyn4UWzUMOtA5SVhrBZOTp0wcoSBgfMfMmQ==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "1.0.2", + "hawk": "3.1.3", + "mkdirp": "0.5.1", + "nopt": "4.0.1", + "npmlog": "4.1.0", + "rc": "1.2.1", + "request": "2.81.0", + "rimraf": "2.6.2", + "semver": "5.5.0", + "tar": "2.2.1", + "tar-pack": "3.4.0" + }, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.10.1" + } + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.5", + "mime-types": "2.1.17" + } + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.5", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.17", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.3", + "tunnel-agent": "0.6.0", + "uuid": "3.1.0" + } + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + } + } + }, "node-sha1": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/node-sha1/-/node-sha1-0.0.1.tgz", "integrity": "sha1-VmL8eZ8cPJXZPjAV3QSAZVFJhdM=" }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1.1.0", + "osenv": "0.1.4" + } + }, "normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -4012,7 +3734,7 @@ "requires": { "hosted-git-info": "2.6.0", "is-builtin-module": "1.0.0", - "semver": "5.4.1", + "semver": "5.5.0", "validate-npm-package-license": "3.0.3" } }, @@ -4034,11 +3756,23 @@ "path-key": "2.0.1" } }, + "npmlog": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.0.tgz", + "integrity": "sha512-ocolIkZYZt8UveuiDS0yAkkIjid1o7lPG8cYm05yNYzBn8ykQtaiPMEGp8fY9tKdDgm8okpdKzkvu1y9hUYugA==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "1.1.4", + "console-control-strings": "1.1.0", + "gauge": "2.7.4", + "set-blocking": "2.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "nwmatcher": { "version": "1.4.4", @@ -4248,6 +3982,17 @@ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "osenv": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.4.tgz", + "integrity": "sha1-Qv5tWVPfBsgGS+bxdsPQWqqjRkQ=", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "1.0.2", + "os-tmpdir": "1.0.2" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -4346,6 +4091,12 @@ "pinkie-promise": "2.0.1" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4435,6 +4186,12 @@ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", "dev": true }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -4496,6 +4253,28 @@ } } }, + "rc": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.1.tgz", + "integrity": "sha1-LgPo5C7kULjLPc5lvhv4l04d/ZU=", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "0.4.2", + "ini": "1.3.4", + "minimist": "1.2.0", + "strip-json-comments": "2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", @@ -4635,9 +4414,9 @@ } }, "request": { - "version": "2.83.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.83.0.tgz", - "integrity": "sha512-lR3gD69osqm6EYLk9wB/G1W/laGWjzH90t1vEa2xuxHD5KUrSzp9pUSfTm+YC5Nxt2T8nMPEvKlhbQayU7bgFw==", + "version": "2.85.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.85.0.tgz", + "integrity": "sha512-8H7Ehijd4js+s6wuVPLjwORxD4zeuyjYugprdOXlPSqaApmL/QOy+EB/beICHVCHkGMKNh5rvihb5ov+IDw4mg==", "requires": { "aws-sign2": "0.7.0", "aws4": "1.6.0", @@ -4670,19 +4449,8 @@ "requires": { "lodash.assign": "4.2.0", "lodash.clonedeep": "4.5.0", - "lru-cache": "4.1.1", - "request": "2.83.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.1.tgz", - "integrity": "sha512-q4spe4KTfsAS1SUHLO0wz8Qiyf1+vMIAgpRYioFYDMNqKfHQbg+AVDH3i4fvpl71/P1L0dBl+fQi+P37UYf0ew==", - "requires": { - "pseudomap": "1.0.2", - "yallist": "2.1.2" - } - } + "lru-cache": "4.1.2", + "request": "2.85.0" } }, "request-promise-core": { @@ -5075,9 +4843,9 @@ "dev": true }, "semver": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" }, "set-blocking": { "version": "2.0.0", @@ -5276,7 +5044,7 @@ "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", "requires": { - "hoek": "4.2.0" + "hoek": "4.2.1" } }, "source-map": { @@ -5544,6 +5312,13 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, "supports-color": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.3.0.tgz", @@ -5559,6 +5334,34 @@ "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, + "tar": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz", + "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=", + "dev": true, + "requires": { + "block-stream": "0.0.9", + "fstream": "1.0.11", + "inherits": "2.0.3" + } + }, + "tar-pack": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tar-pack/-/tar-pack-3.4.0.tgz", + "integrity": "sha1-I74tf2cagzk3bL2wuP4/3r8xeYQ=", + "dev": true, + "optional": true, + "requires": { + "debug": "2.6.9", + "fstream": "1.0.11", + "fstream-ignore": "1.0.5", + "once": "1.4.0", + "readable-stream": "2.3.5", + "rimraf": "2.6.2", + "tar": "2.2.1", + "uid-number": "0.0.6" + } + }, "test-exclude": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-4.2.1.tgz", @@ -5923,7 +5726,7 @@ }, "tunnel": { "version": "https://github.com/launchdarkly/node-tunnel/tarball/d860e57650cce1ea655d00854c81babe6b47e02c", - "integrity": "sha1-DxkgfzcgRtPUaCGDy+INSgR8zdk=" + "integrity": "sha512-prl+yIntUTIhkHoz2YtT7xtcAoMEgfsm+RL2bUGFI6e229NTICfo+jFKj1UFCDqc1wm/SQK7TM2U06sgeoO9jQ==" }, "tunnel-agent": { "version": "0.6.0", @@ -5948,6 +5751,12 @@ "prelude-ls": "1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", @@ -5982,6 +5791,13 @@ "dev": true, "optional": true }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=", + "dev": true, + "optional": true + }, "union-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", @@ -6213,6 +6029,50 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true, + "requires": { + "string-width": "1.0.2" + }, + "dependencies": { + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "optional": true, + "requires": { + "number-is-nan": "1.0.1" + } + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "optional": true, + "requires": { + "code-point-at": "1.1.0", + "is-fullwidth-code-point": "1.0.0", + "strip-ansi": "3.0.1" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "optional": true, + "requires": { + "ansi-regex": "2.1.1" + } + } + } + }, "window-size": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", @@ -6221,9 +6081,9 @@ "optional": true }, "winston": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-2.3.1.tgz", - "integrity": "sha1-C0hCDZeMAYBM8CMLZIhhWYIloRk=", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/winston/-/winston-2.4.1.tgz", + "integrity": "sha512-k/+Dkzd39ZdyJHYkuaYmf4ff+7j+sCIy73UCOWHYA67/WXU+FF/Y6PF28j+Vy7qNRPHWO+dR+/+zkoQWPimPqg==", "requires": { "async": "1.0.0", "colors": "1.0.3", diff --git a/package.json b/package.json index caa8c75..d5618ba 100644 --- a/package.json +++ b/package.json @@ -22,24 +22,27 @@ }, "homepage": "https://github.com/launchdarkly/node-client", "dependencies": { - "async": "^2.0.0-rc.5", + "async": "2.6.0", "crypto": "0.0.3", + "hoek": "4.2.1", + "lrucache": "^1.0.3", "node-cache": "^3.2.1", "node-sha1": "0.0.1", "original": "~0.0.8", "redis": "^2.6.0-2", - "request": "^2.83.0", + "request": "2.85.0", "request-etag": "^2.0.3", - "semver": "^5.4.1", + "semver": "5.5.0", "tunnel": "https://github.com/launchdarkly/node-tunnel/tarball/d860e57650cce1ea655d00854c81babe6b47e02c", - "winston": "^2.2.0" + "winston": "2.4.1" }, "engines": { "node": ">= 0.8.x" }, "devDependencies": { "jest": "22.4.3", - "jest-junit": "3.6.0" + "jest-junit": "3.6.0", + "nock": "9.2.3" }, "jest": { "rootDir": ".", diff --git a/polling.js b/polling.js index b15ad8c..f58baf4 100644 --- a/polling.js +++ b/polling.js @@ -3,12 +3,11 @@ var dataKind = require('./versioned_data_kind'); function PollingProcessor(config, requestor) { var processor = {}, - featureStore = config.feature_store, - segmentStore = config.segment_store, + featureStore = config.featureStore, stopped = false; function poll(cb) { - var start_time, delta; + var startTime, delta; cb = cb || function(){}; @@ -16,11 +15,11 @@ function PollingProcessor(config, requestor) { return; } - start_time = new Date().getTime(); + startTime = new Date().getTime(); config.logger.debug("Polling LaunchDarkly for feature flag updates"); - requestor.request_all_data(function(err, resp) { - elapsed = new Date().getTime() - start_time; - sleepFor = Math.max(config.poll_interval * 1000 - elapsed, 0); + requestor.requestAllData(function(err, resp) { + elapsed = new Date().getTime() - startTime; + sleepFor = Math.max(config.pollInterval * 1000 - elapsed, 0); config.logger.debug("Elapsed: %d ms, sleeping for %d ms", elapsed, sleepFor); if (err) { cb(new errors.LDPollingError('Failed to fetch all feature flags: ' + (err.message || JSON.stringify(err))), err.status); diff --git a/redis_feature_store.js b/redis_feature_store.js index e014cea..7b35504 100644 --- a/redis_feature_store.js +++ b/redis_feature_store.js @@ -7,15 +7,15 @@ var redis = require('redis'), var noop = function(){}; -function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { +function RedisFeatureStore(redisOpts, cacheTTL, prefix, logger) { - var client = redis.createClient(redis_opts), + var client = redis.createClient(redisOpts), store = {}, - items_prefix = (prefix || "launchdarkly") + ":", - cache = cache_ttl ? new NodeCache({ stdTTL: cache_ttl}) : null, + itemsPrefix = (prefix || "launchdarkly") + ":", + cache = cacheTTL ? new NodeCache({ stdTTL: cacheTTL}) : null, updateQueue = [], inited = false, - checked_init = false; + checkedInit = false; logger = (logger || new winston.Logger({ @@ -51,21 +51,21 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { // Allow driver programs to exit, even if the Redis socket is active client.unref(); - function items_key(kind) { - return items_prefix + kind.namespace; + function itemsKey(kind) { + return itemsPrefix + kind.namespace; } - function cache_key(kind, key) { + function cacheKey(kind, key) { return kind.namespace + ":" + key; } // A helper that performs a get with the redis client - function do_get(kind, key, cb) { + function doGet(kind, key, cb) { var item; cb = cb || noop; - if (cache_ttl) { - item = cache.get(cache_key(kind, key)); + if (cacheTTL) { + item = cache.get(cacheKey(kind, key)); if (item) { cb(item); return; @@ -78,7 +78,7 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { return; } - client.hget(items_key(kind), key, function(err, obj) { + client.hget(itemsKey(kind), key, function(err, obj) { if (err) { logger.error("Error fetching key " + key + " from Redis in '" + kind.namespace + "'", err); cb(null); @@ -117,7 +117,7 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { store.get = function(kind, key, cb) { cb = cb || noop; - do_get(kind, key, function(item) { + doGet(kind, key, function(item) { if (item && !item.deleted) { cb(item); } else { @@ -134,7 +134,7 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { return; } - client.hgetall(items_key(kind), function(err, obj) { + client.hgetall(itemsKey(kind), function(err, obj) { if (err) { logger.error("Error fetching '" + kind.namespace + "'' from Redis", err); cb(null); @@ -162,14 +162,14 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { store._init = function(allData, cb) { var multi = client.multi(); - if (cache_ttl) { + if (cacheTTL) { cache.flushAll(); } for (var kindNamespace in allData) { if (Object.hasOwnProperty.call(allData, kindNamespace)) { var kind = dataKind[kindNamespace]; - var baseKey = items_key(kind); + var baseKey = itemsKey(kind); var items = allData[kindNamespace]; var stringified = {}; multi.del(baseKey); @@ -177,8 +177,8 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { if (Object.hasOwnProperty.call(items, key)) { stringified[key] = JSON.stringify(items[key]); } - if (cache_ttl) { - cache.set(cache_key(kind, key), items[key]); + if (cacheTTL) { + cache.set(cacheKey(kind, key), items[key]); } } // Redis does not allow hmset() with an empty object @@ -226,17 +226,17 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { } function updateItemWithVersioning(kind, newItem, cb, resultFn) { - client.watch(items_key(kind)); + client.watch(itemsKey(kind)); var multi = client.multi(); // test_transaction_hook is instrumentation, set only by the unit tests var prepare = store.test_transaction_hook || function(prepareCb) { prepareCb(); }; prepare(function() { - do_get(kind, newItem.key, function(oldItem) { + doGet(kind, newItem.key, function(oldItem) { if (oldItem && oldItem.version >= newItem.version) { multi.discard(); cb(); } else { - multi.hset(items_key(kind), newItem.key, JSON.stringify(newItem)); + multi.hset(itemsKey(kind), newItem.key, JSON.stringify(newItem)); multi.exec(function(err, replies) { if (!err && replies === null) { // This means the EXEC failed because someone modified the watched key @@ -244,8 +244,8 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { updateItemWithVersioning(kind, newItem, cb, resultFn); } else { resultFn(err); - if (!err && cache_ttl) { - cache.set(cache_key(kind, newItem.key), newItem); + if (!err && cacheTTL) { + cache.set(cacheKey(kind, newItem.key), newItem); } cb(); } @@ -261,18 +261,18 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { // Once we've determined that we're initialized, we can never become uninitialized again cb(true); } - else if (checked_init) { + else if (checkedInit) { // We don't want to hit Redis for this question more than once; if we've already checked there // and it wasn't populated, we'll continue to say we're uninited until init() has been called cb(false); } else { var inited = false; - client.exists(items_key(dataKind.features), function(err, obj) { + client.exists(itemsKey(dataKind.features), function(err, obj) { if (!err && obj) { inited = true; } - checked_init = true; + checkedInit = true; cb(inited); }); } @@ -280,7 +280,7 @@ function RedisFeatureStore(redis_opts, cache_ttl, prefix, logger) { store.close = function() { client.quit(); - if (cache_ttl) { + if (cacheTTL) { cache.close(); } }; diff --git a/requestor.js b/requestor.js index 107d065..62e5e32 100644 --- a/requestor.js +++ b/requestor.js @@ -10,7 +10,7 @@ var ETagRequest = require('request-etag'); * @param {String} the SDK key * @param {Object} the LaunchDarkly client configuration object **/ -function Requestor(sdk_key, config) { +function Requestor(sdkKey, config) { var requestor = {}; var cacheConfig = { @@ -22,24 +22,24 @@ function Requestor(sdk_key, config) { }; var requestWithETagCaching = new ETagRequest(cacheConfig); - function make_request(resource) { - var request_params = { + function makeRequest(resource) { + var requestParams = { method: "GET", - url: config.base_uri + resource, + url: config.baseUri + resource, headers: { - 'Authorization': sdk_key, - 'User-Agent': config.user_agent + 'Authorization': sdkKey, + 'User-Agent': config.userAgent }, timeout: config.timeout * 1000, - agent: config.proxy_agent + agent: config.proxyAgent } - return function(cb, err_cb) { - requestWithETagCaching(request_params, function(err, resp, body) { + return function(cb, errCb) { + requestWithETagCaching(requestParams, function(err, resp, body) { // Note that when request-etag gives us a cached response, the body will only be in the "body" // callback parameter -- not in resp.getBody(). For a fresh response, it'll be in both. if (err) { - err_cb(err); + errCb(err); } else { cb(resp, body); } @@ -47,7 +47,7 @@ function Requestor(sdk_key, config) { }; } - function process_response(cb) { + function processResponse(cb) { return function(response, body) { if (response.statusCode !== 200 && response.statusCode != 304) { var err = new Error('Unexpected status code: ' + response.statusCode); @@ -59,25 +59,25 @@ function Requestor(sdk_key, config) { }; } - function process_error_response(cb) { + function processErrorResponse(cb) { return function(err) { cb(err, null); } } - requestor.request_object = function(kind, key, cb) { - var req = make_request(kind.requestPath + key); + requestor.requestObject = function(kind, key, cb) { + var req = makeRequest(kind.requestPath + key); req( - process_response(cb), - process_error_response(cb) + processResponse(cb), + processErrorResponse(cb) ); } - requestor.request_all_data = function(cb) { - var req = make_request('/sdk/latest-all'); + requestor.requestAllData = function(cb) { + var req = makeRequest('/sdk/latest-all'); req( - process_response(cb), - process_error_response(cb) + processResponse(cb), + processErrorResponse(cb) ); } diff --git a/streaming.js b/streaming.js index 979c9f6..16b5498 100644 --- a/streaming.js +++ b/streaming.js @@ -3,9 +3,9 @@ var errors = require('./errors'); var EventSource = require('./eventsource'); var dataKind = require('./versioned_data_kind'); -function StreamProcessor(sdk_key, config, requestor) { +function StreamProcessor(sdkKey, config, requestor) { var processor = {}, - featureStore = config.feature_store, + featureStore = config.featureStore, es; function getKeyFromPath(kind, path) { @@ -14,10 +14,10 @@ function StreamProcessor(sdk_key, config, requestor) { processor.start = function(fn) { var cb = fn || function(){}; - es = new EventSource(config.stream_uri + "/all", + es = new EventSource(config.streamUri + "/all", { - agent: config.proxy_agent, - headers: {'Authorization': sdk_key,'User-Agent': config.user_agent} + agent: config.proxyAgent, + headers: {'Authorization': sdkKey,'User-Agent': config.userAgent} }); es.onerror = function(err) { @@ -78,7 +78,7 @@ function StreamProcessor(sdk_key, config, requestor) { es.addEventListener('indirect/put', function(e) { config.logger.debug('Received indirect put event') - requestor.request_all_flags(function (err, resp) { + requestor.requestAllFlags(function (err, resp) { if (err) { cb(err); } else { @@ -101,7 +101,7 @@ function StreamProcessor(sdk_key, config, requestor) { var kind = dataKind[k]; var key = getKeyFromPath(kind, path); if (key != null) { - requestor.request_object(kind, key, function(err, resp) { + requestor.requestObject(kind, key, function(err, resp) { if (err) { cb(new errors.LDStreamingError('Unexpected error requesting ' + key + ' in ' + kind.namespace)); } else { diff --git a/test/LDClient-test.js b/test/LDClient-test.js index fb6903e..138d4e3 100644 --- a/test/LDClient-test.js +++ b/test/LDClient-test.js @@ -1,29 +1,269 @@ var InMemoryFeatureStore = require('../feature_store'); var LDClient = require('../index.js'); var dataKind = require('../versioned_data_kind'); +var messages = require('../messages'); describe('LDClient', function() { - it('should trigger the ready event in offline mode', function() { + + var logger = {}; + + var eventProcessor = { + events: [], + sendEvent: function(event) { + eventProcessor.events.push(event); + }, + flush: function(callback) { + if (callback) { + setImmediate(callback); + } else { + return Promise.resolve(null); + } + }, + close: function() {} + }; + + var updateProcessor = { + start: function(callback) { + setImmediate(callback, null); + } + }; + + beforeEach(function() { + logger.info = jest.fn(); + logger.warn = jest.fn(); + eventProcessor.events = []; + }); + + it('should trigger the ready event in offline mode', function(done) { var client = LDClient.init('sdk_key', {offline: true}); - var callback = jest.fn(); - client.on('ready', callback); - process.nextTick(function() { - expect(callback).toHaveBeenCalled(); + client.on('ready', function() { + done(); + }); + }); + + it('returns true for isOffline in offline mode', function(done) { + var client = LDClient.init('sdk_key', {offline: true}); + client.on('ready', function() { + expect(client.isOffline()).toEqual(true); + done(); + }); + }); + + it('allows deprecated method is_offline', function(done) { + var client = LDClient.init('sdk_key', {offline: true, logger: logger}); + client.on('ready', function() { + expect(client.is_offline()).toEqual(true); + expect(logger.warn).toHaveBeenCalledWith(messages.deprecated('is_offline', 'isOffline')); + done(); }); }); it('should correctly compute the secure mode hash for a known message and secret', function() { var client = LDClient.init('secret', {offline: true}); + var hash = client.secureModeHash({"key": "Message"}); + expect(hash).toEqual("aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597"); + }); + + it('allows deprecated method secure_mode_hash', function() { + var client = LDClient.init('secret', {offline: true, logger: logger}); var hash = client.secure_mode_hash({"key": "Message"}); expect(hash).toEqual("aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597"); + expect(logger.warn).toHaveBeenCalledWith(messages.deprecated('secure_mode_hash', 'secureModeHash')); + }); + + it('returns empty map for allFlags in offline mode and logs a message', function(done) { + var client = LDClient.init('secret', {offline: true, logger: logger}); + client.on('ready', function() { + client.allFlags({key: 'user'}, function(err, result) { + expect(result).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() { + client.all_flags({key: 'user'}, function(err, result) { + expect(result).toEqual({}); + expect(logger.warn).toHaveBeenCalledWith(messages.deprecated('all_flags', 'allFlags')); + done(); + }); + }); + }); + + function createOnlineClientWithFlags(flagsMap) { + var store = InMemoryFeatureStore(); + var allData = {}; + var dummyUri = 'bad'; + allData[dataKind.features.namespace] = flagsMap; + store.init(allData); + return LDClient.init('secret', { + featureStore: store, + eventProcessor: eventProcessor, + updateProcessor: updateProcessor + }); + } + + it('evaluates a flag with variation()', function(done) { + var flag = { + key: 'flagkey', + version: 1, + on: true, + targets: [], + fallthrough: { variation: 1 }, + variations: ['a', 'b'], + trackEvents: true + }; + var client = createOnlineClientWithFlags({ flagkey: flag }); + var user = { key: 'user' }; + client.on('ready', function() { + client.variation(flag.key, user, 'c', function(err, result) { + expect(err).toBeNull(); + expect(result).toEqual('b'); + done(); + }); + }); + }); + + it('generates an event for an existing feature', function(done) { + var flag = { + key: 'flagkey', + version: 1, + on: true, + targets: [], + fallthrough: { variation: 1 }, + variations: ['a', 'b'], + trackEvents: true + }; + var client = createOnlineClientWithFlags({ flagkey: flag }); + var user = { key: 'user' }; + client.on('ready', function() { + client.variation(flag.key, user, 'c', function(err, result) { + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'feature', + key: 'flagkey', + version: 1, + user: user, + variation: 1, + value: 'b', + default: 'c', + trackEvents: true + }); + done(); + }); + }); + }); + + it('generates an event for an unknown feature', function(done) { + var client = createOnlineClientWithFlags({}); + var user = { key: 'user' }; + client.on('ready', function() { + client.variation('flagkey', user, 'c', function(err, result) { + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'feature', + key: 'flagkey', + version: null, + user: user, + variation: null, + value: 'c', + default: 'c', + trackEvents: null + }); + done(); + }); + }); + }); + + it('generates an event for an existing feature even if user key is missing', function(done) { + var flag = { + key: 'flagkey', + version: 1, + on: true, + targets: [], + fallthrough: { variation: 1 }, + variations: ['a', 'b'], + trackEvents: true + }; + var client = createOnlineClientWithFlags({ flagkey: flag }); + var user = { name: 'Bob' }; + client.on('ready', function() { + client.variation(flag.key, user, 'c', function(err, result) { + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'feature', + key: 'flagkey', + version: 1, + user: user, + variation: null, + value: 'c', + default: 'c', + trackEvents: true + }); + done(); + }); + }); + }); + + it('generates an event for an existing feature even if user is null', function(done) { + var flag = { + key: 'flagkey', + version: 1, + on: true, + targets: [], + fallthrough: { variation: 1 }, + variations: ['a', 'b'], + trackEvents: true + }; + var client = createOnlineClientWithFlags({ flagkey: flag }); + client.on('ready', function() { + client.variation(flag.key, null, 'c', function(err, result) { + expect(eventProcessor.events).toHaveLength(1); + var e = eventProcessor.events[0]; + expect(e).toMatchObject({ + kind: 'feature', + key: 'flagkey', + version: 1, + user: null, + variation: null, + value: 'c', + default: 'c', + trackEvents: true + }); + done(); + }); + }); + }); + + it('evaluates a flag with allFlags()', function(done) { + var flag = { + key: 'feature', + version: 1, + on: true, + targets: [], + fallthrough: { variation: 1 }, + variations: ['a', 'b'] + }; + var client = createOnlineClientWithFlags({ feature: flag }); + var user = { key: 'user' }; + client.on('ready', function() { + client.allFlags(user, function(err, results) { + expect(err).toBeNull(); + expect(results).toEqual({feature: 'b'}); + done(); + }); + }); }); it('should not overflow the call stack when evaluating a huge number of flags', function(done) { var flagCount = 5000; - var dummyUri = 'bad'; var flags = {}; - var store = InMemoryFeatureStore(); - var allData = {}; for (var i = 0; i < flagCount; i++) { var key = 'feature' + i; var flag = { @@ -33,19 +273,13 @@ describe('LDClient', function() { }; flags[key] = flag; } - allData[dataKind.features.namespace] = flags; - store.init(allData); - var client = LDClient.init('secret', { - base_uri: dummyUri, - stream_uri: dummyUri, - events_uri: dummyUri, - feature_store: store - }); - // Deliberately not waiting for ready event; the update processor is irrelevant for this test - client.all_flags({key: 'user'}, function(err, result) { - expect(err).toEqual(null); - expect(Object.keys(result).length).toEqual(flagCount); - done(); + var client = createOnlineClientWithFlags(flags); + client.on('ready', function() { + client.allFlags({key: 'user'}, function(err, result) { + expect(err).toEqual(null); + expect(Object.keys(result).length).toEqual(flagCount); + done(); + }); }); }); diff --git a/test/configuration-test.js b/test/configuration-test.js new file mode 100644 index 0000000..9791aa6 --- /dev/null +++ b/test/configuration-test.js @@ -0,0 +1,72 @@ +var configuration = require('../configuration'); + +describe('configuration', function() { + function checkDefault(name, value) { + it('applies defaults correctly for "' + name + "'", function() { + var configWithUnspecifiedValue = {}; + expect(configuration.validate(configWithUnspecifiedValue)[name]).toEqual(value); + var configWithNullValue = {}; + configWithNullValue[name] = null; + expect(configuration.validate(configWithNullValue)[name]).toEqual(value); + var configWithSpecifiedValue = {}; + configWithSpecifiedValue[name] = 'value'; + expect(configuration.validate(configWithSpecifiedValue)[name]).toEqual('value'); + }); + } + + checkDefault('sendEvents', true); + checkDefault('stream', true); + checkDefault('offline', false); + checkDefault('useLdd', false); + + function checkDeprecated(oldName, newName, value) { + it('allows "' + oldName + '" as a deprecated equivalent to "' + newName + '"', function() { + var logger = { + warn: jest.fn() + }; + var config0 = { + logger: logger + }; + config0[oldName] = value; + var config1 = configuration.validate(config0); + expect(config1[newName]).toEqual(value); + expect(config1[oldName]).toBeUndefined(); + expect(logger.warn).toHaveBeenCalledTimes(1); + }); + } + + checkDeprecated('base_uri', 'baseUri', 'http://test.com'); + checkDeprecated('stream_uri', 'streamUri', 'http://test.com'); + checkDeprecated('events_uri', 'eventsUri', 'http://test.com'); + checkDeprecated('send_events', 'sendEvents', true); + checkDeprecated('flush_interval', 'flushInterval', 10); + checkDeprecated('poll_interval', 'pollInterval', 60); + checkDeprecated('use_ldd', 'useLdd', true); + checkDeprecated('all_attributes_private', 'allAttributesPrivate', true); + checkDeprecated('private_attribute_names', 'privateAttributeNames', ['foo']); + checkDeprecated('proxy_host', 'proxyHost', 'test.com'); + checkDeprecated('proxy_port', 'proxyPort', 8888); + checkDeprecated('proxy_auth', 'proxyAuth', 'basic'); + checkDeprecated('feature_store', 'featureStore', {}); + + function checkUriProperty(name) { + var config0 = {}; + config0[name] = 'http://test.com/'; + var config1 = configuration.validate(config0); + expect(config1[name]).toEqual('http://test.com'); + } + + checkUriProperty('baseUri'); + checkUriProperty('streamUri'); + checkUriProperty('eventsUri'); + + it('enforces minimum poll interval', function() { + var config = configuration.validate({ pollInterval: 29 }); + expect(config.pollInterval).toEqual(30); + }); + + it('allows larger poll interval', function() { + var config = configuration.validate({ pollInterval: 31 }); + expect(config.pollInterval).toEqual(31); + }); +}); diff --git a/test/evaluate_flag-test.js b/test/evaluate_flag-test.js index 891de64..d4c3cf3 100644 --- a/test/evaluate_flag-test.js +++ b/test/evaluate_flag-test.js @@ -58,7 +58,7 @@ describe('evaluate', function() { variations: ['a', 'b', 'c'] }; var user = { key: 'x' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe('b'); done(); }); @@ -72,7 +72,7 @@ describe('evaluate', function() { variations: ['a', 'b', 'c'] }; var user = { key: 'x' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(null); done(); }); @@ -89,7 +89,7 @@ describe('evaluate', function() { variations: ['a', 'b', 'c'] }; var user = { key: 'x' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe('a'); done(); }); @@ -105,7 +105,7 @@ describe('evaluate', function() { variations: ['a', 'b', 'c'] }; var user = { key: 'x' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe('b'); done(); }); @@ -135,9 +135,9 @@ describe('evaluate', function() { defineFeatures([flag, flag1], function() { var user = { key: 'x' }; var eventsShouldBe = [ - { kind: 'feature', key: 'feature1', value: 'd', version: 2, prereqOf: 'feature0' } + { kind: 'feature', key: 'feature1', variation: 0, value: 'd', version: 2, prereqOf: 'feature0' } ]; - evaluate.evaluate(flag, user, featureStore, function(err, result, events) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result, events) { expect(result).toBe('b'); expect(events).toMatchObject(eventsShouldBe); done(); @@ -169,9 +169,9 @@ describe('evaluate', function() { defineFeatures([flag, flag1], function() { var user = { key: 'x' }; var eventsShouldBe = [ - { kind: 'feature', key: 'feature1', value: 'e', version: 2, prereqOf: 'feature0' } + { kind: 'feature', key: 'feature1', variation: 1, value: 'e', version: 2, prereqOf: 'feature0' } ]; - evaluate.evaluate(flag, user, featureStore, function(err, result, events) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result, events) { expect(result).toBe('a'); expect(events).toMatchObject(eventsShouldBe); done(); @@ -201,7 +201,7 @@ describe('evaluate', function() { variations: ['a', 'b', 'c'] }; var user = { key: 'userkey' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe('c'); done(); }); @@ -223,7 +223,7 @@ describe('evaluate', function() { variations: ['a', 'b', 'c'] }; var user = { key: 'userkey' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe('c'); done(); }); @@ -231,7 +231,7 @@ describe('evaluate', function() { function testClauseMatch(clause, user, shouldBe, done) { var flag = makeBooleanFlagWithOneClause(clause); - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(shouldBe); done(); }); @@ -270,7 +270,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(true); done(); }); @@ -286,7 +286,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(false); done(); }); @@ -302,7 +302,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'bar' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(false); done(); }); @@ -319,7 +319,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(true); done(); }); @@ -346,7 +346,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo', email: 'test@example.com' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(true); done(); }); @@ -373,7 +373,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo', email: 'test@example.com' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(false); done(); }); @@ -404,7 +404,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo', email: 'test@example.com', name: 'bob' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(true); done(); }); @@ -435,7 +435,7 @@ describe('evaluate', function() { defineSegment(segment, function() { var flag = makeFlagWithSegmentMatch(segment); var user = { key: 'foo', email: 'test@example.com', name: 'bob' }; - evaluate.evaluate(flag, user, featureStore, function(err, result) { + evaluate.evaluate(flag, user, featureStore, function(err, variation, result) { expect(result).toBe(false); done(); }); @@ -463,9 +463,9 @@ describe('evaluate', function() { rules.push({ clauses: [clause], variation: 1 }); } flag.rules = rules; - evaluate.evaluate(flag, {key: 'user'}, featureStore, function(err, result) { + evaluate.evaluate(flag, {key: 'user'}, featureStore, function(err, variation, value) { expect(err).toEqual(null); - expect(result).toEqual(false); + expect(value).toEqual(false); done(); }); }); @@ -492,26 +492,26 @@ describe('evaluate', function() { } var rule = { clauses: clauses, variation: 1 }; flag.rules = [rule]; - evaluate.evaluate(flag, {key: 'user'}, featureStore, function(err, result) { + evaluate.evaluate(flag, {key: 'user'}, featureStore, function(err, variation, value) { expect(err).toEqual(null); - expect(result).toEqual(true); + expect(value).toEqual(true); done(); }); }); }); -describe('bucket_user', function() { +describe('bucketUser', function() { it('gets expected bucket values for specific keys', function() { var user = { key: 'userKeyA' }; - var bucket = evaluate.bucket_user(user, 'hashKey', 'key', 'saltyA'); + var bucket = evaluate.bucketUser(user, 'hashKey', 'key', 'saltyA'); expect(bucket).toBeCloseTo(0.42157587, 7); user = { key: 'userKeyB' }; - bucket = evaluate.bucket_user(user, 'hashKey', 'key', 'saltyA'); + bucket = evaluate.bucketUser(user, 'hashKey', 'key', 'saltyA'); expect(bucket).toBeCloseTo(0.6708485, 7); user = { key: 'userKeyC' }; - bucket = evaluate.bucket_user(user, 'hashKey', 'key', 'saltyA'); + bucket = evaluate.bucketUser(user, 'hashKey', 'key', 'saltyA'); expect(bucket).toBeCloseTo(0.10343106, 7); }); @@ -523,8 +523,8 @@ describe('bucket_user', function() { stringAttr: '33333' } }; - var bucket = evaluate.bucket_user(user, 'hashKey', 'intAttr', 'saltyA'); - var bucket2 = evaluate.bucket_user(user, 'hashKey', 'stringAttr', 'saltyA'); + var bucket = evaluate.bucketUser(user, 'hashKey', 'intAttr', 'saltyA'); + var bucket2 = evaluate.bucketUser(user, 'hashKey', 'stringAttr', 'saltyA'); expect(bucket).toBeCloseTo(0.54771423, 7); expect(bucket2).toBe(bucket); }); @@ -536,7 +536,7 @@ describe('bucket_user', function() { floatAttr: 33.5 } }; - var bucket = evaluate.bucket_user(user, 'hashKey', 'floatAttr', 'saltyA'); + var bucket = evaluate.bucketUser(user, 'hashKey', 'floatAttr', 'saltyA'); expect(bucket).toBe(0); }); }); diff --git a/test/event_processor-test.js b/test/event_processor-test.js new file mode 100644 index 0000000..e42b4ea --- /dev/null +++ b/test/event_processor-test.js @@ -0,0 +1,432 @@ +var nock = require('nock'); +var EventProcessor = require('../event_processor'); +var EventEmitter = require('events').EventEmitter; + +describe('EventProcessor', function() { + + var ep; + var eventsUri = 'http://example.com'; + var sdkKey = 'SDK_KEY'; + var defaultConfig = { + eventsUri: eventsUri, + capacity: 100, + flushInterval: 30, + userKeysCapacity: 1000, + userKeysFlushInterval: 300, + logger: { + debug: jest.fn(), + warn: jest.fn() + } + }; + var user = { key: 'userKey', name: 'Red' }; + var filteredUser = { key: 'userKey', privateAttrs: [ 'name' ] }; + + afterEach(function() { + if (ep) { + ep.close(); + } + }); + + function flushAndGetRequest(options, cb) { + var callback = cb || options; + options = cb ? options : {}; + var requestBody; + var requestHeaders; + nock(eventsUri).post('/bulk') + .reply(function(uri, body) { + requestBody = body; + requestHeaders = this.req.headers; + return [ options.status || 200, '', options.headers || {} ]; + }); + ep.flush().then( + function() { + callback(requestBody, requestHeaders); + }, + function(error) { + callback(requestBody, requestHeaders, error); + }); + } + + function headersWithDate(timestamp) { + return { date: new Date(timestamp).toUTCString() }; + } + + function checkIndexEvent(e, source, user) { + expect(e.kind).toEqual('index'); + expect(e.creationDate).toEqual(source.creationDate); + expect(e.user).toEqual(user); + } + + function checkFeatureEvent(e, source, debug, inlineUser) { + expect(e.kind).toEqual(debug ? 'debug' : 'feature'); + expect(e.creationDate).toEqual(source.creationDate); + expect(e.key).toEqual(source.key); + expect(e.version).toEqual(source.version); + expect(e.variation).toEqual(source.variation); + expect(e.value).toEqual(source.value); + expect(e.default).toEqual(source.default); + if (inlineUser) { + expect(e.user).toEqual(inlineUser); + } else { + expect(e.userKey).toEqual(source.user.key); + } + } + + function checkCustomEvent(e, source, inlineUser) { + expect(e.kind).toEqual('custom'); + expect(e.creationDate).toEqual(source.creationDate); + expect(e.key).toEqual(source.key); + expect(e.data).toEqual(source.data); + if (inlineUser) { + expect(e.user).toEqual(inlineUser); + } else { + expect(e.userKey).toEqual(source.user.key); + } + } + + function checkSummaryEvent(e) { + expect(e.kind).toEqual('summary'); + } + + it('queues identify event', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output).toEqual([{ + kind: 'identify', + creationDate: 1000, + key: user.key, + user: user + }]); + done(); + }); + }); + + it('filters user in identify event', function(done) { + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output).toEqual([{ + kind: 'identify', + creationDate: 1000, + key: user.key, + user: filteredUser + }]); + done(); + }); + }); + + it('queues individual feature event with index event', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(3); + checkIndexEvent(output[0], e, user); + checkFeatureEvent(output[1], e, false); + checkSummaryEvent(output[2]); + done(); + }); + }); + + it('filters user in index event', function(done) { + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(3); + checkIndexEvent(output[0], e, filteredUser); + checkFeatureEvent(output[1], e, false); + checkSummaryEvent(output[2]); + done(); + }); + }); + + it('can include inline user in feature event', function(done) { + var config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], e, false, user); + checkSummaryEvent(output[1]); + done(); + }); + }); + + it('filters user in feature event', function(done) { + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true, + inlineUsersInEvents: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkFeatureEvent(output[0], e, false, filteredUser); + checkSummaryEvent(output[1]); + done(); + }); + }); + + it('still generates index event if inlineUsers is true but feature event is not tracked', function(done) { + var config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e, user); + checkSummaryEvent(output[1]); + done(); + }); + }); + + it('sets event kind to debug if event is temporarily in debug mode', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var futureTime = new Date().getTime() + 1000000; + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: futureTime }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(3); + checkIndexEvent(output[0], e, user); + checkFeatureEvent(output[1], e, true, user); + checkSummaryEvent(output[2]); + done(); + }); + }); + + it('can both track and debug an event', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var futureTime = new Date().getTime() + 1000000; + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true, debugEventsUntilDate: futureTime }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(4); + checkIndexEvent(output[0], e, user); + checkFeatureEvent(output[1], e, false); + checkFeatureEvent(output[2], e, true, user); + checkSummaryEvent(output[3]); + done(); + }); + }); + + it('expires debug mode based on client time if client time is later than server time', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + + // Pick a server time that is somewhat behind the client time + var serverTime = new Date().getTime() - 20000; + + // Send and flush an event we don't care about, just to set the last server time + ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); + flushAndGetRequest({ status: 200, headers: headersWithDate(serverTime) }, function() { + // Now send an event with debug mode on, with a "debug until" time that is further in + // the future than the server time, but in the past compared to the client. + var debugUntil = serverTime + 1000; + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: debugUntil }; + ep.sendEvent(e); + + // Should get a summary event only, not a full feature event + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e, user); + checkSummaryEvent(output[1]); + done(); + }); + }); + }); + + it('expires debug mode based on server time if server time is later than client time', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + + // Pick a server time that is somewhat ahead of the client time + var serverTime = new Date().getTime() + 20000; + + // Send and flush an event we don't care about, just to set the last server time + ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); + flushAndGetRequest({ status: 200, headers: headersWithDate(serverTime) }, function() { + // Now send an event with debug mode on, with a "debug until" time that is further in + // the future than the client time, but in the past compared to the server. + var debugUntil = serverTime - 1000; + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false, debugEventsUntilDate: debugUntil }; + ep.sendEvent(e); + + // Should get a summary event only, not a full feature event + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e, user); + checkSummaryEvent(output[1]); + done(); + }); + }); + }); + + it('generates only one index event from two feature events for same user', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e1 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey1', + version: 11, variation: 1, value: 'value', trackEvents: true }; + var e2 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey2', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.sendEvent(e1); + ep.sendEvent(e2); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(4); + checkIndexEvent(output[0], e1, user); + checkFeatureEvent(output[1], e1, false); + checkFeatureEvent(output[2], e2, false); + checkSummaryEvent(output[3]); + done(); + }); + }); + + it('summarizes nontracked events', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e1 = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey1', + version: 11, variation: 1, value: 'value1', default: 'default1', trackEvents: false }; + var e2 = { kind: 'feature', creationDate: 2000, user: user, key: 'flagkey2', + version: 22, variation: 1, value: 'value2', default: 'default2', trackEvents: false }; + ep.sendEvent(e1); + ep.sendEvent(e2); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e1, user); + var se = output[1]; + checkSummaryEvent(se); + expect(se.startDate).toEqual(1000); + expect(se.endDate).toEqual(2000); + expect(se.features).toEqual({ + flagkey1: { + default: 'default1', + counters: [ { version: 11, variation: 1, value: 'value1', count: 1 } ] + }, + flagkey2: { + default: 'default2', + counters: [ { version: 22, variation: 1, value: 'value2', count: 1 } ] + } + }); + done(); + }); + }); + + it('queues custom event with user', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(2); + checkIndexEvent(output[0], e, user); + checkCustomEvent(output[1], e); + done(); + }); + }); + + it('can include inline user in custom event', function(done) { + var config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(1); + checkCustomEvent(output[0], e, user); + done(); + }); + }); + + it('filters user in custom event', function(done) { + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true, + inlineUsersInEvents: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.sendEvent(e); + + flushAndGetRequest(function(output) { + expect(output.length).toEqual(1); + checkCustomEvent(output[0], e, filteredUser); + done(); + }); + }); + + it('sends nothing if there are no events', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + ep.flush(function() { + // Nock will generate an error if we sent a request we didn't explicitly listen for. + done(); + }); + }); + + it('sends SDK key', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + + flushAndGetRequest(function(requestBody, requestHeaders) { + expect(requestHeaders['authorization']).toEqual(sdkKey); + done(); + }); + }); + + it('stops sending events after a 401 error', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + + flushAndGetRequest({ status: 401 }, function(body, headers, error) { + expect(error.message).toContain("status code 401"); + + ep.sendEvent(e); + + ep.flush().then( + // no HTTP request should have been done here - Nock will error out if there was one + function() { }, + function(err) { + expect(err.message).toContain("SDK key is invalid"); + done(); + }); + }); + }); + + it('retries once after a 5xx error', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.sendEvent(e); + + nock(eventsUri).post('/bulk').reply(503); + nock(eventsUri).post('/bulk').reply(503); + // since we only queued two responses, Nock will throw an error if it gets a third. + ep.flush().then( + function() {}, + function(err) { + expect(err.message).toContain('Unexpected status code 503'); + done(); + }); + }); +}); diff --git a/test/event_serializer-test.js b/test/event_serializer-test.js deleted file mode 100644 index 8ad5820..0000000 --- a/test/event_serializer-test.js +++ /dev/null @@ -1,115 +0,0 @@ -var assert = require('assert'); -var EventSerializer = require('../event_serializer.js'); - -describe('event_serializer', function() { - - // users to serialize - var user = { - 'key': 'abc', - 'firstName': 'Sue', - 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } - }; - - var user_specifying_own_private_attr = { - 'key': 'abc', - 'firstName': 'Sue', - 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' }, - 'privateAttributeNames': [ 'dizzle', 'unused' ] - }; - - var user_with_unknown_top_level_attrs = { - 'key': 'abc', - 'firstName': 'Sue', - 'species': 'human', - 'hatSize': 6, - 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } - }; - - var anon_user = { - 'key': 'abc', - 'anonymous': true, - 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } - }; - - // expected results from serializing user - var user_with_all_attrs_hidden = { - 'key': 'abc', - 'custom': { }, - 'privateAttrs': [ 'bizzle', 'dizzle', 'firstName' ] - }; - - var user_with_some_attrs_hidden = { - 'key': 'abc', - 'custom': { - 'dizzle': 'ghi' - }, - 'privateAttrs': [ 'bizzle', 'firstName' ] - }; - - var user_with_own_specified_attr_hidden = { - 'key': 'abc', - 'firstName': 'Sue', - 'custom': { - 'bizzle': 'def' - }, - 'privateAttrs': [ 'dizzle' ] - }; - - var anon_user_with_all_attrs_hidden = { - 'key': 'abc', - 'anonymous': true, - 'custom': { }, - 'privateAttrs': [ 'bizzle', 'dizzle' ] - }; - - function make_event(user) { - return { - 'creationDate': 1000000, - 'key': 'xyz', - 'kind': 'thing', - 'user': user - } - } - - it('includes all user attributes by default', function() { - var es = EventSerializer({}); - var event = make_event(user); - assert.deepEqual(es.serialize_events([event]), [event]); - }); - - it('hides all except key if all_attrs_private is true', function() { - var es = EventSerializer({ all_attributes_private: true}); - var event = make_event(user); - assert.deepEqual(es.serialize_events([event]), [make_event(user_with_all_attrs_hidden)]); - }); - - it('hides some attributes if private_attr_names is set', function() { - var es = EventSerializer({ private_attribute_names: [ 'firstName', 'bizzle' ]}); - var event = make_event(user); - assert.deepEqual(es.serialize_events([event]), [make_event(user_with_some_attrs_hidden)]); - }); - - it('hides attributes specified in per-user privateAttrs', function() { - var es = EventSerializer({}); - var event = make_event(user_specifying_own_private_attr); - assert.deepEqual(es.serialize_events([event]), [make_event(user_with_own_specified_attr_hidden)]); - }); - - it('looks at both per-user privateAttrs and global config', function() { - var es = EventSerializer({ private_attribute_names: [ 'firstName', 'bizzle' ]}); - var event = make_event(user_specifying_own_private_attr); - assert.deepEqual(es.serialize_events([event]), [make_event(user_with_all_attrs_hidden)]); - }); - - it('strips unknown top-level attributes', function() { - var es = EventSerializer({}); - var event = make_event(user_with_unknown_top_level_attrs); - assert.deepEqual(es.serialize_events([event]), [make_event(user)]); - }); - - it('leaves the "anonymous" attribute as is', function() { - var es = EventSerializer({ all_attributes_private: true}); - var event = make_event(anon_user); - assert.deepEqual(es.serialize_events([event]), [make_event(anon_user_with_all_attrs_hidden)]); - }); -}); diff --git a/test/event_summarizer-test.js b/test/event_summarizer-test.js new file mode 100644 index 0000000..02520a3 --- /dev/null +++ b/test/event_summarizer-test.js @@ -0,0 +1,74 @@ +var EventSummarizer = require('../event_summarizer'); + +describe('EventSummarizer', function() { + + var user = { key: 'key1' }; + + it('does nothing for identify event', function() { + var es = EventSummarizer(); + var snapshot = es.getSummary(); + es.summarizeEvent({ kind: 'identify', creationDate: 1000, user: user }); + expect(es.getSummary()).toEqual(snapshot); + }); + + it('does nothing for custom event', function() { + var es = EventSummarizer(); + var snapshot = es.getSummary(); + es.summarizeEvent({ kind: 'custom', creationDate: 1000, key: 'eventkey', user: user }); + expect(es.getSummary()).toEqual(snapshot); + }); + + it('sets start and end dates for feature events', function() { + var es = EventSummarizer(); + var event1 = { kind: 'feature', creationDate: 2000, key: 'key', user: user }; + var event2 = { kind: 'feature', creationDate: 1000, key: 'key', user: user }; + var event3 = { kind: 'feature', creationDate: 1500, key: 'key', user: user }; + es.summarizeEvent(event1); + es.summarizeEvent(event2); + es.summarizeEvent(event3); + var data = es.getSummary(); + + expect(data.startDate).toEqual(1000); + expect(data.endDate).toEqual(2000); + }); + + it('increments counters for feature events', function() { + var es = EventSummarizer(); + var event1 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, user: user, + variation: 1, value: 100, default: 111 }; + var event2 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, user: user, + variation: 2, value: 200, default: 111 }; + var event3 = { kind: 'feature', creationDate: 1000, key: 'key2', version: 22, user: user, + variation: 1, value: 999, default: 222 }; + var event4 = { kind: 'feature', creationDate: 1000, key: 'key1', version: 11, user: user, + variation: 1, value: 100, default: 111 }; + var event5 = { kind: 'feature', creationDate: 1000, key: 'badkey', user: user, + value: 333, default: 333 }; + es.summarizeEvent(event1); + es.summarizeEvent(event2); + es.summarizeEvent(event3); + es.summarizeEvent(event4); + es.summarizeEvent(event5); + var data = es.getSummary(); + + data.features.key1.counters.sort(function(a, b) { return a.value - b.value; }); + var expectedFeatures = { + key1: { + default: 111, + counters: [ + { variation: 1, value: 100, version: 11, count: 2 }, + { variation: 2, value: 200, version: 11, count: 1 } + ] + }, + key2: { + default: 222, + counters: [ { variation: 1, value: 999, version: 22, count: 1 }] + }, + badkey: { + default: 333, + counters: [ { value: 333, unknown: true, count: 1 }] + } + }; + expect(data.features).toEqual(expectedFeatures); + }); +}); diff --git a/test/user_filter-test.js b/test/user_filter-test.js new file mode 100644 index 0000000..4b5d41b --- /dev/null +++ b/test/user_filter-test.js @@ -0,0 +1,98 @@ +var assert = require('assert'); +var UserFilter = require('../user_filter'); + +describe('user_filter', function() { + // users to serialize + var user = { + 'key': 'abc', + 'firstName': 'Sue', + 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } + }; + + var userSpecifyingOwnPrivateAttr = { + 'key': 'abc', + 'firstName': 'Sue', + 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' }, + 'privateAttributeNames': [ 'dizzle', 'unused' ] + }; + + var userWithUnknownTopLevelAttrs = { + 'key': 'abc', + 'firstName': 'Sue', + 'species': 'human', + 'hatSize': 6, + 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } + }; + + var anonUser = { + 'key': 'abc', + 'anonymous': true, + 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } + }; + + // expected results from serializing user + var userWithAllAttrsHidden = { + 'key': 'abc', + 'custom': { }, + 'privateAttrs': [ 'bizzle', 'dizzle', 'firstName' ] + }; + + var userWithSomeAttrsHidden = { + 'key': 'abc', + 'custom': { + 'dizzle': 'ghi' + }, + 'privateAttrs': [ 'bizzle', 'firstName' ] + }; + + var userWithOwnSpecifiedAttrHidden = { + 'key': 'abc', + 'firstName': 'Sue', + 'custom': { + 'bizzle': 'def' + }, + 'privateAttrs': [ 'dizzle' ] + }; + + var anonUserWithAllAttrsHidden = { + 'key': 'abc', + 'anonymous': true, + 'custom': { }, + 'privateAttrs': [ 'bizzle', 'dizzle' ] + }; + + it('includes all user attributes by default', function() { + var uf = UserFilter({}); + assert.deepEqual(uf.filterUser(user), user); + }); + + it('hides all except key if allAttributesPrivate is true', function() { + var uf = UserFilter({ allAttributesPrivate: true}); + assert.deepEqual(uf.filterUser(user), userWithAllAttrsHidden); + }); + + it('hides some attributes if privateAttributeNames is set', function() { + var uf = UserFilter({ privateAttributeNames: [ 'firstName', 'bizzle' ]}); + assert.deepEqual(uf.filterUser(user), userWithSomeAttrsHidden); + }); + + it('hides attributes specified in per-user privateAttrs', function() { + var uf = UserFilter({}); + assert.deepEqual(uf.filterUser(userSpecifyingOwnPrivateAttr), userWithOwnSpecifiedAttrHidden); + }); + + it('looks at both per-user privateAttrs and global config', function() { + var uf = UserFilter({ privateAttributeNames: [ 'firstName', 'bizzle' ]}); + assert.deepEqual(uf.filterUser(userSpecifyingOwnPrivateAttr), userWithAllAttrsHidden); + }); + + it('strips unknown top-level attributes', function() { + var uf = UserFilter({}); + assert.deepEqual(uf.filterUser(userWithUnknownTopLevelAttrs), user); + }); + + it('leaves the "anonymous" attribute as is', function() { + var uf = UserFilter({ allAttributesPrivate: true}); + assert.deepEqual(uf.filterUser(anonUser), anonUserWithAllAttrsHidden); + }); +}); diff --git a/event_serializer.js b/user_filter.js similarity index 66% rename from event_serializer.js rename to user_filter.js index 0ec817e..ccd6b86 100644 --- a/event_serializer.js +++ b/user_filter.js @@ -1,31 +1,20 @@ +var messages = require('./messages'); + /** - * The EventSerializer object transforms the internal representation of events into objects suitable to be sent - * as JSON to the server. This includes hiding any private user attributes. + * The UserFilter object transforms user objects into objects suitable to be sent as JSON to + * the server, hiding any private user attributes. * * @param {Object} the LaunchDarkly client configuration object **/ -function EventSerializer(config) { - var serializer = {}; - var allAttributesPrivate = config.all_attributes_private; - var privateAttributeNames = config.private_attribute_names || []; +function UserFilter(config) { + var filter = {}; + const allAttributesPrivate = config.allAttributesPrivate; + const privateAttributeNames = config.privateAttributeNames || []; var ignoreAttrs = { key: true, custom: true, anonymous: true }; var allowedTopLevelAttrs = { key: true, secondary: true, ip: true, country: true, email: true, firstName: true, lastName: true, avatar: true, name: true, anonymous: true, custom: true }; - serializer.serialize_events = function(events) { - return events.map(serialize_event); - } - - function serialize_event(event) { - return Object.keys(event).map(function(key) { - return [key, (key === 'user') ? filter_user(event[key]) : event[key]]; - }).reduce(function(acc, p) { - acc[p[0]] = p[1]; - return acc; - }, {}); - } - - function filter_user(user) { + filter.filterUser = function(user) { var allPrivateAttrs = {}; var userPrivateAttrs = user.privateAttributeNames || []; @@ -63,7 +52,7 @@ function EventSerializer(config) { return filteredProps; } - return serializer; + return filter; } -module.exports = EventSerializer; +module.exports = UserFilter; diff --git a/utils/__tests__/wrapPromiseCallback-test.js b/utils/__tests__/wrapPromiseCallback-test.js index 4c07443..2cfae6a 100644 --- a/utils/__tests__/wrapPromiseCallback-test.js +++ b/utils/__tests__/wrapPromiseCallback-test.js @@ -14,30 +14,22 @@ describe('wrapPromiseCallback',function() { return expect(promise).rejects.toBe(error); }); - it('should call the callback with a value if the promise resolves', function() { - const callback = jest.fn(); - const promise = wrapPromiseCallback(Promise.resolve('woohoo'), callback); - - return promise.then(function(result) { - expect(result).toEqual('woohoo'); - // callback run on next tick to maintain asynchronous expections - setTimeout(function() { - expect(callback).toHaveBeenCalledWith(null, 'woohoo'); - }, 0); + it('should call the callback with a value if the promise resolves', function(done) { + const promise = wrapPromiseCallback(Promise.resolve('woohoo'), function(error, value) { + expect(promise).toBeUndefined(); + expect(error).toBeNull(); + expect(value).toBe('woohoo'); + done() }); }); - it('should call the callback with an error if the promise rejects', function() { - const error = new Error('something went wrong'); - const callback = jest.fn(); - const promise = wrapPromiseCallback(Promise.reject(error), callback); - - return promise.catch(function(error) { - expect(promise).rejects.toBe(error); - // callback run on next tick to maintain asynchronous expections - setTimeout(function() { - expect(callback).toHaveBeenCalledWith(error, null); - }, 0); + it('should call the callback with an error if the promise rejects', function(done) { + const actualError = new Error('something went wrong'); + const promise = wrapPromiseCallback(Promise.reject(actualError), function(error, value) { + expect(promise).toBeUndefined(); + expect(error).toBe(actualError); + expect(value).toBeNull(); + done(); }); }); }); \ No newline at end of file diff --git a/utils/wrapPromiseCallback.js b/utils/wrapPromiseCallback.js index ac4219b..909c17c 100644 --- a/utils/wrapPromiseCallback.js +++ b/utils/wrapPromiseCallback.js @@ -9,10 +9,10 @@ * * @param {Promise} promise * @param {Function} callback - * @returns Promise + * @returns Promise | undefined */ module.exports = function wrapPromiseCallback(promise, callback) { - return promise.then( + const ret = promise.then( function(value) { if (callback) { setTimeout(function() { callback(null, value); }, 0); @@ -22,8 +22,11 @@ module.exports = function wrapPromiseCallback(promise, callback) { function(error) { if (callback) { setTimeout(function() { callback(error, null); }, 0); + } else { + return Promise.reject(error); } - return Promise.reject(error); } ); + + return !callback ? ret : undefined; } \ No newline at end of file