From 46476ddc41d2537696fffd75c2ed087ad3b5fe3b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Mar 2018 18:38:19 -0700 Subject: [PATCH 01/38] capture variation index and new flag properties in events --- evaluate_flag.js | 69 +++++++++++++++++--------------------- index.js | 18 +++++----- test/evaluate_flag-test.js | 38 ++++++++++----------- 3 files changed, 58 insertions(+), 67 deletions(-) diff --git a/evaluate_flag.js b/evaluate_flag.js index badc1ca..20abd37 100644 --- a/evaluate_flag.js +++ b/evaluate_flag.js @@ -11,44 +11,32 @@ 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, get_variation(flag, flag.offVariation), null); return; } - eval_internal(flag, user, featureStore, [], function(err, result, events) { + eval_internal(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, get_variation(flag, flag.offVariation), events); } else { - cb(err, result, events); + cb(err, variation, value, events); } }); return; @@ -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) { + eval_internal(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(create_flag_event(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 = get_variation(flag, target.variation); + cb(value === null ? new Error("Undefined variation for flag " + flag.key) : null, + target.variation, value); return; } } @@ -132,12 +120,12 @@ function evalRules(flag, user, featureStore, cb) { 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); } 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); } + cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null, + variation, get_variation(flag, variation)); } ); } @@ -270,7 +258,7 @@ 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) { + if (index === null || index === undefined || index >= flag.variations.length) { return null; } else { return flag.variations[index]; @@ -287,7 +275,7 @@ 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 @@ -297,7 +285,7 @@ function variation_for_user(r, user, flag) { variate = r.rollout.variations[i]; sum += variate.weight / 100000.0; if (bucket < sum) { - return get_variation(flag, variate.variation); + return variate.variation; } } } @@ -349,16 +337,19 @@ function bucketable_string_value(value) { return null; } -function create_flag_event(key, user, value, default_val, version, prereqOf) { +function create_flag_event(key, flag, user, variation, value, default_val, prereqOf) { return { "kind": "feature", "key": key, "user": user, + "variation": variation, "value": value, "default": default_val, "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 }; } diff --git a/index.js b/index.js index a470053..610a99a 100644 --- a/index.js +++ b/index.js @@ -138,14 +138,14 @@ var new_client = function(sdk_key, config) { 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); + send_flag_event(key, null, user, null, default_val, default_val); return resolve(default_val); } else if (!user) { variationErr = new errors.LDClientError('No user specified. Returning default value.'); maybeReportError(variationErr); - send_flag_event(key, user, default_val, default_val); + send_flag_event(key, null, user, null, default_val, default_val); return resolve(default_val); } @@ -161,7 +161,7 @@ var new_client = function(sdk_key, config) { } 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); + send_flag_event(key, null, user, null, default_val, default_val); return resolve(default_val); } }); @@ -173,7 +173,7 @@ var new_client = function(sdk_key, config) { 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) { + evaluate.evaluate(flag, user, config.feature_store, function(err, variation, value, events) { var i; var version = flag ? flag.version : null; @@ -189,12 +189,12 @@ var new_client = function(sdk_key, config) { } } - 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); + send_flag_event(key, flag, user, null, default_val, default_val); return resolve(default_val); } else { - send_flag_event(key, user, result, default_val, version); + send_flag_event(key, flag, user, variation, value, default_val); return resolve(result); } }); @@ -329,8 +329,8 @@ var new_client = function(sdk_key, config) { } } - function send_flag_event(key, user, value, default_val, version) { - var event = evaluate.create_flag_event(key, user, value, default_val, version); + function send_flag_event(key, flag, user, variation, value, default_val) { + var event = evaluate.create_flag_event(key, flag, user, variation, value, default_val); enqueue(event); } diff --git a/test/evaluate_flag-test.js b/test/evaluate_flag-test.js index 467a5d0..5eac8cb 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(); }); From 28fd53fc5a7a1880921e6cbd33d8808ef602c3f2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Mar 2018 18:58:30 -0700 Subject: [PATCH 02/38] move EventProcessor out of index.js --- event_processor.js | 80 ++++++++++++++++++++++++++++++++++++++ index.js | 96 +++++++++++++--------------------------------- 2 files changed, 106 insertions(+), 70 deletions(-) create mode 100644 event_processor.js diff --git a/event_processor.js b/event_processor.js new file mode 100644 index 0000000..33da921 --- /dev/null +++ b/event_processor.js @@ -0,0 +1,80 @@ +var request = require('request'); +var EventSerializer = require('./event_serializer'); +var errors = require('./errors'); +var wrapPromiseCallback = require('./utils/wrapPromiseCallback'); + +function EventProcessor(sdk_key, config) { + var ep = {}; + + var eventSerializer = EventSerializer(config); + var queue = []; + var shutdown = false; + var flushTimer; + + ep.send_event = function(event) { + if (shutdown) { + return; + } + config.logger.debug("Sending flag event", JSON.stringify(event)); + queue.push(event); + if (queue.length >= config.capacity) { + ep.flush(); + } + } + + ep.flush = function(callback) { + return wrapPromiseCallback(new Promise(function(resolve, reject) { + var worklist; + if (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); + shutdown = true; + } + } else { + resolve(resp, body); + } + }).on('error', reject); + }.bind(this)), callback); + } + + ep.close = function() { + clearInterval(flushTimer); + } + + flushTimer = setInterval(ep.flush.bind(ep), config.flush_interval * 1000); + + return ep; +} + +module.exports = EventProcessor; diff --git a/index.js b/index.js index 610a99a..cefd32a 100644 --- a/index.js +++ b/index.js @@ -1,10 +1,9 @@ -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 evaluate = require('./evaluate_flag'); @@ -33,13 +32,21 @@ function createErrorReporter(emitter, logger) { global.setImmediate = global.setImmediate || process.nextTick.bind(process); +function NullEventProcessor() { + return { + send_event: function() {}, + flush: function(callback) { callback(); }, + close: function() {} + } +} + var new_client = function(sdk_key, config) { var client = new EventEmitter(), init_complete = false, queue = [], requestor, update_processor, - event_queue_shutdown = false; + event_processor; config = Object.assign({}, config || {}); config.user_agent = 'NodeJSClient/' + package_json.version; @@ -53,6 +60,8 @@ var new_client = function(sdk_key, config) { config.capacity = config.capacity || 1000; config.flush_interval = config.flush_interval || 5; config.poll_interval = config.poll_interval > 30 ? config.poll_interval : 30; + config.user_keys_capacity = config.user_keys_capacity || 1000; + config.user_keys_flush_interval = config.user_keys_flush_interval || 300; // Initialize global tunnel if proxy options are set if (config.proxy_host && config.proxy_port ) { config.proxy_agent = create_proxy_agent(config); @@ -74,8 +83,12 @@ var new_client = function(sdk_key, config) { var featureStore = config.feature_store || InMemoryFeatureStore(); config.feature_store = FeatureStoreEventWrapper(featureStore, client); - var eventSerializer = EventSerializer(config); - + if (config.offline || !config.send_events) { + event_processor = NullEventProcessor(); + } else { + event_processor = EventProcessor(sdk_key, config); + } + var maybeReportError = createErrorReporter(client, config.logger); if (!sdk_key && !config.offline) { @@ -117,7 +130,7 @@ var new_client = function(sdk_key, config) { client.initialized = function() { return init_complete; - } + }; client.waitUntilReady = function() { return new Promise(function(resolve) { @@ -185,7 +198,7 @@ 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]); + event_processor.send_event(events[i]); } } @@ -195,7 +208,7 @@ var new_client = function(sdk_key, config) { return resolve(default_val); } else { send_flag_event(key, flag, user, variation, value, default_val); - return resolve(result); + return resolve(value); } }); }); @@ -237,6 +250,7 @@ var new_client = function(sdk_key, config) { } client.close = function() { + event_processor.close(); if (update_processor) { update_processor.close(); } @@ -258,7 +272,7 @@ var new_client = function(sdk_key, config) { event.data = data; } - enqueue(event); + event_processor.send_event(event); }; client.identify = function(user) { @@ -267,76 +281,18 @@ var new_client = function(sdk_key, config) { "kind": "identify", "user": user, "creationDate": new Date().getTime()}; - enqueue(event); + event_processor.send_event(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); + event_processor.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 send_flag_event(key, flag, user, variation, value, default_val) { var event = evaluate.create_flag_event(key, flag, user, variation, value, default_val); - enqueue(event); + event_processor.send_event(event); } - // TODO keep the reference and stop flushing after close - setInterval(client.flush.bind(client), config.flush_interval * 1000).unref(); - return client; }; From 4a247e51a917fe4277ec63a31ef0bbcdc9451eb0 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 21 Mar 2018 21:23:59 -0700 Subject: [PATCH 03/38] implement summary events --- event_processor.js | 142 ++++++++- event_summarizer.js | 89 ++++++ package-lock.json | 26 +- package.json | 1 + test/event_processor-test.js | 296 ++++++++++++++++++ test/event_summarizer-test.js | 96 ++++++ ...serializer-test.js => user_filter_test.js} | 48 +-- event_serializer.js => user_filter.js | 27 +- 8 files changed, 646 insertions(+), 79 deletions(-) create mode 100644 event_summarizer.js create mode 100644 test/event_processor-test.js create mode 100644 test/event_summarizer-test.js rename test/{event_serializer-test.js => user_filter_test.js} (53%) rename event_serializer.js => user_filter.js (71%) diff --git a/event_processor.js b/event_processor.js index 33da921..30c2561 100644 --- a/event_processor.js +++ b/event_processor.js @@ -1,45 +1,146 @@ var request = require('request'); -var EventSerializer = require('./event_serializer'); +var EventSummarizer = require('./event_summarizer'); +var UserFilter = require('./user_filter'); var errors = require('./errors'); var wrapPromiseCallback = require('./utils/wrapPromiseCallback'); -function EventProcessor(sdk_key, config) { +function EventProcessor(sdk_key, config, request_client) { var ep = {}; - var eventSerializer = EventSerializer(config); - var queue = []; - var shutdown = false; - var flushTimer; + var makeRequest = request_client || request, + userFilter = UserFilter(config), + summarizer = EventSummarizer(config), + 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 && config.logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); + } + } + } + + function should_track_full_event(event) { + if (event.kind === 'feature') { + if (event.trackEvents) { + return true; + } + if (event.debugEventsUntilDate) { + if (event.debugEventsUntilDate > lastKnownPastTime && + event.debugEventsUntilDate > new Date().getTime()) { + return true; + } + } + return false; + } else { + return true; + } + } + + function make_output_event(event) { + if (event.kind === 'feature') { + debug = !event.trackEvents || !!event.debugEventsUntilDate; + var out = { + kind: debug ? 'debug' : 'feature', + creationDate: event.creationDate, + key: event.key, + version: event.version, + value: event.value, + default: event.default, + prereqOf: event.prereqOf + }; + if (config.inline_users_in_events) { + out.user = userFilter.filter_user(event.user); + } else { + out.userKey = event.user.key; + } + return out; + } else if (event.kind === 'identify') { + return { + kind: 'identify', + creationDate: event.creationDate, + user: userFilter.filter_user(event.user) + }; + } else if (event.kind === 'custom') { + var out = { + kind: 'custom', + creationDate: event.creationDate, + key: event.key, + data: event.data + }; + if (config.inline_users_in_events) { + out.user = userFilter.filter_user(event.user); + } else { + out.userKey = event.user.key; + } + return out; + } + return event; + } ep.send_event = function(event) { if (shutdown) { return; } - config.logger.debug("Sending flag event", JSON.stringify(event)); - queue.push(event); - if (queue.length >= config.capacity) { - ep.flush(); + config.logger && config.logger.debug("Sending event", JSON.stringify(event)); + + // For each user we haven't seen before, we add an index event - unless this is already + // an identify event for that user. + if (!config.inline_users_in_events && event.user && !summarizer.notice_user(event.user)) { + if (event.kind != 'identify') { + enqueue({ + kind: 'index', + creationDate: event.creationDate, + user: userFilter.filter_user(event.user) + }); + } + } + + // Always record the event in the summarizer. + summarizer.summarize_event(event); + + if (should_track_full_event(event)) { + enqueue(make_output_event(event)); } } 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; - } else if (!queue.length) { - resolve(); - return; } - worklist = eventSerializer.serialize_events(queue.slice(0)); + worklist = queue; queue = []; + summary = summarizer.get_summary(); + summarizer.clear_summary(); + if (Object.keys(summary.features).length) { + summary.kind = 'summary'; + worklist.push(summary); + } - config.logger.debug("Flushing %d events", worklist.length); + if (!worklist.length) { + resolve(); + return; + } - request({ + config.logger && config.logger.debug("Flushing %d events", worklist.length); + + makeRequest({ method: "POST", url: config.events_uri + '/bulk', headers: { @@ -51,6 +152,12 @@ function EventProcessor(sdk_key, config) { timeout: config.timeout * 1000, agent: config.proxy_agent }).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); @@ -70,9 +177,12 @@ function EventProcessor(sdk_key, config) { ep.close = function() { clearInterval(flushTimer); + clearInterval(flushUsersTimer); } flushTimer = setInterval(ep.flush.bind(ep), config.flush_interval * 1000); + flushUsersTimer = setInterval(summarizer.reset_users.bind(summarizer), + config.user_keys_flush_interval * 1000); return ep; } diff --git a/event_summarizer.js b/event_summarizer.js new file mode 100644 index 0000000..38b46c1 --- /dev/null +++ b/event_summarizer.js @@ -0,0 +1,89 @@ +var LRUCache = require('lrucache'); + +function EventSummarizer(config) { + var es = {}; + + var users = LRUCache(config.user_keys_capacity), + startDate = 0, + endDate = 0, + counters = {}; + + es.notice_user = function(user) { + if (!user || !user.key) { + return false; + } + if (users.get(user.key)) { + return true; + } + users.set(user.key, true); + return false; + } + + es.reset_users = function() { + users.removeAll(); + } + + es.summarize_event = 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, + value: event.value, + default: event.default + }; + } + if (startDate === 0 || event.creationDate < startDate) { + startDate = event.creationDate; + } + if (event.creationDate > endDate) { + endDate = event.creationDate; + } + } + } + + es.get_summary = 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.version) { + counterOut.version = c.version; + } else { + counterOut.unknown = true; + } + flag.counters.push(counterOut); + } + return { + startDate: startDate, + endDate: endDate, + features: flagsOut + }; + } + + es.clear_summary = function() { + startDate = 0; + endDate = 0; + counters = {}; + } + + return es; +} + +module.exports = EventSummarizer; diff --git a/package-lock.json b/package-lock.json index a35f759..d0ae15b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ldclient-node", - "version": "3.0.15", + "version": "4.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1826,14 +1826,6 @@ } } }, - "string_decoder": { - "version": "1.0.1", - "bundled": true, - "dev": true, - "requires": { - "safe-buffer": "5.0.1" - } - }, "string-width": { "version": "1.0.2", "bundled": true, @@ -1844,6 +1836,14 @@ "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, @@ -2985,6 +2985,11 @@ "js-tokens": "3.0.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", @@ -3739,8 +3744,7 @@ "semver": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", - "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==", - "dev": true + "integrity": "sha512-WfG/X9+oATh81XtllIo/I8gOiY9EXRdv1cQdyykeXK17YcUW3EXUAi2To4pcH6nZtJPr7ZOpM5OMyWJZm+8Rsg==" }, "set-blocking": { "version": "2.0.0", diff --git a/package.json b/package.json index 9bee3fe..3864e13 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "async": "^2.0.0-rc.5", "crypto": "0.0.3", + "lrucache": "^1.0.3", "node-cache": "^3.2.1", "node-sha1": "0.0.1", "original": "~0.0.8", diff --git a/test/event_processor-test.js b/test/event_processor-test.js new file mode 100644 index 0000000..e55f433 --- /dev/null +++ b/test/event_processor-test.js @@ -0,0 +1,296 @@ +var EventProcessor = require('../event_processor'); +var EventEmitter = require('events').EventEmitter; + +describe('EventProcessor', function() { + + var ep; + var mockRequest; + var mockServerTime; + var requestParams; + var sdkKey = 'SDK_KEY'; + var defaultConfig = { capacity: 100, flush_interval: 30, user_keys_capacity: 1000, user_keys_flush_interval: 300 }; + var user = { key: 'userKey', name: 'Red' }; + var filteredUser = { key: 'userKey', privateAttrs: [ 'name' ] }; + + beforeEach(function() { + requestParams = null; + mockServerTime = null; + var requestEvent = new EventEmitter(); + mockRequest = function(params) { + requestParams = params; + var resp = { statusCode: 200, headers: {} }; + if (mockServerTime) { + resp.headers['Date'] = new Date(mockServerTime).toUTCString(); + } + setTimeout(function() { + requestEvent.emit('response', resp, null); + }, 0); + return requestEvent; + }; + }); + + afterEach(function() { + if (ep) { + ep.close(); + } + }); + + function flush_and_get_events(cb) { + ep.flush(function() { + cb(requestParams.body); + }); + } + + function check_index_event(e, source) { + expect(e.kind).toEqual('index'); + expect(e.creationDate).toEqual(source.creationDate); + expect(e.user).toEqual(source.user); + } + + function check_feature_event(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.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 check_custom_event(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 check_summary_event(e) { + expect(e.kind).toEqual('summary'); + } + + it('queues identify event', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output).toEqual([{ + kind: 'identify', + creationDate: 1000, + user: user + }]); + done(); + }); + }); + + it('filters user in identify event', function(done) { + var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); + ep = EventProcessor(sdkKey, config, mockRequest); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output).toEqual([{ + kind: 'identify', + creationDate: 1000, + user: filteredUser + }]); + done(); + }); + }); + + it('queues individual feature event with index event', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(3); + check_index_event(output[0], e); + check_feature_event(output[1], e, false); + check_summary_event(output[2]); + done(); + }); + }); + + it('can include inline user in feature event', function(done) { + var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); + ep = EventProcessor(sdkKey, config, mockRequest); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(2); + check_feature_event(output[0], e, false, user); + check_summary_event(output[1]); + done(); + }); + }); + + it('filters user in feature event', function(done) { + var config = Object.assign({}, defaultConfig, { all_attributes_private: true, + inline_users_in_events: true }); + ep = EventProcessor(sdkKey, config, mockRequest); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(2); + check_feature_event(output[0], e, false, filteredUser); + check_summary_event(output[1]); + done(); + }); + }); + + it('sets event kind to debug if event is temporarily in debug mode', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + 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.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(3); + check_index_event(output[0], e); + check_feature_event(output[1], e, true); + check_summary_event(output[2]); + done(); + }); + }); + + it('expires debug mode based on client time if client time is later than server time', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + + // 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 + mockServerTime = serverTime; + ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); + flush_and_get_events(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.send_event(e); + + // Should get a summary event only, not a full feature event + flush_and_get_events(function(output) { + expect(output.length).toEqual(2); + check_index_event(output[0], e); + check_summary_event(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, mockRequest); + + // 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 + mockServerTime = serverTime; + ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); + flush_and_get_events(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.send_event(e); + + // Should get a summary event only, not a full feature event + flush_and_get_events(function(output) { + expect(output.length).toEqual(2); + check_index_event(output[0], e); + check_summary_event(output[1]); + done(); + }); + }); + }); + + it('generates only one index event from two feature events for same user', function(done) { + done(); + }); + + it('summarizes nontracked events', function(done) { + done(); + }); + + it('queues custom event with user', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(2); + check_index_event(output[0], e); + check_custom_event(output[1], e); + done(); + }); + }); + + it('can include inline user in custom event', function(done) { + var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); + ep = EventProcessor(sdkKey, config, mockRequest); + var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(1); + check_custom_event(output[0], e, user); + done(); + }); + }); + + it('filters user in custom event', function(done) { + var config = Object.assign({}, defaultConfig, { all_attributes_private: true, + inline_users_in_events: true }); + ep = EventProcessor(sdkKey, config, mockRequest); + var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', + data: { thing: 'stuff' } }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(1); + check_custom_event(output[0], e, filteredUser); + done(); + }); + }); + + it('sends nothing if there are no events', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep.flush(function() { + expect(requestParams).toEqual(null); + done(); + }); + }); + + it('sends SDK key', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.send_event(e); + + ep.flush(function() { + expect(requestParams.headers['Authorization']).toEqual(sdkKey); + done(); + }); + }); +}); diff --git a/test/event_summarizer-test.js b/test/event_summarizer-test.js new file mode 100644 index 0000000..9ea3089 --- /dev/null +++ b/test/event_summarizer-test.js @@ -0,0 +1,96 @@ +var EventSummarizer = require('../event_summarizer'); + +describe('EventSummarizer', function() { + + var defaultConfig = { user_keys_capacity: 100 }; + var user = { key: 'key1' }; + + it('returns false from notice_user for never-seen user', function() { + var es = EventSummarizer(defaultConfig); + expect(es.notice_user(user)).toEqual(false); + }); + + it('returns true from notice_user for already-seen user', function() { + var es = EventSummarizer(defaultConfig); + es.notice_user({ key: 'key1' }); + expect(es.notice_user({ key: 'key1' })).toEqual(true); + }); + + it('discards oldest user if capacity is exceeded', function() { + var es = EventSummarizer({ user_keys_capacity: 2 }); + es.notice_user({ key: 'key1' }); + es.notice_user({ key: 'key2' }); + es.notice_user({ key: 'key3' }); + expect(es.notice_user({ key: 'key3' })).toEqual(true); + expect(es.notice_user({ key: 'key2' })).toEqual(true); + expect(es.notice_user({ key: 'key1' })).toEqual(false); + }); + + it('does nothing for identify event', function() { + var es = EventSummarizer(defaultConfig); + var snapshot = es.get_summary(); + es.summarize_event({ kind: 'identify', creationDate: 1000, user: user }); + expect(es.get_summary()).toEqual(snapshot); + }); + + it('does nothing for custom event', function() { + var es = EventSummarizer(defaultConfig); + var snapshot = es.get_summary(); + es.summarize_event({ kind: 'custom', creationDate: 1000, key: 'eventkey', user: user }); + expect(es.get_summary()).toEqual(snapshot); + }); + + it('sets start and end dates for feature events', function() { + var es = EventSummarizer(defaultConfig); + 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.summarize_event(event1); + es.summarize_event(event2); + es.summarize_event(event3); + var data = es.get_summary(); + + expect(data.startDate).toEqual(1000); + expect(data.endDate).toEqual(2000); + }); + + it('increments counters for feature events', function() { + var es = EventSummarizer(defaultConfig); + 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.summarize_event(event1); + es.summarize_event(event2); + es.summarize_event(event3); + es.summarize_event(event4); + es.summarize_event(event5); + var data = es.get_summary(); + + data.features.key1.counters.sort(function(a, b) { return a.value - b.value; }); + var expectedFeatures = { + key1: { + default: 111, + counters: [ + { value: 100, version: 11, count: 2 }, + { value: 200, version: 11, count: 1 } + ] + }, + key2: { + default: 222, + counters: [ { 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/event_serializer-test.js b/test/user_filter_test.js similarity index 53% rename from test/event_serializer-test.js rename to test/user_filter_test.js index 8ad5820..ad2f070 100644 --- a/test/event_serializer-test.js +++ b/test/user_filter_test.js @@ -1,7 +1,7 @@ var assert = require('assert'); -var EventSerializer = require('../event_serializer.js'); +var UserFilter = require('../user_filter'); -describe('event_serializer', function() { +describe('user_filter', function() { // users to serialize var user = { @@ -62,54 +62,38 @@ describe('event_serializer', function() { '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]); + var uf = UserFilter({}); + assert.deepEqual(uf.filter_user(user), user); }); 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)]); + var uf = UserFilter({ all_attributes_private: true}); + assert.deepEqual(uf.filter_user(user), 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)]); + var uf = UserFilter({ private_attribute_names: [ 'firstName', 'bizzle' ]}); + assert.deepEqual(uf.filter_user(user), 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)]); + var uf = UserFilter({}); + assert.deepEqual(uf.filter_user(user_specifying_own_private_attr), 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)]); + var uf = UserFilter({ private_attribute_names: [ 'firstName', 'bizzle' ]}); + assert.deepEqual(uf.filter_user(user_specifying_own_private_attr), 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)]); + var uf = UserFilter({}); + assert.deepEqual(uf.filter_user(user_with_unknown_top_level_attrs), 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)]); + var uf = UserFilter({ all_attributes_private: true}); + assert.deepEqual(uf.filter_user(anon_user), anon_user_with_all_attrs_hidden); }); }); diff --git a/event_serializer.js b/user_filter.js similarity index 71% rename from event_serializer.js rename to user_filter.js index 0ec817e..43a826e 100644 --- a/event_serializer.js +++ b/user_filter.js @@ -1,31 +1,18 @@ /** - * 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 = {}; +function UserFilter(config) { + var filter = {}; var allAttributesPrivate = config.all_attributes_private; var privateAttributeNames = config.private_attribute_names || []; 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.filter_user = function(user) { var allPrivateAttrs = {}; var userPrivateAttrs = user.privateAttributeNames || []; @@ -63,7 +50,7 @@ function EventSerializer(config) { return filteredProps; } - return serializer; + return filter; } -module.exports = EventSerializer; +module.exports = UserFilter; From 6d580423b1d821045b9c322e92fdf2d81f74a6c6 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 22 Mar 2018 12:50:35 -0700 Subject: [PATCH 04/38] misc fixes --- event_processor.js | 6 +- index.js | 6 +- test/event_processor-test.js | 120 ++++++++++++++++++++++++++--------- 3 files changed, 95 insertions(+), 37 deletions(-) diff --git a/event_processor.js b/event_processor.js index 30c2561..39d8163 100644 --- a/event_processor.js +++ b/event_processor.js @@ -4,7 +4,7 @@ var UserFilter = require('./user_filter'); var errors = require('./errors'); var wrapPromiseCallback = require('./utils/wrapPromiseCallback'); -function EventProcessor(sdk_key, config, request_client) { +function EventProcessor(sdk_key, config, error_reporter, request_client) { var ep = {}; var makeRequest = request_client || request, @@ -161,11 +161,11 @@ function EventProcessor(sdk_key, config, request_client) { if (resp.statusCode > 204) { var err = new errors.LDUnexpectedResponseError("Unexpected status code " + resp.statusCode + "; events may not have been processed", resp.statusCode); - maybeReportError(err); + error_reporter && error_reporter(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); + error_reporter && error_reporter(err1); shutdown = true; } } else { diff --git a/index.js b/index.js index cefd32a..481b4b4 100644 --- a/index.js +++ b/index.js @@ -83,14 +83,14 @@ var new_client = function(sdk_key, config) { var featureStore = config.feature_store || InMemoryFeatureStore(); config.feature_store = FeatureStoreEventWrapper(featureStore, client); + var maybeReportError = createErrorReporter(client, config.logger); + if (config.offline || !config.send_events) { event_processor = NullEventProcessor(); } else { - event_processor = EventProcessor(sdk_key, config); + event_processor = EventProcessor(sdk_key, config, maybeReportError); } - var maybeReportError = createErrorReporter(client, config.logger); - if (!sdk_key && !config.offline) { throw new Error("You must configure the client with an SDK key"); } diff --git a/test/event_processor-test.js b/test/event_processor-test.js index e55f433..b7272f5 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -5,8 +5,9 @@ describe('EventProcessor', function() { var ep; var mockRequest; - var mockServerTime; var requestParams; + var mockResponse; + var responseStatus; var sdkKey = 'SDK_KEY'; var defaultConfig = { capacity: 100, flush_interval: 30, user_keys_capacity: 1000, user_keys_flush_interval: 300 }; var user = { key: 'userKey', name: 'Red' }; @@ -14,16 +15,12 @@ describe('EventProcessor', function() { beforeEach(function() { requestParams = null; - mockServerTime = null; + mockResponse = { statusCode: 200, headers: {} }; var requestEvent = new EventEmitter(); mockRequest = function(params) { requestParams = params; - var resp = { statusCode: 200, headers: {} }; - if (mockServerTime) { - resp.headers['Date'] = new Date(mockServerTime).toUTCString(); - } setTimeout(function() { - requestEvent.emit('response', resp, null); + requestEvent.emit('response', mockResponse, null); }, 0); return requestEvent; }; @@ -41,10 +38,14 @@ describe('EventProcessor', function() { }); } - function check_index_event(e, source) { + function add_date_header(response, timestamp) { + response.headers['Date'] = new Date(timestamp).toUTCString(); + } + + function check_index_event(e, source, user) { expect(e.kind).toEqual('index'); expect(e.creationDate).toEqual(source.creationDate); - expect(e.user).toEqual(source.user); + expect(e.user).toEqual(user); } function check_feature_event(e, source, debug, inlineUser) { @@ -78,7 +79,7 @@ describe('EventProcessor', function() { } it('queues identify event', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); @@ -94,7 +95,7 @@ describe('EventProcessor', function() { it('filters user in identify event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); - ep = EventProcessor(sdkKey, config, mockRequest); + ep = EventProcessor(sdkKey, config, null, mockRequest); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); @@ -109,14 +110,30 @@ describe('EventProcessor', function() { }); it('queues individual feature event with index event', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); flush_and_get_events(function(output) { expect(output.length).toEqual(3); - check_index_event(output[0], e); + check_index_event(output[0], e, user); + check_feature_event(output[1], e, false); + check_summary_event(output[2]); + done(); + }); + }); + + it('filters user in index event', function(done) { + var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); + ep = EventProcessor(sdkKey, config, null, mockRequest); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: true }; + ep.send_event(e); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(3); + check_index_event(output[0], e, filteredUser); check_feature_event(output[1], e, false); check_summary_event(output[2]); done(); @@ -125,7 +142,7 @@ describe('EventProcessor', function() { it('can include inline user in feature event', function(done) { var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, mockRequest); + ep = EventProcessor(sdkKey, config, null, mockRequest); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); @@ -141,7 +158,7 @@ describe('EventProcessor', function() { it('filters user in feature event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true, inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, mockRequest); + ep = EventProcessor(sdkKey, config, null, mockRequest); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); @@ -155,7 +172,7 @@ describe('EventProcessor', function() { }); it('sets event kind to debug if event is temporarily in debug mode', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); 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 }; @@ -163,7 +180,7 @@ describe('EventProcessor', function() { flush_and_get_events(function(output) { expect(output.length).toEqual(3); - check_index_event(output[0], e); + check_index_event(output[0], e, user); check_feature_event(output[1], e, true); check_summary_event(output[2]); done(); @@ -171,13 +188,13 @@ describe('EventProcessor', function() { }); it('expires debug mode based on client time if client time is later than server time', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); // 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 - mockServerTime = serverTime; + add_date_header(mockResponse, serverTime); ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); flush_and_get_events(function() { // Now send an event with debug mode on, with a "debug until" time that is further in @@ -190,7 +207,7 @@ describe('EventProcessor', function() { // Should get a summary event only, not a full feature event flush_and_get_events(function(output) { expect(output.length).toEqual(2); - check_index_event(output[0], e); + check_index_event(output[0], e, user); check_summary_event(output[1]); done(); }); @@ -198,13 +215,13 @@ describe('EventProcessor', function() { }); it('expires debug mode based on server time if server time is later than client time', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); // 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 - mockServerTime = serverTime; + add_date_header(mockResponse, serverTime); ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); flush_and_get_events(function() { // Now send an event with debug mode on, with a "debug until" time that is further in @@ -217,7 +234,7 @@ describe('EventProcessor', function() { // Should get a summary event only, not a full feature event flush_and_get_events(function(output) { expect(output.length).toEqual(2); - check_index_event(output[0], e); + check_index_event(output[0], e, user); check_summary_event(output[1]); done(); }); @@ -225,22 +242,63 @@ describe('EventProcessor', function() { }); it('generates only one index event from two feature events for same user', function(done) { - done(); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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.send_event(e1); + ep.send_event(e2); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(4); + check_index_event(output[0], e1, user); + check_feature_event(output[1], e1, false); + check_feature_event(output[2], e2, false); + check_summary_event(output[3]); + done(); + }); }); it('summarizes nontracked events', function(done) { - done(); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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.send_event(e1); + ep.send_event(e2); + + flush_and_get_events(function(output) { + expect(output.length).toEqual(2); + check_index_event(output[0], e1, user); + var se = output[1]; + check_summary_event(se); + expect(se.startDate).toEqual(1000); + expect(se.endDate).toEqual(2000); + expect(se.features).toEqual({ + flagkey1: { + default: 'default1', + counters: [ { version: 11, value: 'value1', count: 1 } ] + }, + flagkey2: { + default: 'default2', + counters: [ { versino: 22, value: 'value2', count: 1 } ] + } + }); + done(); + }); }); it('queues custom event with user', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; ep.send_event(e); flush_and_get_events(function(output) { expect(output.length).toEqual(2); - check_index_event(output[0], e); + check_index_event(output[0], e, user); check_custom_event(output[1], e); done(); }); @@ -248,7 +306,7 @@ describe('EventProcessor', function() { it('can include inline user in custom event', function(done) { var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, mockRequest); + ep = EventProcessor(sdkKey, config, null, mockRequest); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; ep.send_event(e); @@ -263,7 +321,7 @@ describe('EventProcessor', function() { it('filters user in custom event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true, inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, mockRequest); + ep = EventProcessor(sdkKey, config, null, mockRequest); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; ep.send_event(e); @@ -276,7 +334,7 @@ describe('EventProcessor', function() { }); it('sends nothing if there are no events', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); ep.flush(function() { expect(requestParams).toEqual(null); done(); @@ -284,7 +342,7 @@ describe('EventProcessor', function() { }); it('sends SDK key', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); From 2f6e5a1f925e4490df08665444c0fa075b7dbe1c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 22 Mar 2018 12:51:26 -0700 Subject: [PATCH 05/38] typo --- test/event_processor-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/event_processor-test.js b/test/event_processor-test.js index b7272f5..c28afa4 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -283,7 +283,7 @@ describe('EventProcessor', function() { }, flagkey2: { default: 'default2', - counters: [ { versino: 22, value: 'value2', count: 1 } ] + counters: [ { version: 22, value: 'value2', count: 1 } ] } }); done(); From 53f61c41ffde47556f5545a57f3c13a238d38c6d Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 23 Mar 2018 11:07:58 -0700 Subject: [PATCH 06/38] add test for 401 error --- test/event_processor-test.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/event_processor-test.js b/test/event_processor-test.js index c28afa4..821b581 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -7,7 +7,6 @@ describe('EventProcessor', function() { var mockRequest; var requestParams; var mockResponse; - var responseStatus; var sdkKey = 'SDK_KEY'; var defaultConfig = { capacity: 100, flush_interval: 30, user_keys_capacity: 1000, user_keys_flush_interval: 300 }; var user = { key: 'userKey', name: 'Red' }; @@ -351,4 +350,20 @@ describe('EventProcessor', function() { done(); }); }); + + it('stops sending events after a 401 error', function(done) { + ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.send_event(e); + + mockResponse.statusCode = 401; + ep.flush(function() { + requestParams = null; + ep.send_event(e); + ep.flush(function() { + expect(requestParams).toEqual(null); + done(); + }); + }); + }); }); From e0aed4044e03dc315cb169b2ec565cefe6d21348 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 23 Mar 2018 15:51:28 -0700 Subject: [PATCH 07/38] fix test to catch promise rejections --- test/event_processor-test.js | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/test/event_processor-test.js b/test/event_processor-test.js index 821b581..dfbeedf 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -357,13 +357,22 @@ describe('EventProcessor', function() { ep.send_event(e); mockResponse.statusCode = 401; - ep.flush(function() { - requestParams = null; - ep.send_event(e); - ep.flush(function() { - expect(requestParams).toEqual(null); - done(); - }); - }); + ep.flush().then( + function() { }, + function(err) { + expect(err.message).toContain("status code 401"); + requestParams = null; + + ep.send_event(e); + + ep.flush().then( + function() { }, + function(err) { + expect(err.message).toContain("SDK key is invalid"); + expect(requestParams).toEqual(null); + done(); + }); + } + ); }); }); From 3151932ebbd0a5fd4de0473d83b867b451d8d2da Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 23 Mar 2018 16:38:55 -0700 Subject: [PATCH 08/38] revise tests using Nock --- event_processor.js | 4 +- package-lock.json | 244 +++++++++++++++++++++++++---------- package.json | 11 +- test/event_processor-test.js | 157 +++++++++++----------- 4 files changed, 264 insertions(+), 152 deletions(-) diff --git a/event_processor.js b/event_processor.js index 39d8163..749e292 100644 --- a/event_processor.js +++ b/event_processor.js @@ -152,8 +152,8 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { timeout: config.timeout * 1000, agent: config.proxy_agent }).on('response', function(resp, body) { - if (resp.headers['Date']) { - var date = Date.parse(resp.headers['Date']); + if (resp.headers['date']) { + var date = Date.parse(resp.headers['date']); if (date) { lastKnownPastTime = date; } diff --git a/package-lock.json b/package-lock.json index d0ae15b..7051cf1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,14 +26,14 @@ } }, "ajv": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.2.3.tgz", - "integrity": "sha1-wG9Zh3jETGsWGrr+NGa4GtGBTtI=", + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", "requires": { "co": "4.6.0", - "fast-deep-equal": "1.0.0", - "json-schema-traverse": "0.3.1", - "json-stable-stringify": "1.0.1" + "fast-deep-equal": "1.1.0", + "fast-json-stable-stringify": "2.0.0", + "json-schema-traverse": "0.3.1" } }, "align-text": { @@ -145,6 +145,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 + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -152,9 +158,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" } @@ -462,7 +468,7 @@ "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": { @@ -539,6 +545,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.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz", @@ -567,6 +587,12 @@ } } }, + "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.1", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.1.1.tgz", @@ -631,9 +657,9 @@ "integrity": "sha1-BDP0TYCWgP3rYO0mDxsMJi6CpAs=" }, "combined-stream": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz", - "integrity": "sha1-k4NwpXtKUd6ix3wV1cX9+JUWQAk=", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", "requires": { "delayed-stream": "1.0.0" } @@ -703,7 +729,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" } } } @@ -741,12 +767,36 @@ "assert-plus": "1.0.0" } }, + "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" + } + }, "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "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-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -934,9 +984,14 @@ "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=" }, "fast-deep-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz", - "integrity": "sha1-liVqO8l1WV6zbYLpkp0GDYk0Of8=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -1037,13 +1092,13 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.1.tgz", - "integrity": "sha1-b7lPvXGIUwbXPRXMSX/kzE7NRL8=", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", "requires": { "asynckit": "0.4.0", - "combined-stream": "1.0.5", - "mime-types": "2.1.17" + "combined-stream": "1.0.6", + "mime-types": "2.1.18" } }, "fs.realpath": { @@ -1962,6 +2017,12 @@ "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", @@ -2052,7 +2113,7 @@ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz", "integrity": "sha1-ukAsJmGU8VlW7xXg/PJCmT9qff0=", "requires": { - "ajv": "5.2.3", + "ajv": "5.5.2", "har-schema": "2.0.0" } }, @@ -2078,14 +2139,14 @@ "requires": { "boom": "4.3.1", "cryptiles": "3.1.2", - "hoek": "4.2.0", - "sntp": "2.0.2" + "hoek": "4.2.1", + "sntp": "2.1.0" } }, "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", @@ -2119,7 +2180,7 @@ "requires": { "assert-plus": "1.0.0", "jsprim": "1.4.1", - "sshpk": "1.13.1" + "sshpk": "1.14.1" } }, "iconv-lite": { @@ -2319,7 +2380,7 @@ "integrity": "sha512-oFCwXvd65amgaPCzqrR+a2XjanS1MvpXN6l/MlMUTv6uiA1NOgGX+I0uyq8Lg3GDxsxPsaP1049krz3hIJ5+KA==", "dev": true, "requires": { - "async": "2.5.0", + "async": "2.6.0", "fileset": "2.0.3", "istanbul-lib-coverage": "1.1.1", "istanbul-lib-hook": "1.1.0", @@ -2359,7 +2420,7 @@ "babel-types": "6.26.0", "babylon": "6.18.0", "istanbul-lib-coverage": "1.1.1", - "semver": "5.4.1" + "semver": "5.5.0" } }, "istanbul-lib-report": { @@ -2822,7 +2883,7 @@ "html-encoding-sniffer": "1.0.2", "nwmatcher": "1.4.3", "parse5": "1.5.1", - "request": "2.83.0", + "request": "2.85.0", "sax": "1.2.4", "symbol-tree": "3.2.2", "tough-cookie": "2.3.3", @@ -2852,6 +2913,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, "requires": { "jsonify": "0.0.0" } @@ -2870,7 +2932,8 @@ "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true }, "jsprim": { "version": "1.4.1", @@ -3036,16 +3099,16 @@ } }, "mime-db": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.30.0.tgz", - "integrity": "sha1-dMZD2i3Z1qRTmZY0ZbJtXKfXHwE=" + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" }, "mime-types": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.17.tgz", - "integrity": "sha1-Cdejk/A+mVp5+K+Fe3Cp4KsWVXo=", + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", "requires": { - "mime-db": "1.30.0" + "mime-db": "1.33.0" } }, "mimic-fn": { @@ -3069,6 +3132,12 @@ "minimist": "0.0.8" } }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, "nan": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.7.0.tgz", @@ -3082,6 +3151,31 @@ "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.5", + "mkdirp": "0.5.1", + "propagate": "1.0.0", + "qs": "6.5.1", + "semver": "5.5.0" + }, + "dependencies": { + "lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-svL3uiZf1RwhH+cWrfZn3A4+U58wbP0tGVTLQPbjplZxZ8ROD9VLuNgsRniTlLe7OlSqR79RUehXgpBW/s0IQw==", + "dev": true + } + } + }, "node-cache": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/node-cache/-/node-cache-3.2.1.tgz", @@ -3104,7 +3198,7 @@ "dev": true, "requires": { "growly": "1.3.0", - "semver": "5.4.1", + "semver": "5.5.0", "shellwords": "0.1.1", "which": "1.3.0" } @@ -3122,7 +3216,7 @@ "requires": { "hosted-git-info": "2.5.0", "is-builtin-module": "1.0.0", - "semver": "5.4.1", + "semver": "5.5.0", "validate-npm-package-license": "3.0.1" } }, @@ -3346,6 +3440,12 @@ } } }, + "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", @@ -3408,6 +3508,12 @@ "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", "dev": true }, + "propagate": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", + "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", + "dev": true + }, "prr": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", @@ -3580,24 +3686,24 @@ } }, "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", "caseless": "0.12.0", - "combined-stream": "1.0.5", + "combined-stream": "1.0.6", "extend": "3.0.1", "forever-agent": "0.6.1", - "form-data": "2.3.1", + "form-data": "2.3.2", "har-validator": "5.0.3", "hawk": "6.0.2", "http-signature": "1.2.0", "is-typedarray": "1.0.0", "isstream": "0.1.2", "json-stringify-safe": "5.0.1", - "mime-types": "2.1.17", + "mime-types": "2.1.18", "oauth-sign": "0.8.2", "performance-now": "2.1.0", "qs": "6.5.1", @@ -3605,7 +3711,7 @@ "stringstream": "0.0.5", "tough-cookie": "2.3.3", "tunnel-agent": "0.6.0", - "uuid": "3.1.0" + "uuid": "3.2.1" } }, "request-etag": { @@ -3616,7 +3722,7 @@ "lodash.assign": "4.2.0", "lodash.clonedeep": "4.5.0", "lru-cache": "4.1.1", - "request": "2.83.0" + "request": "2.85.0" }, "dependencies": { "lru-cache": { @@ -3742,9 +3848,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", @@ -3786,11 +3892,11 @@ "dev": true }, "sntp": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.0.2.tgz", - "integrity": "sha1-UGQRDwr4X3z9t9a2ekACjOUrSys=", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-2.1.0.tgz", + "integrity": "sha512-FL1b58BDrqS3A11lJ0zEdnJ3UOKqVxawAkF3k7F0CVN7VQ34aZrV+G8BZ1WC9ZL7NyrwsW0oviwsWDgRuVYtJg==", "requires": { - "hoek": "4.2.0" + "hoek": "4.2.1" } }, "source-map": { @@ -3836,9 +3942,9 @@ "dev": true }, "sshpk": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", - "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", + "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", "requires": { "asn1": "0.2.3", "assert-plus": "1.0.0", @@ -4004,6 +4110,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", @@ -4048,9 +4160,9 @@ } }, "uuid": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", - "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==" }, "validate-npm-package-license": { "version": "3.0.1", @@ -4155,9 +4267,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 3864e13..8f0176e 100644 --- a/package.json +++ b/package.json @@ -22,24 +22,25 @@ }, "homepage": "https://github.com/launchdarkly/node-client", "dependencies": { - "async": "^2.0.0-rc.5", + "async": "2.6.0", "crypto": "0.0.3", "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": "21.2.1" + "jest": "21.2.1", + "nock": "9.2.3" }, "jest": { "rootDir": ".", diff --git a/test/event_processor-test.js b/test/event_processor-test.js index dfbeedf..53986ea 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -1,44 +1,50 @@ +var nock = require('nock'); var EventProcessor = require('../event_processor'); var EventEmitter = require('events').EventEmitter; describe('EventProcessor', function() { var ep; - var mockRequest; - var requestParams; - var mockResponse; + var eventsUri = 'http://example.com'; var sdkKey = 'SDK_KEY'; - var defaultConfig = { capacity: 100, flush_interval: 30, user_keys_capacity: 1000, user_keys_flush_interval: 300 }; + var defaultConfig = { + events_uri: eventsUri, + capacity: 100, + flush_interval: 30, + user_keys_capacity: 1000, + user_keys_flush_interval: 300 + }; var user = { key: 'userKey', name: 'Red' }; var filteredUser = { key: 'userKey', privateAttrs: [ 'name' ] }; - beforeEach(function() { - requestParams = null; - mockResponse = { statusCode: 200, headers: {} }; - var requestEvent = new EventEmitter(); - mockRequest = function(params) { - requestParams = params; - setTimeout(function() { - requestEvent.emit('response', mockResponse, null); - }, 0); - return requestEvent; - }; - }); - afterEach(function() { if (ep) { ep.close(); } }); - function flush_and_get_events(cb) { - ep.flush(function() { - cb(requestParams.body); - }); + function flush_and_get_request(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 add_date_header(response, timestamp) { - response.headers['Date'] = new Date(timestamp).toUTCString(); + function headers_with_date(timestamp) { + return { date: new Date(timestamp).toUTCString() }; } function check_index_event(e, source, user) { @@ -78,11 +84,11 @@ describe('EventProcessor', function() { } it('queues identify event', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output).toEqual([{ kind: 'identify', creationDate: 1000, @@ -94,11 +100,11 @@ describe('EventProcessor', function() { it('filters user in identify event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); - ep = EventProcessor(sdkKey, config, null, mockRequest); + ep = EventProcessor(sdkKey, config); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output).toEqual([{ kind: 'identify', creationDate: 1000, @@ -109,12 +115,12 @@ describe('EventProcessor', function() { }); it('queues individual feature event with index event', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(3); check_index_event(output[0], e, user); check_feature_event(output[1], e, false); @@ -125,12 +131,12 @@ describe('EventProcessor', function() { it('filters user in index event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); - ep = EventProcessor(sdkKey, config, null, mockRequest); + ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(3); check_index_event(output[0], e, filteredUser); check_feature_event(output[1], e, false); @@ -141,12 +147,12 @@ describe('EventProcessor', function() { it('can include inline user in feature event', function(done) { var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, null, mockRequest); + ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(2); check_feature_event(output[0], e, false, user); check_summary_event(output[1]); @@ -157,12 +163,12 @@ describe('EventProcessor', function() { it('filters user in feature event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true, inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, null, mockRequest); + ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(2); check_feature_event(output[0], e, false, filteredUser); check_summary_event(output[1]); @@ -171,13 +177,13 @@ describe('EventProcessor', function() { }); it('sets event kind to debug if event is temporarily in debug mode', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(3); check_index_event(output[0], e, user); check_feature_event(output[1], e, true); @@ -187,15 +193,14 @@ describe('EventProcessor', function() { }); it('expires debug mode based on client time if client time is later than server time', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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 - add_date_header(mockResponse, serverTime); ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); - flush_and_get_events(function() { + flush_and_get_request({ status: 200, headers: headers_with_date(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; @@ -204,7 +209,7 @@ describe('EventProcessor', function() { ep.send_event(e); // Should get a summary event only, not a full feature event - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(2); check_index_event(output[0], e, user); check_summary_event(output[1]); @@ -214,15 +219,14 @@ describe('EventProcessor', function() { }); it('expires debug mode based on server time if server time is later than client time', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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 - add_date_header(mockResponse, serverTime); ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); - flush_and_get_events(function() { + flush_and_get_request({ status: 200, headers: headers_with_date(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; @@ -231,7 +235,7 @@ describe('EventProcessor', function() { ep.send_event(e); // Should get a summary event only, not a full feature event - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(2); check_index_event(output[0], e, user); check_summary_event(output[1]); @@ -241,7 +245,7 @@ describe('EventProcessor', function() { }); it('generates only one index event from two feature events for same user', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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', @@ -249,7 +253,7 @@ describe('EventProcessor', function() { ep.send_event(e1); ep.send_event(e2); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(4); check_index_event(output[0], e1, user); check_feature_event(output[1], e1, false); @@ -260,7 +264,7 @@ describe('EventProcessor', function() { }); it('summarizes nontracked events', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + 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', @@ -268,7 +272,7 @@ describe('EventProcessor', function() { ep.send_event(e1); ep.send_event(e2); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(2); check_index_event(output[0], e1, user); var se = output[1]; @@ -290,12 +294,12 @@ describe('EventProcessor', function() { }); it('queues custom event with user', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(2); check_index_event(output[0], e, user); check_custom_event(output[1], e); @@ -305,12 +309,12 @@ describe('EventProcessor', function() { it('can include inline user in custom event', function(done) { var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, null, mockRequest); + ep = EventProcessor(sdkKey, config); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(1); check_custom_event(output[0], e, user); done(); @@ -320,12 +324,12 @@ describe('EventProcessor', function() { it('filters user in custom event', function(done) { var config = Object.assign({}, defaultConfig, { all_attributes_private: true, inline_users_in_events: true }); - ep = EventProcessor(sdkKey, config, null, mockRequest); + ep = EventProcessor(sdkKey, config); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; ep.send_event(e); - flush_and_get_events(function(output) { + flush_and_get_request(function(output) { expect(output.length).toEqual(1); check_custom_event(output[0], e, filteredUser); done(); @@ -333,46 +337,41 @@ describe('EventProcessor', function() { }); it('sends nothing if there are no events', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig); ep.flush(function() { - expect(requestParams).toEqual(null); + // 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, null, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); - ep.flush(function() { - expect(requestParams.headers['Authorization']).toEqual(sdkKey); + flush_and_get_request(function(requestBody, requestHeaders) { + expect(requestHeaders['authorization']).toEqual(sdkKey); done(); }); }); it('stops sending events after a 401 error', function(done) { - ep = EventProcessor(sdkKey, defaultConfig, null, mockRequest); + ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); - mockResponse.statusCode = 401; - ep.flush().then( - function() { }, - function(err) { - expect(err.message).toContain("status code 401"); - requestParams = null; - - ep.send_event(e); - - ep.flush().then( - function() { }, - function(err) { - expect(err.message).toContain("SDK key is invalid"); - expect(requestParams).toEqual(null); - done(); - }); - } - ); + flush_and_get_request({ status: 401 }, function(body, headers, error) { + expect(error.message).toContain("status code 401"); + + ep.send_event(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(); + }); + }); }); }); From ccafa73721ff5451244221a9e614a1090f0d3c6c Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Mon, 26 Mar 2018 08:33:44 -0400 Subject: [PATCH 09/38] fix promise/callback utility: avoid rejecting and returning the promise if the caller is using the callback interface --- utils/__tests__/wrapPromiseCallback-test.js | 32 ++++++++------------- utils/wrapPromiseCallback.js | 9 ++++-- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/utils/__tests__/wrapPromiseCallback-test.js b/utils/__tests__/wrapPromiseCallback-test.js index 4c07443..ea991d9 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); + 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(); + expect(error).toEqual(error); }); }); }); \ 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 From 1a80d2b005cbc533003e8b4c353946bfc96b83b3 Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Mon, 26 Mar 2018 14:45:20 -0400 Subject: [PATCH 10/38] remove unneeded assertion --- utils/__tests__/wrapPromiseCallback-test.js | 1 - 1 file changed, 1 deletion(-) diff --git a/utils/__tests__/wrapPromiseCallback-test.js b/utils/__tests__/wrapPromiseCallback-test.js index ea991d9..274394a 100644 --- a/utils/__tests__/wrapPromiseCallback-test.js +++ b/utils/__tests__/wrapPromiseCallback-test.js @@ -29,7 +29,6 @@ describe('wrapPromiseCallback',function() { expect(promise).toBeUndefined(); expect(error).toBe(actualError); expect(value).toBeNull(); - expect(error).toEqual(error); }); }); }); \ No newline at end of file From 04a1a4e0851adb0a74c47d8530c4f909761df07a Mon Sep 17 00:00:00 2001 From: Alexis Georges Date: Mon, 26 Mar 2018 14:45:46 -0400 Subject: [PATCH 11/38] call done() --- utils/__tests__/wrapPromiseCallback-test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/__tests__/wrapPromiseCallback-test.js b/utils/__tests__/wrapPromiseCallback-test.js index 274394a..2cfae6a 100644 --- a/utils/__tests__/wrapPromiseCallback-test.js +++ b/utils/__tests__/wrapPromiseCallback-test.js @@ -23,12 +23,13 @@ describe('wrapPromiseCallback',function() { }); }); - it('should call the callback with an error if the promise rejects', function() { + 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 From 2ba1d2cffcd91c88df6d201756dc61b9901da182 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 4 Apr 2018 17:51:57 -0700 Subject: [PATCH 12/38] explicit return --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 0f1a221..ad01f12 100644 --- a/index.js +++ b/index.js @@ -288,7 +288,7 @@ var new_client = function(sdk_key, config) { }; client.flush = function(callback) { - event_processor.flush(callback); + return event_processor.flush(callback); }; function send_flag_event(key, flag, user, variation, value, default_val) { From 6e3ba858b65900602d0570b2f9b55c781fe7adb4 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 9 Apr 2018 17:32:14 -0700 Subject: [PATCH 13/38] debug events should be in addition to regular events, & should always include inline user --- event_processor.js | 61 +++++++++++++++++++++--------------- test/event_processor-test.js | 19 ++++++++++- 2 files changed, 54 insertions(+), 26 deletions(-) diff --git a/event_processor.js b/event_processor.js index 749e292..a25d26f 100644 --- a/event_processor.js +++ b/event_processor.js @@ -29,26 +29,19 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { } } - function should_track_full_event(event) { - if (event.kind === 'feature') { - if (event.trackEvents) { + function should_debug_event(event) { + if (event.debugEventsUntilDate) { + if (event.debugEventsUntilDate > lastKnownPastTime && + event.debugEventsUntilDate > new Date().getTime()) { return true; } - if (event.debugEventsUntilDate) { - if (event.debugEventsUntilDate > lastKnownPastTime && - event.debugEventsUntilDate > new Date().getTime()) { - return true; - } - } - return false; - } else { - return true; } + return false; } function make_output_event(event) { if (event.kind === 'feature') { - debug = !event.trackEvents || !!event.debugEventsUntilDate; + debug = !!event.debug; var out = { kind: debug ? 'debug' : 'feature', creationDate: event.creationDate, @@ -58,7 +51,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { default: event.default, prereqOf: event.prereqOf }; - if (config.inline_users_in_events) { + if (config.inline_users_in_events || debug) { out.user = userFilter.filter_user(event.user); } else { out.userKey = event.user.key; @@ -93,24 +86,42 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { } config.logger && config.logger.debug("Sending event", JSON.stringify(event)); + // Always record the event in the summarizer. + summarizer.summarize_event(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. + var willAddFullEvent = false, + debugEvent = null; + if (event.kind === 'feature') { + willAddFullEvent = event.trackEvents; + if (should_debug_event(event)) { + debugEvent = Object.assign({}, event, { debug: true }); + } + } else { + willAddFullEvent = 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 (!config.inline_users_in_events && event.user && !summarizer.notice_user(event.user)) { - if (event.kind != 'identify') { - enqueue({ - kind: 'index', - creationDate: event.creationDate, - user: userFilter.filter_user(event.user) - }); + if (!(willAddFullEvent && config.inline_users_in_events)) { + if (event.user && !summarizer.notice_user(event.user)) { + if (event.kind != 'identify') { + enqueue({ + kind: 'index', + creationDate: event.creationDate, + user: userFilter.filter_user(event.user) + }); + } } } - // Always record the event in the summarizer. - summarizer.summarize_event(event); - - if (should_track_full_event(event)) { + if (willAddFullEvent) { enqueue(make_output_event(event)); } + if (debugEvent) { + enqueue(make_output_event(debugEvent)); + } } ep.flush = function(callback) { diff --git a/test/event_processor-test.js b/test/event_processor-test.js index 53986ea..11480d2 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -186,12 +186,29 @@ describe('EventProcessor', function() { flush_and_get_request(function(output) { expect(output.length).toEqual(3); check_index_event(output[0], e, user); - check_feature_event(output[1], e, true); + check_feature_event(output[1], e, true, user); check_summary_event(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.send_event(e); + + flush_and_get_request(function(output) { + expect(output.length).toEqual(4); + check_index_event(output[0], e, user); + check_feature_event(output[1], e, false); + check_feature_event(output[2], e, true, user); + check_summary_event(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); From ecf667b07dcd0d330728640a5fe37cdae39f620c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 9 Apr 2018 17:42:28 -0700 Subject: [PATCH 14/38] add unit test --- test/event_processor-test.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/event_processor-test.js b/test/event_processor-test.js index 11480d2..c4fca17 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -176,6 +176,21 @@ describe('EventProcessor', function() { }); }); + it('still generates index event if inline_users is true but feature event is not tracked', function(done) { + var config = Object.assign({}, defaultConfig, { inline_users_in_events: true }); + ep = EventProcessor(sdkKey, config); + var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', + version: 11, variation: 1, value: 'value', trackEvents: false }; + ep.send_event(e); + + flush_and_get_request(function(output) { + expect(output.length).toEqual(2); + check_index_event(output[0], e, user); + check_summary_event(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; From 961042d2a99d7a9364eef96a52b0d073aad2fe9b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 10 Apr 2018 15:09:15 -0700 Subject: [PATCH 15/38] retry event post once after a 5xx error or connection error --- event_processor.js | 84 ++++++++++++++++++++++-------------- index.js | 4 +- test/event_processor-test.js | 16 +++++++ 3 files changed, 70 insertions(+), 34 deletions(-) diff --git a/event_processor.js b/event_processor.js index a25d26f..b9c6648 100644 --- a/event_processor.js +++ b/event_processor.js @@ -4,11 +4,10 @@ var UserFilter = require('./user_filter'); var errors = require('./errors'); var wrapPromiseCallback = require('./utils/wrapPromiseCallback'); -function EventProcessor(sdk_key, config, error_reporter, request_client) { +function EventProcessor(sdk_key, config, error_reporter) { var ep = {}; - var makeRequest = request_client || request, - userFilter = UserFilter(config), + var userFilter = UserFilter(config), summarizer = EventSummarizer(config), queue = [], lastKnownPastTime = 0, @@ -151,39 +150,58 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { config.logger && config.logger.debug("Flushing %d events", worklist.length); - makeRequest({ - 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.headers['date']) { - var date = Date.parse(resp.headers['date']); - if (date) { - lastKnownPastTime = date; - } + 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.events_uri + '/bulk', + headers: { + 'Authorization': sdk_key, + 'User-Agent': config.user_agent + }, + json: true, + body: events, + timeout: config.timeout * 1000, + agent: config.proxy_agent + }).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); - error_reporter && error_reporter(err); + } + if (resp.statusCode > 204) { + var err = new errors.LDUnexpectedResponseError("Unexpected status code " + resp.statusCode + "; events may not have been processed", + resp.statusCode); + error_reporter && error_reporter(err); + if (resp.statusCode === 401) { 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"); - error_reporter && error_reporter(err1); - shutdown = true; - } - } else { - resolve(resp, body); + var err1 = new errors.LDInvalidSDKKeyError("Received 401 error, no further events will be posted since SDK key is invalid"); + error_reporter && error_reporter(err1); + shutdown = true; + } else if (resp.statusCode >= 500) { + retryOrReject(err); } - }).on('error', reject); - }.bind(this)), callback); + } else { + resolve(resp, body); + } + }).on('error', function(err) { + retryOrReject(err); + }); } ep.close = function() { diff --git a/index.js b/index.js index ad01f12..a58d2a3 100644 --- a/index.js +++ b/index.js @@ -35,7 +35,9 @@ global.setImmediate = global.setImmediate || process.nextTick.bind(process); function NullEventProcessor() { return { send_event: function() {}, - flush: function(callback) { callback(); }, + flush: function(callback) { + return wrapPromiseCallback(Promise.resolve(), callback); + }, close: function() {} } } diff --git a/test/event_processor-test.js b/test/event_processor-test.js index c4fca17..e9b1d88 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -406,4 +406,20 @@ describe('EventProcessor', function() { }); }); }); + + it('retries once after a 5xx error', function(done) { + ep = EventProcessor(sdkKey, defaultConfig); + var e = { kind: 'identify', creationDate: 1000, user: user }; + ep.send_event(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(); + }); + }); }); From 910ad4a168930aec4103c61d5d7839f408a9895f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 11 Apr 2018 17:42:05 -0700 Subject: [PATCH 16/38] move user key cache out of summarizer, misc cleanup --- event_processor.js | 124 +++++++++++++++++++--------------- event_summarizer.js | 21 +----- test/event_summarizer-test.js | 30 ++------ 3 files changed, 74 insertions(+), 101 deletions(-) diff --git a/event_processor.js b/event_processor.js index a25d26f..a30e8c2 100644 --- a/event_processor.js +++ b/event_processor.js @@ -1,3 +1,4 @@ +var LRUCache = require('lrucache'); var request = require('request'); var EventSummarizer = require('./event_summarizer'); var UserFilter = require('./user_filter'); @@ -10,6 +11,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { var makeRequest = request_client || request, userFilter = UserFilter(config), summarizer = EventSummarizer(config), + userKeysCache = LRUCache(config.user_keys_capacity || 1000), queue = [], lastKnownPastTime = 0, exceededCapacity = false, @@ -40,47 +42,53 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { } function make_output_event(event) { - if (event.kind === 'feature') { - debug = !!event.debug; - var out = { - kind: debug ? 'debug' : 'feature', - creationDate: event.creationDate, - key: event.key, - version: event.version, - value: event.value, - default: event.default, - prereqOf: event.prereqOf - }; - if (config.inline_users_in_events || debug) { - out.user = userFilter.filter_user(event.user); - } else { - out.userKey = event.user.key; - } - return out; - } else if (event.kind === 'identify') { - return { - kind: 'identify', - creationDate: event.creationDate, - user: userFilter.filter_user(event.user) - }; - } else if (event.kind === 'custom') { - var out = { - kind: 'custom', - creationDate: event.creationDate, - key: event.key, - data: event.data - }; - if (config.inline_users_in_events) { - out.user = userFilter.filter_user(event.user); - } else { - out.userKey = event.user.key; - } - return out; + switch (event.kind) { + case 'feature': + debug = !!event.debug; + var out = { + kind: debug ? 'debug' : 'feature', + creationDate: event.creationDate, + key: event.key, + version: event.version, + value: event.value, + default: event.default, + prereqOf: event.prereqOf + }; + if (config.inline_users_in_events || debug) { + out.user = userFilter.filter_user(event.user); + } else { + out.userKey = event.user.key; + } + return out; + case 'identify': + return { + kind: 'identify', + creationDate: event.creationDate, + user: userFilter.filter_user(event.user) + }; + case 'custom': + var out = { + kind: 'custom', + creationDate: event.creationDate, + key: event.key, + data: event.data + }; + if (config.inline_users_in_events) { + out.user = userFilter.filter_user(event.user); + } else { + out.userKey = event.user.key; + } + return out; + default: + return event; } - return event; } ep.send_event = function(event) { + var addIndexEvent = false, + addFullEvent = false, + addDebugEvent = false; + if (shutdown) { return; } @@ -91,35 +99,36 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { // 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. - var willAddFullEvent = false, - debugEvent = null; if (event.kind === 'feature') { - willAddFullEvent = event.trackEvents; - if (should_debug_event(event)) { - debugEvent = Object.assign({}, event, { debug: true }); - } + addFullEvent = event.trackEvents; + addDebugEvent = should_debug_event(event); } else { - willAddFullEvent = true; + 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 (!(willAddFullEvent && config.inline_users_in_events)) { - if (event.user && !summarizer.notice_user(event.user)) { + if (!addFullEvent || !config.inline_users_in_events) { + if (event.user && !userKeysCache.get(event.user.key)) { + userKeysCache.set(event.user.key, true); if (event.kind != 'identify') { - enqueue({ - kind: 'index', - creationDate: event.creationDate, - user: userFilter.filter_user(event.user) - }); + addIndexEvent = true; } } } - if (willAddFullEvent) { + if (addIndexEvent) { + enqueue({ + kind: 'index', + creationDate: event.creationDate, + user: userFilter.filter_user(event.user) + }); + } + if (addFullEvent) { enqueue(make_output_event(event)); } - if (debugEvent) { + if (addDebugEvent) { + var debugEvent = Object.assign({}, event, { debug: true }); enqueue(make_output_event(debugEvent)); } } @@ -191,9 +200,12 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { clearInterval(flushUsersTimer); } - flushTimer = setInterval(ep.flush.bind(ep), config.flush_interval * 1000); - flushUsersTimer = setInterval(summarizer.reset_users.bind(summarizer), - config.user_keys_flush_interval * 1000); + flushTimer = setInterval(function() { + ep.flush(); + }, config.flush_interval * 1000); + flushUsersTimer = setInterval(function() { + userKeysCache.removeAll(); + }, config.user_keys_flush_interval * 1000); return ep; } diff --git a/event_summarizer.js b/event_summarizer.js index 38b46c1..e3b1766 100644 --- a/event_summarizer.js +++ b/event_summarizer.js @@ -1,28 +1,11 @@ -var LRUCache = require('lrucache'); function EventSummarizer(config) { var es = {}; - var users = LRUCache(config.user_keys_capacity), - startDate = 0, + var startDate = 0, endDate = 0, counters = {}; - - es.notice_user = function(user) { - if (!user || !user.key) { - return false; - } - if (users.get(user.key)) { - return true; - } - users.set(user.key, true); - return false; - } - - es.reset_users = function() { - users.removeAll(); - } - + es.summarize_event = function(event) { if (event.kind === 'feature') { var counterKey = event.key + ':' + (event.variation || '') + (event.version || ''); diff --git a/test/event_summarizer-test.js b/test/event_summarizer-test.js index 9ea3089..35a3c13 100644 --- a/test/event_summarizer-test.js +++ b/test/event_summarizer-test.js @@ -2,46 +2,24 @@ var EventSummarizer = require('../event_summarizer'); describe('EventSummarizer', function() { - var defaultConfig = { user_keys_capacity: 100 }; var user = { key: 'key1' }; - it('returns false from notice_user for never-seen user', function() { - var es = EventSummarizer(defaultConfig); - expect(es.notice_user(user)).toEqual(false); - }); - - it('returns true from notice_user for already-seen user', function() { - var es = EventSummarizer(defaultConfig); - es.notice_user({ key: 'key1' }); - expect(es.notice_user({ key: 'key1' })).toEqual(true); - }); - - it('discards oldest user if capacity is exceeded', function() { - var es = EventSummarizer({ user_keys_capacity: 2 }); - es.notice_user({ key: 'key1' }); - es.notice_user({ key: 'key2' }); - es.notice_user({ key: 'key3' }); - expect(es.notice_user({ key: 'key3' })).toEqual(true); - expect(es.notice_user({ key: 'key2' })).toEqual(true); - expect(es.notice_user({ key: 'key1' })).toEqual(false); - }); - it('does nothing for identify event', function() { - var es = EventSummarizer(defaultConfig); + var es = EventSummarizer(); var snapshot = es.get_summary(); es.summarize_event({ kind: 'identify', creationDate: 1000, user: user }); expect(es.get_summary()).toEqual(snapshot); }); it('does nothing for custom event', function() { - var es = EventSummarizer(defaultConfig); + var es = EventSummarizer(); var snapshot = es.get_summary(); es.summarize_event({ kind: 'custom', creationDate: 1000, key: 'eventkey', user: user }); expect(es.get_summary()).toEqual(snapshot); }); it('sets start and end dates for feature events', function() { - var es = EventSummarizer(defaultConfig); + 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 }; @@ -55,7 +33,7 @@ describe('EventSummarizer', function() { }); it('increments counters for feature events', function() { - var es = EventSummarizer(defaultConfig); + 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, From f3111227f3d79454e11e04136ac3028a14e6370c Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 12 Apr 2018 15:15:43 -0700 Subject: [PATCH 17/38] change UserFilter options and method to camelcase --- event_processor.js | 8 +++--- messages.js | 4 +++ test/event_processor-test.js | 8 +++--- test/user_filter_test.js | 47 ++++++++++++++++++++++++++---------- user_filter.js | 17 ++++++++++--- 5 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 messages.js diff --git a/event_processor.js b/event_processor.js index a30e8c2..f361cc2 100644 --- a/event_processor.js +++ b/event_processor.js @@ -55,7 +55,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { prereqOf: event.prereqOf }; if (config.inline_users_in_events || debug) { - out.user = userFilter.filter_user(event.user); + out.user = userFilter.filterUser(event.user); } else { out.userKey = event.user.key; } @@ -64,7 +64,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { return { kind: 'identify', creationDate: event.creationDate, - user: userFilter.filter_user(event.user) + user: userFilter.filterUser(event.user) }; case 'custom': var out = { @@ -74,7 +74,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { data: event.data }; if (config.inline_users_in_events) { - out.user = userFilter.filter_user(event.user); + out.user = userFilter.filterUser(event.user); } else { out.userKey = event.user.key; } @@ -121,7 +121,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { enqueue({ kind: 'index', creationDate: event.creationDate, - user: userFilter.filter_user(event.user) + user: userFilter.filterUser(event.user) }); } if (addFullEvent) { diff --git a/messages.js b/messages.js new file mode 100644 index 0000000..867ba63 --- /dev/null +++ b/messages.js @@ -0,0 +1,4 @@ + +exports.deprecated = function(oldName, newName) { + return '[LaunchDarkly] "' + oldName + '" is deprecated, please use "' + newName + '"'; +} diff --git a/test/event_processor-test.js b/test/event_processor-test.js index c4fca17..c5f0248 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -99,7 +99,7 @@ describe('EventProcessor', function() { }); it('filters user in identify event', function(done) { - var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true }); ep = EventProcessor(sdkKey, config); var e = { kind: 'identify', creationDate: 1000, user: user }; ep.send_event(e); @@ -130,7 +130,7 @@ describe('EventProcessor', function() { }); it('filters user in index event', function(done) { - var config = Object.assign({}, defaultConfig, { all_attributes_private: true }); + 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 }; @@ -161,7 +161,7 @@ describe('EventProcessor', function() { }); it('filters user in feature event', function(done) { - var config = Object.assign({}, defaultConfig, { all_attributes_private: true, + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true, inline_users_in_events: true }); ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', @@ -354,7 +354,7 @@ describe('EventProcessor', function() { }); it('filters user in custom event', function(done) { - var config = Object.assign({}, defaultConfig, { all_attributes_private: true, + var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true, inline_users_in_events: true }); ep = EventProcessor(sdkKey, config); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', diff --git a/test/user_filter_test.js b/test/user_filter_test.js index ad2f070..d6f1a89 100644 --- a/test/user_filter_test.js +++ b/test/user_filter_test.js @@ -2,6 +2,7 @@ var assert = require('assert'); var UserFilter = require('../user_filter'); describe('user_filter', function() { + var warnSpy; // users to serialize var user = { @@ -62,38 +63,58 @@ describe('user_filter', function() { 'privateAttrs': [ 'bizzle', 'dizzle' ] }; + beforeEach(function() { + warnSpy = jest.spyOn(console, 'warn').mockImplementation(function() {}); + }); + + afterEach(function() { + warnSpy.mockRestore(); + }); + it('includes all user attributes by default', function() { var uf = UserFilter({}); - assert.deepEqual(uf.filter_user(user), user); + 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), user_with_all_attrs_hidden); + }); + + it('allows all_attributes_private as deprecated synonym for allAttributesPrivate', () => { + const uf = UserFilter({ all_attributes_private: true }); + expect(uf.filterUser(user)).toEqual(userWithAllAttrsHidden); + expect(warnSpy).toHaveBeenCalled(); }); - it('hides all except key if all_attrs_private is true', function() { - var uf = UserFilter({ all_attributes_private: true}); - assert.deepEqual(uf.filter_user(user), user_with_all_attrs_hidden); + it('hides some attributes if privateAttributeNames is set', function() { + var uf = UserFilter({ privateAttributeNames: [ 'firstName', 'bizzle' ]}); + assert.deepEqual(uf.filterUser(user), user_with_some_attrs_hidden); }); - it('hides some attributes if private_attr_names is set', function() { - var uf = UserFilter({ private_attribute_names: [ 'firstName', 'bizzle' ]}); - assert.deepEqual(uf.filter_user(user), user_with_some_attrs_hidden); + it('allows private_attribute_names as deprecated synonym for privateAttributeNames', () => { + const uf = UserFilter({ private_attribute_names: [ 'firstName', 'bizzle' ]}); + expect(uf.filterUser(user)).toEqual(userWithSomeAttrsHidden); + expect(warnSpy).toHaveBeenCalled(); }); it('hides attributes specified in per-user privateAttrs', function() { var uf = UserFilter({}); - assert.deepEqual(uf.filter_user(user_specifying_own_private_attr), user_with_own_specified_attr_hidden); + assert.deepEqual(uf.filterUser(user_specifying_own_private_attr), user_with_own_specified_attr_hidden); }); it('looks at both per-user privateAttrs and global config', function() { - var uf = UserFilter({ private_attribute_names: [ 'firstName', 'bizzle' ]}); - assert.deepEqual(uf.filter_user(user_specifying_own_private_attr), user_with_all_attrs_hidden); + var uf = UserFilter({ privateAttributeNames: [ 'firstName', 'bizzle' ]}); + assert.deepEqual(uf.filterUser(user_specifying_own_private_attr), user_with_all_attrs_hidden); }); it('strips unknown top-level attributes', function() { var uf = UserFilter({}); - assert.deepEqual(uf.filter_user(user_with_unknown_top_level_attrs), user); + assert.deepEqual(uf.filterUser(user_with_unknown_top_level_attrs), user); }); it('leaves the "anonymous" attribute as is', function() { - var uf = UserFilter({ all_attributes_private: true}); - assert.deepEqual(uf.filter_user(anon_user), anon_user_with_all_attrs_hidden); + var uf = UserFilter({ allAttributesPrivate: true}); + assert.deepEqual(uf.filterUser(anon_user), anon_user_with_all_attrs_hidden); }); }); diff --git a/user_filter.js b/user_filter.js index 43a826e..0b1a3f0 100644 --- a/user_filter.js +++ b/user_filter.js @@ -1,3 +1,5 @@ +var messages = require('./messages'); + /** * The UserFilter object transforms user objects into objects suitable to be sent as JSON to * the server, hiding any private user attributes. @@ -6,13 +8,22 @@ **/ function UserFilter(config) { var filter = {}; - var allAttributesPrivate = config.all_attributes_private; - var privateAttributeNames = config.private_attribute_names || []; + const allAttributesPrivate = + config.allAttributesPrivate !== undefined ? config.allAttributesPrivate : config.all_attributes_private; + const privateAttributeNames = + (config.privateAttributeNames !== undefined ? config.privateAttributeNames : config.private_attribute_names) || []; 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 }; - filter.filter_user = function(user) { + if (config.all_attributes_private !== undefined) { + console && console.warn && console.warn(messages.deprecated('all_attributes_private', 'allAttributesPrivate')); + } + if (config.private_attribute_names !== undefined) { + console && console.warn && console.warn(messages.deprecated('private_attribute_names', 'privateAttributeNames')); + } + + filter.filterUser = function(user) { var allPrivateAttrs = {}; var userPrivateAttrs = user.privateAttributeNames || []; From b37b65cc048d94f7cf3c32a2f69c9eb1f6b5b780 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 12 Apr 2018 15:55:15 -0700 Subject: [PATCH 18/38] change internally-used symbols to camelcase --- evaluate_flag.js | 78 +++++++++++----------- event_processor.js | 18 +++--- event_summarizer.js | 6 +- index.js | 118 +++++++++++++++++----------------- operators.js | 4 +- polling.js | 6 +- requestor.js | 20 +++--- test/evaluate_flag-test.js | 14 ++-- test/event_processor-test.js | 46 ++++++------- test/event_summarizer-test.js | 32 ++++----- 10 files changed, 170 insertions(+), 172 deletions(-) diff --git a/evaluate_flag.js b/evaluate_flag.js index 20abd37..05e1360 100644 --- a/evaluate_flag.js +++ b/evaluate_flag.js @@ -22,11 +22,11 @@ function evaluate(flag, user, featureStore, cb) { if (!flag.on) { // Return the off variation if defined and valid - cb(null, flag.offVariation, get_variation(flag, flag.offVariation), null); + cb(null, flag.offVariation, getVariation(flag, flag.offVariation), null); return; } - eval_internal(flag, user, featureStore, [], function(err, variation, value, events) { + evalInternal(flag, user, featureStore, [], function(err, variation, value, events) { if (err) { cb(err, variation, value, events); return; @@ -34,7 +34,7 @@ function evaluate(flag, user, featureStore, cb) { if (variation === null) { // Return the off variation if defined and valid - cb(null, flag.offVariation, get_variation(flag, flag.offVariation), events); + cb(null, flag.offVariation, getVariation(flag, flag.offVariation), events); } else { cb(err, variation, value, events); } @@ -42,7 +42,7 @@ function evaluate(flag, user, featureStore, cb) { 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, @@ -54,10 +54,10 @@ function eval_internal(flag, user, featureStore, events, cb) { callback(new Error("Unsatisfied prerequisite"), null); return; } - eval_internal(f, user, featureStore, events, function(err, variation, 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 - events.push(create_flag_event(f.key, f, user, variation, value, null, flag.key)); + 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 { @@ -100,7 +100,7 @@ function evalRules(flag, user, featureStore, cb) { for (j = 0; j < target.values.length; j++) { if (user.key === target.values[j]) { - value = get_variation(flag, target.variation); + value = getVariation(flag, target.variation); cb(value === null ? new Error("Undefined variation for flag " + flag.key) : null, target.variation, value); return; @@ -110,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) { callback(matched ? rule : null, null); }); }, @@ -119,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); + variation = variationForUser(rule, user, flag); } else { // no rule matched; check the fallthrough - variation = variation_for_user(flag.fallthrough, user, flag); + variation = variationForUser(flag.fallthrough, user, flag); } cb(variation === null ? new Error("Undefined variation for flag " + flag.key) : null, - variation, get_variation(flag, variation)); + variation, getVariation(flag, variation)); } ); } -function rule_match_user(r, user, featureStore, cb) { +function ruleMatchUser(r, user, featureStore, cb) { var i; if (!r.clauses) { @@ -140,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 callback(matched ? null : clause, null); }); @@ -151,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 { @@ -166,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; @@ -190,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; @@ -209,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; } } @@ -217,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; } } @@ -230,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 { @@ -243,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++) { @@ -257,7 +257,7 @@ 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) { +function getVariation(flag, index) { if (index === null || index === undefined || index >= flag.variations.length) { return null; } else { @@ -267,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; @@ -280,7 +280,7 @@ function variation_for_user(r, user, flag) { // 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; @@ -295,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]; } @@ -306,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; @@ -327,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; } @@ -337,7 +337,7 @@ function bucketable_string_value(value) { return null; } -function create_flag_event(key, flag, user, variation, value, default_val, prereqOf) { +function createFlagEvent(key, flag, user, variation, value, default_val, prereqOf) { return { "kind": "feature", "key": key, @@ -353,4 +353,4 @@ function create_flag_event(key, flag, user, variation, value, default_val, prere }; } -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 index f361cc2..28043a8 100644 --- a/event_processor.js +++ b/event_processor.js @@ -31,7 +31,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { } } - function should_debug_event(event) { + function shouldDebugEvent(event) { if (event.debugEventsUntilDate) { if (event.debugEventsUntilDate > lastKnownPastTime && event.debugEventsUntilDate > new Date().getTime()) { @@ -41,7 +41,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { return false; } - function make_output_event(event) { + function makeOutputEvent(event) { switch (event.kind) { case 'feature': debug = !!event.debug; @@ -84,7 +84,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { } } - ep.send_event = function(event) { + ep.sendEvent = function(event) { var addIndexEvent = false, addFullEvent = false, addDebugEvent = false; @@ -95,13 +95,13 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { config.logger && config.logger.debug("Sending event", JSON.stringify(event)); // Always record the event in the summarizer. - summarizer.summarize_event(event); + 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 = should_debug_event(event); + addDebugEvent = shouldDebugEvent(event); } else { addFullEvent = true; } @@ -125,11 +125,11 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { }); } if (addFullEvent) { - enqueue(make_output_event(event)); + enqueue(makeOutputEvent(event)); } if (addDebugEvent) { var debugEvent = Object.assign({}, event, { debug: true }); - enqueue(make_output_event(debugEvent)); + enqueue(makeOutputEvent(debugEvent)); } } @@ -146,8 +146,8 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { worklist = queue; queue = []; - summary = summarizer.get_summary(); - summarizer.clear_summary(); + summary = summarizer.getSummary(); + summarizer.clearSummary(); if (Object.keys(summary.features).length) { summary.kind = 'summary'; worklist.push(summary); diff --git a/event_summarizer.js b/event_summarizer.js index e3b1766..37a27d6 100644 --- a/event_summarizer.js +++ b/event_summarizer.js @@ -6,7 +6,7 @@ function EventSummarizer(config) { endDate = 0, counters = {}; - es.summarize_event = function(event) { + es.summarizeEvent = function(event) { if (event.kind === 'feature') { var counterKey = event.key + ':' + (event.variation || '') + (event.version || ''); var counterVal = counters[counterKey]; @@ -30,7 +30,7 @@ function EventSummarizer(config) { } } - es.get_summary = function() { + es.getSummary = function() { var flagsOut = {}; for (var i in counters) { var c = counters[i]; @@ -60,7 +60,7 @@ function EventSummarizer(config) { }; } - es.clear_summary = function() { + es.clearSummary = function() { startDate = 0; endDate = 0; counters = {}; diff --git a/index.js b/index.js index ad01f12..47f1d90 100644 --- a/index.js +++ b/index.js @@ -34,21 +34,20 @@ global.setImmediate = global.setImmediate || process.nextTick.bind(process); function NullEventProcessor() { return { - send_event: function() {}, + sendEvent: function() {}, flush: function(callback) { callback(); }, close: function() {} } } -var new_client = function(sdk_key, config) { +var newClient = function(sdkKey, config) { var client = new EventEmitter(), - init_complete = false, + initComplete = false, queue = [], requestor, - update_processor, - event_processor, - event_queue_shutdown = false, - flush_timer; + updateProcessor, + eventProcessor, + flushTimer; config = Object.assign({}, config || {}); config.user_agent = 'NodeJSClient/' + package_json.version; @@ -66,7 +65,7 @@ var new_client = function(sdk_key, config) { config.user_keys_flush_interval = config.user_keys_flush_interval || 300; // Initialize global tunnel if proxy options are set if (config.proxy_host && config.proxy_port ) { - config.proxy_agent = create_proxy_agent(config); + config.proxy_agent = createProxyAgent(config); } config.logger = (config.logger || new winston.Logger({ @@ -80,7 +79,6 @@ var new_client = function(sdk_key, config) { ] }) ); - config.private_attr_names = config.private_attr_names || []; var featureStore = config.feature_store || InMemoryFeatureStore(); config.feature_store = FeatureStoreEventWrapper(featureStore, client); @@ -88,27 +86,27 @@ var new_client = function(sdk_key, config) { var maybeReportError = createErrorReporter(client, config.logger); if (config.offline || !config.send_events) { - event_processor = NullEventProcessor(); + eventProcessor = NullEventProcessor(); } else { - event_processor = EventProcessor(sdk_key, config, maybeReportError); + eventProcessor = EventProcessor(sdkKey, config, maybeReportError); } - if (!sdk_key && !config.offline) { + 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); + 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) { + updateProcessor.start(function(err) { if (err) { var error; if ((err.status && err.status === 401) || (err.code && err.code === 401)) { @@ -118,20 +116,20 @@ var new_client = function(sdk_key, config) { } maybeReportError(error); - } else if (!init_complete) { - init_complete = true; + } else if (!initComplete) { + initComplete = true; client.emit('ready'); } }); } else { process.nextTick(function() { - init_complete = true; + initComplete = true; client.emit('ready'); }); } client.initialized = function() { - return init_complete; + return initComplete; }; client.waitUntilReady = function() { @@ -140,53 +138,53 @@ 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()) { 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, null, user, null, 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, null, user, null, default_val, default_val); - return resolve(default_val); + sendFlagEvent(key, null, user, null, defaultVal, defaultVal); + return resolve(defaultVal); } else if (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) { + if (!initComplete) { config.feature_store.initialized(function(storeInited) { if (config.feature_store.initialized()) { 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, null, user, null, default_val, default_val); - return resolve(default_val); + sendFlagEvent(key, null, user, null, defaultVal, defaultVal); + return resolve(defaultVal); } }); } - variationInternal(key, user, default_val, resolve, reject); + variationInternal(key, user, defaultVal, resolve, reject); }.bind(this)), callback); } - function variationInternal(key, user, default_val, resolve, reject) { + function variationInternal(key, user, defaultVal, resolve, reject) { config.feature_store.get(dataKind.features, key, function(flag) { evaluate.evaluate(flag, user, config.feature_store, function(err, variation, value, events) { var i; @@ -200,30 +198,30 @@ 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++) { - event_processor.send_event(events[i]); + eventProcessor.sendEvent(events[i]); } } if (value === null) { config.logger.debug("Result value is null in variation"); - send_flag_event(key, flag, user, null, default_val, default_val); - return resolve(default_val); + sendFlagEvent(key, flag, user, null, defaultVal, defaultVal); + return resolve(defaultVal); } else { - send_flag_event(key, flag, user, variation, value, default_val); + 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) { return wrapPromiseCallback(new Promise(function(resolve, reject) { - sanitize_user(user); + sanitizeUser(user); var results = {}; if (this.is_offline() || !user) { @@ -232,11 +230,11 @@ var new_client = function(sdk_key, config) { } config.feature_store.all(dataKind.features, function(flags) { - async.forEachOf(flags, function(flag, key, iteratee_cb) { + 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; - iteratee_cb(null); + iterateeCb(null); }) }, function(err) { return err ? reject(err) : resolve(results); @@ -246,18 +244,18 @@ var new_client = function(sdk_key, config) { } client.secure_mode_hash = function(user) { - var hmac = crypto.createHmac('sha256', sdk_key); + var hmac = crypto.createHmac('sha256', sdkKey); hmac.update(user.key); return hmac.digest('hex'); } client.close = function() { - event_processor.close(); - if (update_processor) { - update_processor.close(); + eventProcessor.close(); + if (updateProcessor) { + updateProcessor.close(); } config.feature_store.close(); - clearInterval(flush_timer); + clearInterval(flushTimer); } client.is_offline = function() { @@ -265,7 +263,7 @@ var new_client = function(sdk_key, config) { } client.track = function(eventName, user, data) { - sanitize_user(user); + sanitizeUser(user); var event = {"key": eventName, "user": user, "kind": "custom", @@ -275,44 +273,44 @@ var new_client = function(sdk_key, config) { event.data = data; } - event_processor.send_event(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()}; - event_processor.send_event(event); + eventProcessor.sendEvent(event); }; client.flush = function(callback) { - return event_processor.flush(callback); + return eventProcessor.flush(callback); }; - function send_flag_event(key, flag, user, variation, value, default_val) { - var event = evaluate.create_flag_event(key, flag, user, variation, value, default_val); - event_processor.send_event(event); + function sendFlagEvent(key, flag, user, variation, value, defaultVal) { + var event = evaluate.createFlagEvent(key, flag, user, variation, value, defaultVal); + eventProcessor.sendEvent(event); } - function background_flush() { + function backgroundFlush() { client.flush().then(function() {}, function() {}); } - flush_timer = setInterval(background_flush, config.flush_interval * 1000); + flushTimer = setInterval(backgroundFlush, config.flush_interval * 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, @@ -335,7 +333,7 @@ function create_proxy_agent(config) { } -function sanitize_user(u) { +function sanitizeUser(u) { if (!u) { return; } 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/polling.js b/polling.js index b15ad8c..e261c3b 100644 --- a/polling.js +++ b/polling.js @@ -8,7 +8,7 @@ function PollingProcessor(config, requestor) { stopped = false; function poll(cb) { - var start_time, delta; + var startTime, delta; cb = cb || function(){}; @@ -16,10 +16,10 @@ 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; + elapsed = new Date().getTime() - startTime; sleepFor = Math.max(config.poll_interval * 1000 - elapsed, 0); config.logger.debug("Elapsed: %d ms, sleeping for %d ms", elapsed, sleepFor); if (err) { diff --git a/requestor.js b/requestor.js index 107d065..8f225b9 100644 --- a/requestor.js +++ b/requestor.js @@ -23,7 +23,7 @@ function Requestor(sdk_key, config) { var requestWithETagCaching = new ETagRequest(cacheConfig); function make_request(resource) { - var request_params = { + var requestParams = { method: "GET", url: config.base_uri + resource, headers: { @@ -34,12 +34,12 @@ function Requestor(sdk_key, config) { agent: config.proxy_agent } - 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,7 +59,7 @@ function Requestor(sdk_key, config) { }; } - function process_error_response(cb) { + function processErrorResponse(cb) { return function(err) { cb(err, null); } @@ -68,16 +68,16 @@ function Requestor(sdk_key, config) { requestor.request_object = function(kind, key, cb) { var req = make_request(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'); req( - process_response(cb), - process_error_response(cb) + processResponse(cb), + processErrorResponse(cb) ); } diff --git a/test/evaluate_flag-test.js b/test/evaluate_flag-test.js index 5eac8cb..3ebd9e4 100644 --- a/test/evaluate_flag-test.js +++ b/test/evaluate_flag-test.js @@ -443,18 +443,18 @@ describe('evaluate', function() { }); }); -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); }); @@ -466,8 +466,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); }); @@ -479,7 +479,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 index c5f0248..9a7abad 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -86,7 +86,7 @@ describe('EventProcessor', function() { it('queues identify event', function(done) { ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'identify', creationDate: 1000, user: user }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output).toEqual([{ @@ -102,7 +102,7 @@ describe('EventProcessor', function() { var config = Object.assign({}, defaultConfig, { allAttributesPrivate: true }); ep = EventProcessor(sdkKey, config); var e = { kind: 'identify', creationDate: 1000, user: user }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output).toEqual([{ @@ -118,7 +118,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(3); @@ -134,7 +134,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(3); @@ -150,7 +150,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(2); @@ -166,7 +166,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: true }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(2); @@ -181,7 +181,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, config); var e = { kind: 'feature', creationDate: 1000, user: user, key: 'flagkey', version: 11, variation: 1, value: 'value', trackEvents: false }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(2); @@ -196,7 +196,7 @@ describe('EventProcessor', function() { 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.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(3); @@ -212,7 +212,7 @@ describe('EventProcessor', function() { 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.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(4); @@ -231,14 +231,14 @@ describe('EventProcessor', function() { var serverTime = new Date().getTime() - 20000; // Send and flush an event we don't care about, just to set the last server time - ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); + ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); flush_and_get_request({ status: 200, headers: headers_with_date(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.send_event(e); + ep.sendEvent(e); // Should get a summary event only, not a full feature event flush_and_get_request(function(output) { @@ -257,14 +257,14 @@ describe('EventProcessor', function() { var serverTime = new Date().getTime() + 20000; // Send and flush an event we don't care about, just to set the last server time - ep.send_event({ kind: 'identify', user: { key: 'otherUser' } }); + ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); flush_and_get_request({ status: 200, headers: headers_with_date(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.send_event(e); + ep.sendEvent(e); // Should get a summary event only, not a full feature event flush_and_get_request(function(output) { @@ -282,8 +282,8 @@ describe('EventProcessor', function() { 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.send_event(e1); - ep.send_event(e2); + ep.sendEvent(e1); + ep.sendEvent(e2); flush_and_get_request(function(output) { expect(output.length).toEqual(4); @@ -301,8 +301,8 @@ describe('EventProcessor', function() { 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.send_event(e1); - ep.send_event(e2); + ep.sendEvent(e1); + ep.sendEvent(e2); flush_and_get_request(function(output) { expect(output.length).toEqual(2); @@ -329,7 +329,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(2); @@ -344,7 +344,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, config); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(1); @@ -359,7 +359,7 @@ describe('EventProcessor', function() { ep = EventProcessor(sdkKey, config); var e = { kind: 'custom', creationDate: 1000, user: user, key: 'eventkey', data: { thing: 'stuff' } }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(output) { expect(output.length).toEqual(1); @@ -379,7 +379,7 @@ describe('EventProcessor', function() { it('sends SDK key', function(done) { ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'identify', creationDate: 1000, user: user }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request(function(requestBody, requestHeaders) { expect(requestHeaders['authorization']).toEqual(sdkKey); @@ -390,12 +390,12 @@ describe('EventProcessor', function() { it('stops sending events after a 401 error', function(done) { ep = EventProcessor(sdkKey, defaultConfig); var e = { kind: 'identify', creationDate: 1000, user: user }; - ep.send_event(e); + ep.sendEvent(e); flush_and_get_request({ status: 401 }, function(body, headers, error) { expect(error.message).toContain("status code 401"); - ep.send_event(e); + ep.sendEvent(e); ep.flush().then( // no HTTP request should have been done here - Nock will error out if there was one diff --git a/test/event_summarizer-test.js b/test/event_summarizer-test.js index 35a3c13..65fad25 100644 --- a/test/event_summarizer-test.js +++ b/test/event_summarizer-test.js @@ -6,16 +6,16 @@ describe('EventSummarizer', function() { it('does nothing for identify event', function() { var es = EventSummarizer(); - var snapshot = es.get_summary(); - es.summarize_event({ kind: 'identify', creationDate: 1000, user: user }); - expect(es.get_summary()).toEqual(snapshot); + 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.get_summary(); - es.summarize_event({ kind: 'custom', creationDate: 1000, key: 'eventkey', user: user }); - expect(es.get_summary()).toEqual(snapshot); + 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() { @@ -23,10 +23,10 @@ describe('EventSummarizer', function() { 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.summarize_event(event1); - es.summarize_event(event2); - es.summarize_event(event3); - var data = es.get_summary(); + es.summarizeEvent(event1); + es.summarizeEvent(event2); + es.summarizeEvent(event3); + var data = es.getSummary(); expect(data.startDate).toEqual(1000); expect(data.endDate).toEqual(2000); @@ -44,12 +44,12 @@ describe('EventSummarizer', function() { variation: 1, value: 100, default: 111 }; var event5 = { kind: 'feature', creationDate: 1000, key: 'badkey', user: user, value: 333, default: 333 }; - es.summarize_event(event1); - es.summarize_event(event2); - es.summarize_event(event3); - es.summarize_event(event4); - es.summarize_event(event5); - var data = es.get_summary(); + 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 = { From 9f4ca02d9ae1f493ad0048e2ba32fab0324c7abd Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 12 Apr 2018 17:10:36 -0700 Subject: [PATCH 19/38] enforce camelcase everywhere else, add deprecation warnings, refactor config --- configuration.js | 91 +++++++++++++++++++ event_processor.js | 12 +-- index.d.ts | 30 +++--- index.js | 89 ++++++++---------- messages.js | 2 +- polling.js | 5 +- redis_feature_store.js | 54 +++++------ requestor.js | 6 +- streaming.js | 8 +- test/LDClient-test.js | 55 +++++++++++ test/configuration-test.js | 64 +++++++++++++ test/event_processor-test.js | 8 +- ...ser_filter_test.js => user_filter-test.js} | 48 +++------- user_filter.js | 13 +-- 14 files changed, 324 insertions(+), 161 deletions(-) create mode 100644 configuration.js create mode 100644 test/configuration-test.js rename test/{user_filter_test.js => user_filter-test.js} (57%) diff --git a/configuration.js b/configuration.js new file mode 100644 index 0000000..69dcd5a --- /dev/null +++ b/configuration.js @@ -0,0 +1,91 @@ +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 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 = Object.assign({}, defaults, config); + + 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/event_processor.js b/event_processor.js index 28043a8..37fd82e 100644 --- a/event_processor.js +++ b/event_processor.js @@ -11,7 +11,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { var makeRequest = request_client || request, userFilter = UserFilter(config), summarizer = EventSummarizer(config), - userKeysCache = LRUCache(config.user_keys_capacity || 1000), + userKeysCache = LRUCache(config.userKeysCapacity || 1000), queue = [], lastKnownPastTime = 0, exceededCapacity = false, @@ -162,15 +162,15 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { makeRequest({ method: "POST", - url: config.events_uri + '/bulk', + url: config.eventsUri + '/bulk', headers: { 'Authorization': sdk_key, - 'User-Agent': config.user_agent + 'User-Agent': config.userAgent }, json: true, body: worklist, timeout: config.timeout * 1000, - agent: config.proxy_agent + agent: config.proxyAgent }).on('response', function(resp, body) { if (resp.headers['date']) { var date = Date.parse(resp.headers['date']); @@ -202,10 +202,10 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { flushTimer = setInterval(function() { ep.flush(); - }, config.flush_interval * 1000); + }, config.flushInterval * 1000); flushUsersTimer = setInterval(function() { userKeysCache.removeAll(); - }, config.user_keys_flush_interval * 1000); + }, config.userKeysFlushInterval * 1000); return ep; } 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 47f1d90..1d0bc39 100644 --- a/index.js +++ b/index.js @@ -1,18 +1,17 @@ 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 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'); @@ -49,43 +48,18 @@ var newClient = function(sdkKey, config) { eventProcessor, flushTimer; - 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; - config.user_keys_capacity = config.user_keys_capacity || 1000; - config.user_keys_flush_interval = config.user_keys_flush_interval || 300; + config = configuration.validate(config); + // Initialize global tunnel if proxy options are set - if (config.proxy_host && config.proxy_port ) { - config.proxy_agent = createProxyAgent(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 : ''); - } - })), - ] - }) - ); - var featureStore = config.feature_store || InMemoryFeatureStore(); - config.feature_store = FeatureStoreEventWrapper(featureStore, client); + config.featureStore = FeatureStoreEventWrapper(config.featureStore, client); var maybeReportError = createErrorReporter(client, config.logger); - if (config.offline || !config.send_events) { + if (config.offline || !config.sendEvents) { eventProcessor = NullEventProcessor(); } else { eventProcessor = EventProcessor(sdkKey, config, maybeReportError); @@ -95,7 +69,7 @@ var newClient = function(sdkKey, config) { throw new Error("You must configure the client with an SDK key"); } - if (!config.use_ldd && !config.offline) { + if (!config.useLdd && !config.offline) { requestor = Requestor(sdkKey, config); if (config.stream) { @@ -167,8 +141,8 @@ var newClient = function(sdkKey, config) { } if (!initComplete) { - config.feature_store.initialized(function(storeInited) { - if (config.feature_store.initialized()) { + config.featureStore.initialized(function(storeInited) { + if (config.featureStore.initialized()) { 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, defaultVal, resolve, reject); } else { @@ -185,8 +159,8 @@ var newClient = function(sdkKey, config) { } function variationInternal(key, user, defaultVal, resolve, reject) { - config.feature_store.get(dataKind.features, key, function(flag) { - evaluate.evaluate(flag, user, config.feature_store, function(err, variation, value, events) { + config.featureStore.get(dataKind.features, key, function(flag) { + evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) { var i; var version = flag ? flag.version : null; @@ -219,7 +193,7 @@ var newClient = function(sdkKey, config) { 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) { sanitizeUser(user); var results = {}; @@ -229,10 +203,10 @@ var newClient = function(sdkKey, config) { return resolve({}); } - config.feature_store.all(dataKind.features, function(flags) { + 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) { + evaluate.evaluate(flag, user, config.featureStore, function(err, result, events) { results[key] = result; iterateeCb(null); }) @@ -243,7 +217,7 @@ var newClient = function(sdkKey, config) { }.bind(this)), callback); } - client.secure_mode_hash = function(user) { + client.secureModeHash = function(user) { var hmac = crypto.createHmac('sha256', sdkKey); hmac.update(user.key); return hmac.digest('hex'); @@ -254,11 +228,11 @@ var newClient = function(sdkKey, config) { if (updateProcessor) { updateProcessor.close(); } - config.feature_store.close(); + config.featureStore.close(); clearInterval(flushTimer); } - client.is_offline = function() { + client.isOffline = function() { return config.offline; } @@ -298,7 +272,18 @@ var newClient = function(sdkKey, config) { client.flush().then(function() {}, function() {}); } - flushTimer = setInterval(backgroundFlush, config.flush_interval * 1000); + 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'); + + flushTimer = setInterval(backgroundFlush, config.flushInterval * 1000); return client; }; @@ -313,19 +298,19 @@ module.exports = { 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); diff --git a/messages.js b/messages.js index 867ba63..95ae5e9 100644 --- a/messages.js +++ b/messages.js @@ -1,4 +1,4 @@ exports.deprecated = function(oldName, newName) { - return '[LaunchDarkly] "' + oldName + '" is deprecated, please use "' + newName + '"'; + return '"' + oldName + '" is deprecated, please use "' + newName + '"'; } diff --git a/polling.js b/polling.js index e261c3b..75bba5f 100644 --- a/polling.js +++ b/polling.js @@ -3,8 +3,7 @@ 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) { @@ -20,7 +19,7 @@ function PollingProcessor(config, requestor) { config.logger.debug("Polling LaunchDarkly for feature flag updates"); requestor.request_all_data(function(err, resp) { elapsed = new Date().getTime() - startTime; - sleepFor = Math.max(config.poll_interval * 1000 - elapsed, 0); + 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 8f225b9..3722796 100644 --- a/requestor.js +++ b/requestor.js @@ -25,13 +25,13 @@ function Requestor(sdk_key, config) { function make_request(resource) { var requestParams = { method: "GET", - url: config.base_uri + resource, + url: config.baseUri + resource, headers: { 'Authorization': sdk_key, - 'User-Agent': config.user_agent + 'User-Agent': config.userAgent }, timeout: config.timeout * 1000, - agent: config.proxy_agent + agent: config.proxyAgent } return function(cb, errCb) { diff --git a/streaming.js b/streaming.js index 979c9f6..5f8ff93 100644 --- a/streaming.js +++ b/streaming.js @@ -5,7 +5,7 @@ var dataKind = require('./versioned_data_kind'); function StreamProcessor(sdk_key, 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': sdk_key,'User-Agent': config.userAgent} }); es.onerror = function(err) { diff --git a/test/LDClient-test.js b/test/LDClient-test.js index f4089be..ea3a84a 100644 --- a/test/LDClient-test.js +++ b/test/LDClient-test.js @@ -1,6 +1,15 @@ var LDClient = require('../index.js'); +var messages = require('../messages'); describe('LDClient', function() { + + var logger = {}; + + beforeEach(function() { + logger.info = jest.fn(); + logger.warn = jest.fn(); + }); + it('should trigger the ready event in offline mode', function() { var client = LDClient.init('sdk_key', {offline: true}); var callback = jest.fn(); @@ -10,9 +19,55 @@ describe('LDClient', function() { }); }); + 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(); + }); + }); }); }); diff --git a/test/configuration-test.js b/test/configuration-test.js new file mode 100644 index 0000000..b3e04fb --- /dev/null +++ b/test/configuration-test.js @@ -0,0 +1,64 @@ +var configuration = require('../configuration'); + +describe('configuration', function() { + function checkDefault(name, value) { + var config = configuration.validate({}); + expect(config[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/event_processor-test.js b/test/event_processor-test.js index 9a7abad..f7943f6 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -8,11 +8,11 @@ describe('EventProcessor', function() { var eventsUri = 'http://example.com'; var sdkKey = 'SDK_KEY'; var defaultConfig = { - events_uri: eventsUri, + eventsUri: eventsUri, capacity: 100, - flush_interval: 30, - user_keys_capacity: 1000, - user_keys_flush_interval: 300 + flushInterval: 30, + userKeysCapacity: 1000, + userKeysFlushInterval: 300 }; var user = { key: 'userKey', name: 'Red' }; var filteredUser = { key: 'userKey', privateAttrs: [ 'name' ] }; diff --git a/test/user_filter_test.js b/test/user_filter-test.js similarity index 57% rename from test/user_filter_test.js rename to test/user_filter-test.js index d6f1a89..4b5d41b 100644 --- a/test/user_filter_test.js +++ b/test/user_filter-test.js @@ -2,8 +2,6 @@ var assert = require('assert'); var UserFilter = require('../user_filter'); describe('user_filter', function() { - var warnSpy; - // users to serialize var user = { 'key': 'abc', @@ -11,14 +9,14 @@ describe('user_filter', function() { 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } }; - var user_specifying_own_private_attr = { + var userSpecifyingOwnPrivateAttr = { 'key': 'abc', 'firstName': 'Sue', 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' }, 'privateAttributeNames': [ 'dizzle', 'unused' ] }; - var user_with_unknown_top_level_attrs = { + var userWithUnknownTopLevelAttrs = { 'key': 'abc', 'firstName': 'Sue', 'species': 'human', @@ -26,20 +24,20 @@ describe('user_filter', function() { 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } }; - var anon_user = { + var anonUser = { 'key': 'abc', 'anonymous': true, 'custom': { 'bizzle': 'def', 'dizzle': 'ghi' } }; // expected results from serializing user - var user_with_all_attrs_hidden = { + var userWithAllAttrsHidden = { 'key': 'abc', 'custom': { }, 'privateAttrs': [ 'bizzle', 'dizzle', 'firstName' ] }; - var user_with_some_attrs_hidden = { + var userWithSomeAttrsHidden = { 'key': 'abc', 'custom': { 'dizzle': 'ghi' @@ -47,7 +45,7 @@ describe('user_filter', function() { 'privateAttrs': [ 'bizzle', 'firstName' ] }; - var user_with_own_specified_attr_hidden = { + var userWithOwnSpecifiedAttrHidden = { 'key': 'abc', 'firstName': 'Sue', 'custom': { @@ -56,21 +54,13 @@ describe('user_filter', function() { 'privateAttrs': [ 'dizzle' ] }; - var anon_user_with_all_attrs_hidden = { + var anonUserWithAllAttrsHidden = { 'key': 'abc', 'anonymous': true, 'custom': { }, 'privateAttrs': [ 'bizzle', 'dizzle' ] }; - beforeEach(function() { - warnSpy = jest.spyOn(console, 'warn').mockImplementation(function() {}); - }); - - afterEach(function() { - warnSpy.mockRestore(); - }); - it('includes all user attributes by default', function() { var uf = UserFilter({}); assert.deepEqual(uf.filterUser(user), user); @@ -78,43 +68,31 @@ describe('user_filter', function() { it('hides all except key if allAttributesPrivate is true', function() { var uf = UserFilter({ allAttributesPrivate: true}); - assert.deepEqual(uf.filterUser(user), user_with_all_attrs_hidden); - }); - - it('allows all_attributes_private as deprecated synonym for allAttributesPrivate', () => { - const uf = UserFilter({ all_attributes_private: true }); - expect(uf.filterUser(user)).toEqual(userWithAllAttrsHidden); - expect(warnSpy).toHaveBeenCalled(); + 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), user_with_some_attrs_hidden); - }); - - it('allows private_attribute_names as deprecated synonym for privateAttributeNames', () => { - const uf = UserFilter({ private_attribute_names: [ 'firstName', 'bizzle' ]}); - expect(uf.filterUser(user)).toEqual(userWithSomeAttrsHidden); - expect(warnSpy).toHaveBeenCalled(); + assert.deepEqual(uf.filterUser(user), userWithSomeAttrsHidden); }); it('hides attributes specified in per-user privateAttrs', function() { var uf = UserFilter({}); - assert.deepEqual(uf.filterUser(user_specifying_own_private_attr), user_with_own_specified_attr_hidden); + 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(user_specifying_own_private_attr), user_with_all_attrs_hidden); + assert.deepEqual(uf.filterUser(userSpecifyingOwnPrivateAttr), userWithAllAttrsHidden); }); it('strips unknown top-level attributes', function() { var uf = UserFilter({}); - assert.deepEqual(uf.filterUser(user_with_unknown_top_level_attrs), user); + assert.deepEqual(uf.filterUser(userWithUnknownTopLevelAttrs), user); }); it('leaves the "anonymous" attribute as is', function() { var uf = UserFilter({ allAttributesPrivate: true}); - assert.deepEqual(uf.filterUser(anon_user), anon_user_with_all_attrs_hidden); + assert.deepEqual(uf.filterUser(anonUser), anonUserWithAllAttrsHidden); }); }); diff --git a/user_filter.js b/user_filter.js index 0b1a3f0..ccd6b86 100644 --- a/user_filter.js +++ b/user_filter.js @@ -8,21 +8,12 @@ var messages = require('./messages'); **/ function UserFilter(config) { var filter = {}; - const allAttributesPrivate = - config.allAttributesPrivate !== undefined ? config.allAttributesPrivate : config.all_attributes_private; - const privateAttributeNames = - (config.privateAttributeNames !== undefined ? config.privateAttributeNames : config.private_attribute_names) || []; + 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 }; - if (config.all_attributes_private !== undefined) { - console && console.warn && console.warn(messages.deprecated('all_attributes_private', 'allAttributesPrivate')); - } - if (config.private_attribute_names !== undefined) { - console && console.warn && console.warn(messages.deprecated('private_attribute_names', 'privateAttributeNames')); - } - filter.filterUser = function(user) { var allPrivateAttrs = {}; var userPrivateAttrs = user.privateAttributeNames || []; From df9c2e5263b817bf9b4fcfab444ae72b64ce315f Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 12 Apr 2018 17:13:45 -0700 Subject: [PATCH 20/38] rm redundant default --- event_processor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event_processor.js b/event_processor.js index 37fd82e..681fb93 100644 --- a/event_processor.js +++ b/event_processor.js @@ -11,7 +11,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { var makeRequest = request_client || request, userFilter = UserFilter(config), summarizer = EventSummarizer(config), - userKeysCache = LRUCache(config.userKeysCapacity || 1000), + userKeysCache = LRUCache(config.userKeysCapacity), queue = [], lastKnownPastTime = 0, exceededCapacity = false, From 028ff1d12e52cb71b3b7795285f63d4c467a27f7 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 12 Apr 2018 17:20:42 -0700 Subject: [PATCH 21/38] don't need a guard on the logger, there will always be one --- event_processor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/event_processor.js b/event_processor.js index 681fb93..6853944 100644 --- a/event_processor.js +++ b/event_processor.js @@ -26,7 +26,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { } else { if (!exceededCapacity) { exceededCapacity = true; - config.logger && config.logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); + config.logger.warn("Exceeded event queue capacity. Increase capacity to avoid dropping events."); } } } @@ -92,7 +92,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { if (shutdown) { return; } - config.logger && config.logger.debug("Sending event", JSON.stringify(event)); + config.logger.debug("Sending event", JSON.stringify(event)); // Always record the event in the summarizer. summarizer.summarizeEvent(event); @@ -158,7 +158,7 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { return; } - config.logger && config.logger.debug("Flushing %d events", worklist.length); + config.logger.debug("Flushing %d events", worklist.length); makeRequest({ method: "POST", From 40aead44fde8917697aa4b13422dcc3dd8a47aca Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 12 Apr 2018 17:23:18 -0700 Subject: [PATCH 22/38] add a stub logger in tests --- test/event_processor-test.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/event_processor-test.js b/test/event_processor-test.js index f7943f6..72cbac9 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -12,7 +12,10 @@ describe('EventProcessor', function() { capacity: 100, flushInterval: 30, userKeysCapacity: 1000, - userKeysFlushInterval: 300 + userKeysFlushInterval: 300, + logger: { + debug: jest.fn() + } }; var user = { key: 'userKey', name: 'Red' }; var filteredUser = { key: 'userKey', privateAttrs: [ 'name' ] }; From 5f3c32d31ca39c8eea39f880b280a9fe8471cdbd Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 18 Apr 2018 14:46:28 -0700 Subject: [PATCH 23/38] fix merge --- test/evaluate_flag-test.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/evaluate_flag-test.js b/test/evaluate_flag-test.js index 5fecebd..83bc9b4 100644 --- a/test/evaluate_flag-test.js +++ b/test/evaluate_flag-test.js @@ -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,9 +492,9 @@ 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(); }); }); From c9a7cba280cfbf744e90517569b724afcdaa14ab Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Wed, 18 Apr 2018 14:49:39 -0700 Subject: [PATCH 24/38] add schema header to event payload --- event_processor.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/event_processor.js b/event_processor.js index a30e8c2..6b0a129 100644 --- a/event_processor.js +++ b/event_processor.js @@ -165,7 +165,8 @@ function EventProcessor(sdk_key, config, error_reporter, request_client) { url: config.events_uri + '/bulk', headers: { 'Authorization': sdk_key, - 'User-Agent': config.user_agent + 'User-Agent': config.user_agent, + 'X-LaunchDarkly-Event-Schema': '2' }, json: true, body: worklist, From 462b39716a47ed6458a923455358b3bfbedbdb2b Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Apr 2018 14:15:02 -0700 Subject: [PATCH 25/38] fix behavior of config defaults when a value is null --- configuration.js | 14 +++++++++++++- test/configuration-test.js | 12 ++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/configuration.js b/configuration.js index 69dcd5a..6750fc7 100644 --- a/configuration.js +++ b/configuration.js @@ -52,6 +52,18 @@ module.exports = (function() { }); } + 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(/\/+$/, ""); } @@ -75,7 +87,7 @@ module.exports = (function() { checkDeprecatedOptions(config); - config = Object.assign({}, defaults, config); + config = applyDefaults(config, defaults); config.baseUri = canonicalizeUri(config.baseUri); config.streamUri = canonicalizeUri(config.streamUri); diff --git a/test/configuration-test.js b/test/configuration-test.js index b3e04fb..9791aa6 100644 --- a/test/configuration-test.js +++ b/test/configuration-test.js @@ -2,8 +2,16 @@ var configuration = require('../configuration'); describe('configuration', function() { function checkDefault(name, value) { - var config = configuration.validate({}); - expect(config[name]).toEqual(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); From 00048d16478b2bc43844c12d046ecf0bbb78ba90 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Apr 2018 14:43:02 -0700 Subject: [PATCH 26/38] fix allFlags method --- index.js | 4 +-- test/LDClient-test.js | 64 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/index.js b/index.js index 38ead92..a6379ed 100644 --- a/index.js +++ b/index.js @@ -208,8 +208,8 @@ var newClient = function(sdkKey, config) { config.featureStore.all(dataKind.features, function(flags) { async.forEachOf(flags, function(flag, key, iterateeCb) { // At the moment, we don't send any events here - evaluate.evaluate(flag, user, config.featureStore, function(err, result, events) { - results[key] = result; + evaluate.evaluate(flag, user, config.featureStore, function(err, variation, value, events) { + results[key] = value; setImmediate(iterateeCb); }) }, function(err) { diff --git a/test/LDClient-test.js b/test/LDClient-test.js index f561a94..37d1deb 100644 --- a/test/LDClient-test.js +++ b/test/LDClient-test.js @@ -73,12 +73,60 @@ describe('LDClient', function() { }); }); + function createOnlineClientWithFlags(flagsMap) { + var store = InMemoryFeatureStore(); + var allData = {}; + var dummyUri = 'bad'; + allData[dataKind.features.namespace] = flagsMap; + store.init(allData); + return LDClient.init('secret', { + baseUri: dummyUri, + streamUri: dummyUri, + eventsUri: dummyUri, + featureStore: store + }); + } + + it('evaluates a flag with variation()', 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' }; + // Deliberately not waiting for ready event; the update processor is irrelevant for this test + client.variation(flag.key, user, 'c', function(err, result) { + expect(err).toBeNull(); + expect(result).toEqual('b'); + 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.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 = { @@ -88,15 +136,7 @@ describe('LDClient', function() { }; flags[key] = flag; } - allData[dataKind.features.namespace] = flags; - store.init(allData); - var client = LDClient.init('secret', { - baseUri: dummyUri, - streamUri: dummyUri, - eventsUri: dummyUri, - featureStore: store - }); - // Deliberately not waiting for ready event; the update processor is irrelevant for this test + var client = createOnlineClientWithFlags(flags); client.allFlags({key: 'user'}, function(err, result) { expect(err).toEqual(null); expect(Object.keys(result).length).toEqual(flagCount); From 2cfc5f2cf7ab08f68645e7a459fa99f6f9f936cc Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Apr 2018 14:43:12 -0700 Subject: [PATCH 27/38] fix store initialization check --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index a6379ed..8f4918e 100644 --- a/index.js +++ b/index.js @@ -144,7 +144,7 @@ var newClient = function(sdkKey, config) { if (!initComplete) { config.featureStore.initialized(function(storeInited) { - if (config.featureStore.initialized()) { + 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, defaultVal, resolve, reject); } else { From 47227ca15a948eb52c2697593f0066067512b1af Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Apr 2018 16:49:48 -0700 Subject: [PATCH 28/38] re-add redundant key property to identify event --- event_processor.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/event_processor.js b/event_processor.js index dc931e3..7c90289 100644 --- a/event_processor.js +++ b/event_processor.js @@ -56,13 +56,14 @@ function EventProcessor(sdkKey, config, errorReporter) { if (config.inlineUsersInEvents || debug) { out.user = userFilter.filterUser(event.user); } else { - out.userKey = event.user.key; + 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': @@ -75,7 +76,7 @@ function EventProcessor(sdkKey, config, errorReporter) { if (config.inlineUsersInEvents) { out.user = userFilter.filterUser(event.user); } else { - out.userKey = event.user.key; + out.userKey = event.user && event.user.key; } return out; default: From 29cba7f8d7e3073e199cbd38cf2fbd76a60b2ce2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 23 Apr 2018 17:27:14 -0700 Subject: [PATCH 29/38] fix unit tests --- test/event_processor-test.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/event_processor-test.js b/test/event_processor-test.js index 302dba4..e2a1219 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -96,6 +96,7 @@ describe('EventProcessor', function() { expect(output).toEqual([{ kind: 'identify', creationDate: 1000, + key: user.key, user: user }]); done(); @@ -112,6 +113,7 @@ describe('EventProcessor', function() { expect(output).toEqual([{ kind: 'identify', creationDate: 1000, + key: user.key, user: filteredUser }]); done(); From e7f201e945e27dd073b740d4e7700b2d85fe06eb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Tue, 24 Apr 2018 22:48:36 -0700 Subject: [PATCH 30/38] send as much of a feature event as possible even if user is invalid --- index.js | 27 +++++---- test/LDClient-test.js | 132 ++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 144 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 8f4918e..7f9b862 100644 --- a/index.js +++ b/index.js @@ -61,10 +61,14 @@ var newClient = function(sdkKey, config) { var maybeReportError = createErrorReporter(client, config.logger); - if (config.offline || !config.sendEvents) { - eventProcessor = NullEventProcessor(); + if (config.eventProcessor) { + eventProcessor = config.eventProcessor; } else { - eventProcessor = EventProcessor(sdkKey, config, maybeReportError); + if (config.offline || !config.sendEvents) { + eventProcessor = NullEventProcessor(); + } else { + eventProcessor = EventProcessor(sdkKey, config, maybeReportError); + } } if (!sdkKey && !config.offline) { @@ -131,14 +135,7 @@ var newClient = function(sdkKey, config) { return resolve(defaultVal); } - else if (!user) { - variationErr = new errors.LDClientError('No user specified. Returning default value.'); - maybeReportError(variationErr); - sendFlagEvent(key, null, user, null, defaultVal, defaultVal); - return resolve(defaultVal); - } - - 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"); } @@ -154,6 +151,7 @@ var newClient = function(sdkKey, config) { return resolve(defaultVal); } }); + return; } variationInternal(key, user, defaultVal, resolve, reject); @@ -162,6 +160,13 @@ var newClient = function(sdkKey, config) { 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; diff --git a/test/LDClient-test.js b/test/LDClient-test.js index 37d1deb..028e711 100644 --- a/test/LDClient-test.js +++ b/test/LDClient-test.js @@ -7,9 +7,25 @@ describe('LDClient', 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() {} + }; + beforeEach(function() { logger.info = jest.fn(); logger.warn = jest.fn(); + eventProcessor.events = []; }); it('should trigger the ready event in offline mode', function() { @@ -83,20 +99,22 @@ describe('LDClient', function() { baseUri: dummyUri, streamUri: dummyUri, eventsUri: dummyUri, - featureStore: store + featureStore: store, + eventProcessor: eventProcessor }); } it('evaluates a flag with variation()', function(done) { var flag = { - key: 'feature', + key: 'flagkey', version: 1, on: true, targets: [], fallthrough: { variation: 1 }, - variations: ['a', 'b'] + variations: ['a', 'b'], + trackEvents: true }; - var client = createOnlineClientWithFlags({ feature: flag }); + var client = createOnlineClientWithFlags({ flagkey: flag }); var user = { key: 'user' }; // Deliberately not waiting for ready event; the update processor is irrelevant for this test client.variation(flag.key, user, 'c', function(err, result) { @@ -106,6 +124,112 @@ describe('LDClient', function() { }); }); + 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.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.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.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.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', From 7e2e28b534b8bc0fc4aed26011aef6be0e240cd5 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Apr 2018 11:18:28 -0700 Subject: [PATCH 31/38] run two CI builds in parallel for different Node versions --- .circleci/config.yml | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1661560..89fa3f4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,11 +1,17 @@ version: 2 + +workflows: + version: 2 + test: + jobs: + - oldest-long-term-support-release + - current-release + jobs: - build: - docker: - - image: circleci/node:6 - - image: redis + node-template: &node-template steps: - checkout + - run: echo "Node version:" `node --version` - run: npm install - run: command: npm test @@ -15,3 +21,15 @@ jobs: path: reports/junit - store_artifacts: path: reports/junit + + oldest-long-term-support-release: + <<: *node-template + docker: + - image: circleci/node:6 + - image: redis + + current-release: + <<: *node-template + docker: + - image: circleci/node:latest + - image: redis From f75089854210571c7ae4ea0c0c4dee93cde13abb Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Apr 2018 11:19:34 -0700 Subject: [PATCH 32/38] fix template syntax --- .circleci/config.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 89fa3f4..9cc27c7 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,21 +7,21 @@ workflows: - oldest-long-term-support-release - current-release -jobs: - 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 +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: oldest-long-term-support-release: <<: *node-template docker: From d92f4242d4bdd0c2f8d1b16ae739f819ea052edf Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Apr 2018 17:08:13 -0700 Subject: [PATCH 33/38] add variation index to feature events and summary counters --- event_processor.js | 7 ++++++- event_summarizer.js | 4 ++++ test/event_processor-test.js | 5 +++-- test/event_summarizer-test.js | 6 +++--- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/event_processor.js b/event_processor.js index 7c90289..877e0fb 100644 --- a/event_processor.js +++ b/event_processor.js @@ -48,11 +48,16 @@ function EventProcessor(sdkKey, config, errorReporter) { kind: debug ? 'debug' : 'feature', creationDate: event.creationDate, key: event.key, - version: event.version, 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 { diff --git a/event_summarizer.js b/event_summarizer.js index 37a27d6..aab679d 100644 --- a/event_summarizer.js +++ b/event_summarizer.js @@ -17,6 +17,7 @@ function EventSummarizer(config) { count: 1, key: event.key, version: event.version, + variation: event.variation, value: event.value, default: event.default }; @@ -46,6 +47,9 @@ function EventSummarizer(config) { 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 { diff --git a/test/event_processor-test.js b/test/event_processor-test.js index e2a1219..e42b4ea 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -62,6 +62,7 @@ describe('EventProcessor', function() { 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) { @@ -320,11 +321,11 @@ describe('EventProcessor', function() { expect(se.features).toEqual({ flagkey1: { default: 'default1', - counters: [ { version: 11, value: 'value1', count: 1 } ] + counters: [ { version: 11, variation: 1, value: 'value1', count: 1 } ] }, flagkey2: { default: 'default2', - counters: [ { version: 22, value: 'value2', count: 1 } ] + counters: [ { version: 22, variation: 1, value: 'value2', count: 1 } ] } }); done(); diff --git a/test/event_summarizer-test.js b/test/event_summarizer-test.js index 65fad25..02520a3 100644 --- a/test/event_summarizer-test.js +++ b/test/event_summarizer-test.js @@ -56,13 +56,13 @@ describe('EventSummarizer', function() { key1: { default: 111, counters: [ - { value: 100, version: 11, count: 2 }, - { value: 200, version: 11, count: 1 } + { variation: 1, value: 100, version: 11, count: 2 }, + { variation: 2, value: 200, version: 11, count: 1 } ] }, key2: { default: 222, - counters: [ { value: 999, version: 22, count: 1 }] + counters: [ { variation: 1, value: 999, version: 22, count: 1 }] }, badkey: { default: 333, From 153b3ab6a18b6672ce576923b5cb85f1d473bf91 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Fri, 27 Apr 2018 17:19:45 -0700 Subject: [PATCH 34/38] allow a custom updateProcessor for testing --- index.js | 49 +++++++------ test/LDClient-test.js | 163 +++++++++++++++++++++++------------------- 2 files changed, 117 insertions(+), 95 deletions(-) diff --git a/index.js b/index.js index 7f9b862..aecda65 100644 --- a/index.js +++ b/index.js @@ -38,7 +38,15 @@ function NullEventProcessor() { return wrapPromiseCallback(Promise.resolve(), callback); }, close: function() {} - } + }; +} + +function NullUpdateProcessor() { + return { + start: function(callback) { + setImmediate(callback, null); + } + }; } var newClient = function(sdkKey, config) { @@ -75,7 +83,11 @@ var newClient = function(sdkKey, config) { throw new Error("You must configure the client with an SDK key"); } - if (!config.useLdd && !config.offline) { + if (config.updateProcessor) { + updateProcessor = config.updateProcessor; + } else if (config.useLdd || config.offline) { + updateProcessor = NullUpdateProcessor(); + } else { requestor = Requestor(sdkKey, config); if (config.stream) { @@ -86,28 +98,23 @@ var newClient = function(sdkKey, config) { config.logger.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support"); updateProcessor = PollingProcessor(config, requestor); } - 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'); + } + 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; } - }); - } else { - process.nextTick(function() { + + maybeReportError(error); + } else if (!initComplete) { initComplete = true; client.emit('ready'); - }); - } - + } + }); + client.initialized = function() { return initComplete; }; diff --git a/test/LDClient-test.js b/test/LDClient-test.js index 028e711..56f3527 100644 --- a/test/LDClient-test.js +++ b/test/LDClient-test.js @@ -22,18 +22,22 @@ describe('LDClient', function() { 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() { + 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(); }); }); @@ -96,11 +100,9 @@ describe('LDClient', function() { allData[dataKind.features.namespace] = flagsMap; store.init(allData); return LDClient.init('secret', { - baseUri: dummyUri, - streamUri: dummyUri, - eventsUri: dummyUri, featureStore: store, - eventProcessor: eventProcessor + eventProcessor: eventProcessor, + updateProcessor: updateProcessor }); } @@ -116,11 +118,12 @@ describe('LDClient', function() { }; var client = createOnlineClientWithFlags({ flagkey: flag }); var user = { key: 'user' }; - // Deliberately not waiting for ready event; the update processor is irrelevant for this test - client.variation(flag.key, user, 'c', function(err, result) { - expect(err).toBeNull(); - expect(result).toEqual('b'); - done(); + client.on('ready', function() { + client.variation(flag.key, user, 'c', function(err, result) { + expect(err).toBeNull(); + expect(result).toEqual('b'); + done(); + }); }); }); @@ -136,40 +139,44 @@ describe('LDClient', function() { }; var client = createOnlineClientWithFlags({ flagkey: flag }); var user = { key: 'user' }; - 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 + 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(); }); - done(); }); }); it('generates an event for an unknown feature', function(done) { var client = createOnlineClientWithFlags({}); var user = { key: 'user' }; - 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 + 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(); }); - done(); }); }); @@ -185,20 +192,22 @@ describe('LDClient', function() { }; var client = createOnlineClientWithFlags({ flagkey: flag }); var user = { name: 'Bob' }; - 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 + 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(); }); - done(); }); }); @@ -213,20 +222,22 @@ describe('LDClient', function() { trackEvents: true }; var client = createOnlineClientWithFlags({ flagkey: flag }); - 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 + 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(); }); - done(); }); }); @@ -241,10 +252,12 @@ describe('LDClient', function() { }; var client = createOnlineClientWithFlags({ feature: flag }); var user = { key: 'user' }; - client.allFlags(user, function(err, results) { - expect(err).toBeNull(); - expect(results).toEqual({feature: 'b'}); - done(); + client.on('ready', function() { + client.allFlags(user, function(err, results) { + expect(err).toBeNull(); + expect(results).toEqual({feature: 'b'}); + done(); + }); }); }); @@ -261,10 +274,12 @@ describe('LDClient', function() { flags[key] = flag; } var client = createOnlineClientWithFlags(flags); - client.allFlags({key: 'user'}, function(err, result) { - expect(err).toEqual(null); - expect(Object.keys(result).length).toEqual(flagCount); - done(); + client.on('ready', function() { + client.allFlags({key: 'user'}, function(err, result) { + expect(err).toEqual(null); + expect(Object.keys(result).length).toEqual(flagCount); + done(); + }); }); }); }); From a0a6fd21ecaaf28c34b37129dffb44097ffba930 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Mon, 30 Apr 2018 14:31:14 -0700 Subject: [PATCH 35/38] update event schema version --- event_processor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/event_processor.js b/event_processor.js index 877e0fb..ec98690 100644 --- a/event_processor.js +++ b/event_processor.js @@ -187,7 +187,7 @@ function EventProcessor(sdkKey, config, errorReporter) { headers: { 'Authorization': sdkKey, 'User-Agent': config.userAgent, - 'X-LaunchDarkly-Event-Schema': '2' + 'X-LaunchDarkly-Event-Schema': '3' }, json: true, body: events, From 71fb66fd11ca43124a42c405630d32b09772f5d2 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 3 May 2018 14:53:27 -0700 Subject: [PATCH 36/38] fix merge --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 1fff32b..59c22e0 100644 --- a/index.js +++ b/index.js @@ -120,7 +120,7 @@ var newClient = function(sdkKey, config) { }; client.waitUntilReady = function() { - if (init_complete){ + if (initComplete) { return Promise.resolve(); } From acb0df9e1b674b57bd25b4370974b172c6a34c1a Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 10 May 2018 15:39:41 -0700 Subject: [PATCH 37/38] fix transitive dependency on buggy version of "hoek" --- package-lock.json | 1486 ++++++++++++++++++++------------------------- package.json | 1 + 2 files changed, 674 insertions(+), 813 deletions(-) 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 d263028..3438c3b 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node": ">= 0.8.x" }, "devDependencies": { + "hoek": "4.2.1", "jest": "22.4.3", "jest-junit": "3.6.0", "nock": "9.2.3" From fa8e52b66858b6a3d58eb206fd492b89794715a1 Mon Sep 17 00:00:00 2001 From: Eli Bishop Date: Thu, 10 May 2018 16:00:33 -0700 Subject: [PATCH 38/38] move dependency constraint out of devDependencies --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3438c3b..d5618ba 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dependencies": { "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", @@ -39,7 +40,6 @@ "node": ">= 0.8.x" }, "devDependencies": { - "hoek": "4.2.1", "jest": "22.4.3", "jest-junit": "3.6.0", "nock": "9.2.3"