From 9eab3966ce75da1e371f8e1f346d3ba4f80e6acd Mon Sep 17 00:00:00 2001 From: mwksl Date: Thu, 2 Jan 2020 14:03:05 -0600 Subject: [PATCH 1/2] Add in memory dev data store as a configuration option. This allows users to pass in small features for evaluation locally without having to connect to Launch Darkly services. This also allows easy "pull and play" configurations --- configuration.js | 9 +++++--- in_memory_data_source.js | 43 ++++++++++++++++++++++++++++++++++++ index.js | 9 ++++++-- test/event_processor-test.js | 29 ++++++++++++++++++++++-- 4 files changed, 83 insertions(+), 7 deletions(-) create mode 100644 in_memory_data_source.js diff --git a/configuration.js b/configuration.js index e11b8f1..777aca6 100644 --- a/configuration.js +++ b/configuration.js @@ -1,3 +1,5 @@ +import InMemoryDataSource from "./in_memory_data_source"; + const winston = require('winston'); const InMemoryFeatureStore = require('./feature_store'); const messages = require('./messages'); @@ -21,7 +23,8 @@ module.exports = (function() { privateAttributeNames: [], userKeysCapacity: 1000, userKeysFlushInterval: 300, - featureStore: InMemoryFeatureStore() + featureStore: InMemoryFeatureStore(), + inMemoryDevFlags: false, }; }; @@ -132,7 +135,7 @@ module.exports = (function() { function validate(options) { let config = Object.assign({}, options || {}); - + config.userAgent = 'NodeJSClient/' + package_json.version; config.logger = (config.logger || new winston.Logger({ @@ -146,7 +149,7 @@ module.exports = (function() { ] }) ); - + checkDeprecatedOptions(config); const defaultConfig = defaults(); diff --git a/in_memory_data_source.js b/in_memory_data_source.js new file mode 100644 index 0000000..7c49f96 --- /dev/null +++ b/in_memory_data_source.js @@ -0,0 +1,43 @@ +/* + DevDataSource provides a way to pass features in to dev without connecting to LaunchDarkly's live service. + This would typically be used in a local development environment. +*/ + +export default function InMemoryDataSource(features) { + if (!features) { + return; + } + + const flags = {}; + Object.keys(features).forEach(key => { + flags[key] = { + key, + on: features[key], + }; + }); + const ld_features = { + flags, + segments: {}, + }; + + return (config) => { + let inited = false; + const featureStore = config.featureStore; + + const dev_ds = { + start: fn => { + featureStore.init(ld_features, () => { + inited = true; + }); + const cb = fn || (() => {}); + cb(); + }, + stop: () => {}, + initialized: () => inited, + close: () => { + dev_ds.stop(); + }, + }; + return dev_ds; + }; +} diff --git a/index.js b/index.js index f303234..e6d2e14 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const EventProcessor = require('./event_processor'); const PollingProcessor = require('./polling'); const StreamingProcessor = require('./streaming'); const FlagsStateBuilder = require('./flags_state'); +const InMemoryDataSource = require('./in_memory_data_source'); const configuration = require('./configuration'); const evaluate = require('./evaluate_flag'); const messages = require('./messages'); @@ -62,7 +63,7 @@ const newClient = function(sdkKey, originalConfig) { eventFactoryWithReasons; const config = configuration.validate(originalConfig); - + // Initialize global tunnel if proxy options are set if (config.proxyHost && config.proxyPort ) { config.proxyAgent = createProxyAgent(config); @@ -90,11 +91,15 @@ const newClient = function(sdkKey, originalConfig) { } const createDefaultUpdateProcessor = config => { + if (config.inMemoryDevFlags) { + config.logger.info('Creating in-memory flags for offline usage'); + return InMemoryDataSource(config.inMemoryDevFlags) + } if (config.useLdd || config.offline) { return NullUpdateProcessor(); } else { requestor = Requestor(sdkKey, config); - + if (config.stream) { config.logger.info('Initializing stream processor to receive feature flag updates'); return StreamingProcessor(sdkKey, config, requestor); diff --git a/test/event_processor-test.js b/test/event_processor-test.js index 2745301..cf3b004 100644 --- a/test/event_processor-test.js +++ b/test/event_processor-test.js @@ -16,6 +16,18 @@ describe('EventProcessor', () => { warn: jest.fn() } }; + const developmentConfig = { + eventsUri: eventsUri, + capacity: 100, + flushInterval: 30, + userKeysCapacity: 1000, + userKeysFlushInterval: 300, + inMemoryDevFlags: { 'development-feature': true }, + logger: { + debug: jest.fn(), + warn: jest.fn() + } + }; const user = { key: 'userKey', name: 'Red' }; const filteredUser = { key: 'userKey', privateAttrs: [ 'name' ] }; const numericUser = { key: 1, secondary: 2, ip: 3, country: 4, email: 5, firstName: 6, lastName: 7, @@ -212,6 +224,19 @@ describe('EventProcessor', () => { }); })); + it('processes offline events when defined', eventsServerTest(async s => { + const config = Object.assign({}, developmentConfig, { allAttributesPrivate: true }); + await withEventProcessor(config, s, async ep => { + const e = developmentConfig.inMemoryDevFlags['development-feature'] + ep.sendEvent(e); + await ep.flush(); + + const output = await getJsonRequest(s); + expect(output.length).toEqual(1); + expect(output[0]).toEqual(e) + }); + })); + it('stringifies user attributes in feature event', eventsServerTest(async s => { const config = Object.assign({}, defaultConfig, { inlineUsersInEvents: true }); await withEventProcessor(config, s, async ep => { @@ -297,7 +322,7 @@ describe('EventProcessor', () => { const serverTime = new Date().getTime() - 20000; s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(200, headersWithDate(serverTime))); - // Send and flush an event we don't care about, just to set the last server time + // Send and flush an event we don't care about, just to set the last server time ep.sendEvent({ kind: 'identify', user: { key: 'otherUser' } }); await ep.flush(); await s.nextRequest(); @@ -536,7 +561,7 @@ describe('EventProcessor', () => { it('swallows errors from failed background flush', eventsServerTest(async s => { // This test verifies that when a background flush fails, we don't emit an unhandled // promise rejection. Jest will fail the test if we do that. - + const config = Object.assign({}, defaultConfig, { flushInterval: 0.25 }); await withEventProcessor(config, s, async ep => { s.forMethodAndPath('post', '/bulk', TestHttpHandlers.respond(500)); From 1b132c50db1f7951f26db3840830eb43605a06a1 Mon Sep 17 00:00:00 2001 From: mwksl Date: Thu, 2 Jan 2020 14:14:42 -0600 Subject: [PATCH 2/2] Fix linting errors --- configuration.js | 2 -- in_memory_data_source.js | 4 +++- index.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/configuration.js b/configuration.js index 777aca6..d17d1bb 100644 --- a/configuration.js +++ b/configuration.js @@ -1,5 +1,3 @@ -import InMemoryDataSource from "./in_memory_data_source"; - const winston = require('winston'); const InMemoryFeatureStore = require('./feature_store'); const messages = require('./messages'); diff --git a/in_memory_data_source.js b/in_memory_data_source.js index 7c49f96..c7751c6 100644 --- a/in_memory_data_source.js +++ b/in_memory_data_source.js @@ -3,7 +3,7 @@ This would typically be used in a local development environment. */ -export default function InMemoryDataSource(features) { +function InMemoryDataSource(features) { if (!features) { return; } @@ -41,3 +41,5 @@ export default function InMemoryDataSource(features) { return dev_ds; }; } + +module.exports = InMemoryDataSource; diff --git a/index.js b/index.js index e6d2e14..f5a2ae4 100644 --- a/index.js +++ b/index.js @@ -93,7 +93,7 @@ const newClient = function(sdkKey, originalConfig) { const createDefaultUpdateProcessor = config => { if (config.inMemoryDevFlags) { config.logger.info('Creating in-memory flags for offline usage'); - return InMemoryDataSource(config.inMemoryDevFlags) + return InMemoryDataSource(config.inMemoryDevFlags); } if (config.useLdd || config.offline) { return NullUpdateProcessor();