launchdarkly-js-sdk-common 4.3.2 → 5.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.js CHANGED
@@ -7,13 +7,14 @@ const PersistentStorage = require('./PersistentStorage');
7
7
  const Stream = require('./Stream');
8
8
  const Requestor = require('./Requestor');
9
9
  const Identity = require('./Identity');
10
- const UserValidator = require('./UserValidator');
10
+ const AnonymousContextProcessor = require('./AnonymousContextProcessor');
11
11
  const configuration = require('./configuration');
12
12
  const diagnostics = require('./diagnosticEvents');
13
13
  const { commonBasicLogger } = require('./loggers');
14
14
  const utils = require('./utils');
15
15
  const errors = require('./errors');
16
16
  const messages = require('./messages');
17
+ const { checkContext } = require('./context');
17
18
  const { InspectorTypes, InspectorManager } = require('./InspectorManager');
18
19
 
19
20
  const changeEvent = 'change';
@@ -28,7 +29,7 @@ const internalChangeEvent = 'internal-change';
28
29
  //
29
30
  // For definitions of the API in the platform object, see stubPlatform.js in the test code.
30
31
 
31
- function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
32
+ function initialize(env, context, specifiedOptions, platform, extraOptionDefs) {
32
33
  const logger = createLogger();
33
34
  const emitter = EventEmitter(logger);
34
35
  const initializationStateTracker = InitializationStateTracker(emitter);
@@ -77,19 +78,19 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
77
78
  // The "stateProvider" object is used in the Electron SDK, to allow one client instance to take partial
78
79
  // control of another. If present, it has the following contract:
79
80
  // - getInitialState() returns the initial client state if it is already available. The state is an
80
- // object whose properties are "environment", "user", and "flags".
81
+ // object whose properties are "environment", "context", and "flags".
81
82
  // - on("init", listener) triggers an event when the initial client state becomes available, passing
82
83
  // the state object to the listener.
83
- // - on("update", listener) triggers an event when flag values change and/or the current user changes.
84
- // The parameter is an object that *may* contain "user" and/or "flags".
84
+ // - on("update", listener) triggers an event when flag values change and/or the current context changes.
85
+ // The parameter is an object that *may* contain "context" and/or "flags".
85
86
  // - enqueueEvent(event) accepts an analytics event object and returns true if the stateProvider will
86
87
  // be responsible for delivering it, or false if we still should deliver it ourselves.
87
88
  const stateProvider = options.stateProvider;
88
89
 
89
90
  const ident = Identity(null, onIdentifyChange);
90
- const userValidator = UserValidator(persistentStorage);
91
+ const anonymousContextProcessor = new AnonymousContextProcessor(persistentStorage);
91
92
  const persistentFlagStore = persistentStorage.isEnabled()
92
- ? new PersistentFlagStore(persistentStorage, environment, hash, ident, logger)
93
+ ? PersistentFlagStore(persistentStorage, environment, hash, ident, logger)
93
94
  : null;
94
95
 
95
96
  function createLogger() {
@@ -134,22 +135,22 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
134
135
 
135
136
  function enqueueEvent(event) {
136
137
  if (!environment) {
137
- // We're in paired mode and haven't been initialized with an environment or user yet
138
+ // We're in paired mode and haven't been initialized with an environment or context yet
138
139
  return;
139
140
  }
140
141
  if (stateProvider && stateProvider.enqueueEvent && stateProvider.enqueueEvent(event)) {
141
142
  return; // it'll be handled elsewhere
142
143
  }
143
- if (event.kind !== 'alias') {
144
- if (!event.user) {
145
- if (firstEvent) {
146
- logger.warn(messages.eventWithoutUser());
147
- firstEvent = false;
148
- }
149
- return;
144
+
145
+ if (!event.context) {
146
+ if (firstEvent) {
147
+ logger.warn(messages.eventWithoutContext());
148
+ firstEvent = false;
150
149
  }
151
- firstEvent = false;
150
+ return;
152
151
  }
152
+ firstEvent = false;
153
+
153
154
  if (shouldEnqueueEvent()) {
154
155
  logger.debug(messages.debugEnqueueingEvent(event.kind));
155
156
  events.enqueue(event);
@@ -158,13 +159,13 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
158
159
 
159
160
  function notifyInspectionFlagUsed(key, detail) {
160
161
  if (inspectorManager.hasListeners(InspectorTypes.flagUsed)) {
161
- inspectorManager.onFlagUsed(key, detail, ident.getUser());
162
+ inspectorManager.onFlagUsed(key, detail, ident.getContext());
162
163
  }
163
164
  }
164
165
 
165
166
  function notifyInspectionIdentityChanged() {
166
167
  if (inspectorManager.hasListeners(InspectorTypes.clientIdentityChanged)) {
167
- inspectorManager.onIdentityChanged(ident.getUser());
168
+ inspectorManager.onIdentityChanged(ident.getContext());
168
169
  }
169
170
  }
170
171
 
@@ -188,46 +189,39 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
188
189
  }
189
190
  }
190
191
 
191
- function onIdentifyChange(user, previousUser) {
192
+ function onIdentifyChange(user) {
192
193
  sendIdentifyEvent(user);
193
- if (!options.autoAliasingOptOut && previousUser && previousUser.anonymous && user && !user.anonymous) {
194
- alias(user, previousUser);
195
- }
196
194
  notifyInspectionIdentityChanged();
197
195
  }
198
196
 
199
- function sendIdentifyEvent(user) {
197
+ function sendIdentifyEvent(context) {
200
198
  if (stateProvider) {
201
199
  // In paired mode, the other client is responsible for sending identify events
202
200
  return;
203
201
  }
204
- if (user) {
202
+ if (context) {
205
203
  enqueueEvent({
206
204
  kind: 'identify',
207
- key: user.key,
208
- user: user,
205
+ context,
209
206
  creationDate: new Date().getTime(),
210
207
  });
211
208
  }
212
209
  }
213
210
 
214
211
  function sendFlagEvent(key, detail, defaultValue, includeReason) {
215
- const user = ident.getUser();
212
+ const context = ident.getContext();
216
213
  const now = new Date();
217
214
  const value = detail ? detail.value : null;
218
215
 
219
216
  const event = {
220
217
  kind: 'feature',
221
218
  key: key,
222
- user: user,
219
+ context,
223
220
  value: value,
224
221
  variation: detail ? detail.variationIndex : null,
225
222
  default: defaultValue,
226
223
  creationDate: now.getTime(),
227
224
  };
228
- if (user && user.anonymous) {
229
- event.contextKind = userContextKind(user);
230
- }
231
225
  const flag = flags[key];
232
226
  if (flag) {
233
227
  event.version = flag.flagVersion ? flag.flagVersion : flag.version;
@@ -241,26 +235,37 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
241
235
  enqueueEvent(event);
242
236
  }
243
237
 
244
- function identify(user, newHash, onDone) {
238
+ function verifyContext(context) {
239
+ // The context will already have been processed to have a string key, so we
240
+ // do not need to allow for legacy keys in the check.
241
+ if (checkContext(context, false)) {
242
+ return Promise.resolve(context);
243
+ } else {
244
+ return Promise.reject(new errors.LDInvalidUserError(messages.invalidContext()));
245
+ }
246
+ }
247
+
248
+ function identify(context, newHash, onDone) {
245
249
  if (closed) {
246
250
  return utils.wrapPromiseCallback(Promise.resolve({}), onDone);
247
251
  }
248
252
  if (stateProvider) {
249
- // We're being controlled by another client instance, so only that instance is allowed to change the user
253
+ // We're being controlled by another client instance, so only that instance is allowed to change the context
250
254
  logger.warn(messages.identifyDisabled());
251
255
  return utils.wrapPromiseCallback(Promise.resolve(utils.transformVersionedValuesToValues(flags)), onDone);
252
256
  }
253
257
  const clearFirst = useLocalStorage && persistentFlagStore ? persistentFlagStore.clearFlags() : Promise.resolve();
254
258
  return utils.wrapPromiseCallback(
255
259
  clearFirst
256
- .then(() => userValidator.validateUser(user))
257
- .then(realUser =>
260
+ .then(() => anonymousContextProcessor.processContext(context))
261
+ .then(verifyContext)
262
+ .then(validatedContext =>
258
263
  requestor
259
- .fetchFlagSettings(realUser, newHash)
264
+ .fetchFlagSettings(validatedContext, newHash)
260
265
  // the following then() is nested within this one so we can use realUser from the previous closure
261
266
  .then(requestedFlags => {
262
267
  const flagValueMap = utils.transformVersionedValuesToValues(requestedFlags);
263
- ident.setUser(realUser);
268
+ ident.setContext(validatedContext);
264
269
  hash = newHash;
265
270
  if (requestedFlags) {
266
271
  return replaceAllFlags(requestedFlags).then(() => flagValueMap);
@@ -283,8 +288,8 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
283
288
  );
284
289
  }
285
290
 
286
- function getUser() {
287
- return ident.getUser();
291
+ function getContext() {
292
+ return ident.getContext();
288
293
  }
289
294
 
290
295
  function flush(onDone) {
@@ -355,26 +360,6 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
355
360
  return user.anonymous ? 'anonymousUser' : 'user';
356
361
  }
357
362
 
358
- function alias(user, previousUser) {
359
- if (stateProvider) {
360
- // In paired mode, the other client is responsible for sending alias events
361
- return;
362
- }
363
-
364
- if (!user || !previousUser) {
365
- return;
366
- }
367
-
368
- enqueueEvent({
369
- kind: 'alias',
370
- key: user.key,
371
- contextKind: userContextKind(user),
372
- previousKey: previousUser.key,
373
- previousContextKind: userContextKind(previousUser),
374
- creationDate: new Date().getTime(),
375
- });
376
- }
377
-
378
363
  function track(key, data, metricValue) {
379
364
  if (typeof key !== 'string') {
380
365
  emitter.maybeReportError(new errors.LDInvalidEventKeyError(messages.unknownCustomEventKey(key)));
@@ -390,16 +375,16 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
390
375
  logger.warn(messages.unknownCustomEventKey(key));
391
376
  }
392
377
 
393
- const user = ident.getUser();
378
+ const context = ident.getContext();
394
379
  const e = {
395
380
  kind: 'custom',
396
381
  key: key,
397
- user: user,
382
+ context,
398
383
  url: platform.getCurrentUrl(),
399
384
  creationDate: new Date().getTime(),
400
385
  };
401
- if (user && user.anonymous) {
402
- e.contextKind = userContextKind(user);
386
+ if (context && context.anonymous) {
387
+ e.contextKind = userContextKind(context);
403
388
  }
404
389
  // Note, check specifically for null/undefined because it is legal to set these fields to a falsey value.
405
390
  if (data !== null && data !== undefined) {
@@ -413,7 +398,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
413
398
 
414
399
  function connectStream() {
415
400
  streamActive = true;
416
- if (!ident.getUser()) {
401
+ if (!ident.getContext()) {
417
402
  return;
418
403
  }
419
404
  const tryParseData = jsonData => {
@@ -424,16 +409,16 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
424
409
  return undefined;
425
410
  }
426
411
  };
427
- stream.connect(ident.getUser(), hash, {
412
+ stream.connect(ident.getContext(), hash, {
428
413
  ping: function() {
429
414
  logger.debug(messages.debugStreamPing());
430
- const userAtTimeOfPingEvent = ident.getUser();
415
+ const contextAtTimeOfPingEvent = ident.getContext();
431
416
  requestor
432
- .fetchFlagSettings(userAtTimeOfPingEvent, hash)
417
+ .fetchFlagSettings(contextAtTimeOfPingEvent, hash)
433
418
  .then(requestedFlags => {
434
- // Check whether the current user is still the same - we don't want to overwrite the flags if
419
+ // Check whether the current context is still the same - we don't want to overwrite the flags if
435
420
  // the application has called identify() while this request was in progress
436
- if (utils.deepEquals(userAtTimeOfPingEvent, ident.getUser())) {
421
+ if (utils.deepEquals(contextAtTimeOfPingEvent, ident.getContext())) {
437
422
  replaceAllFlags(requestedFlags || {});
438
423
  }
439
424
  })
@@ -663,17 +648,20 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
663
648
  if (!env) {
664
649
  return Promise.reject(new errors.LDInvalidEnvironmentIdError(messages.environmentNotSpecified()));
665
650
  }
666
- return userValidator.validateUser(user).then(realUser => {
667
- ident.setUser(realUser);
668
- if (typeof options.bootstrap === 'object') {
669
- // flags have already been set earlier
670
- return signalSuccessfulInit();
671
- } else if (useLocalStorage) {
672
- return finishInitWithLocalStorage();
673
- } else {
674
- return finishInitWithPolling();
675
- }
676
- });
651
+ return anonymousContextProcessor
652
+ .processContext(context)
653
+ .then(verifyContext)
654
+ .then(validatedContext => {
655
+ ident.setContext(validatedContext);
656
+ if (typeof options.bootstrap === 'object') {
657
+ // flags have already been set earlier
658
+ return signalSuccessfulInit();
659
+ } else if (useLocalStorage) {
660
+ return finishInitWithLocalStorage();
661
+ } else {
662
+ return finishInitWithPolling();
663
+ }
664
+ });
677
665
  }
678
666
 
679
667
  function finishInitWithLocalStorage() {
@@ -681,7 +669,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
681
669
  if (storedFlags === null || storedFlags === undefined) {
682
670
  flags = {};
683
671
  return requestor
684
- .fetchFlagSettings(ident.getUser(), hash)
672
+ .fetchFlagSettings(ident.getContext(), hash)
685
673
  .then(requestedFlags => replaceAllFlags(requestedFlags || {}))
686
674
  .then(signalSuccessfulInit)
687
675
  .catch(err => {
@@ -696,7 +684,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
696
684
  utils.onNextTick(signalSuccessfulInit);
697
685
 
698
686
  return requestor
699
- .fetchFlagSettings(ident.getUser(), hash)
687
+ .fetchFlagSettings(ident.getContext(), hash)
700
688
  .then(requestedFlags => replaceAllFlags(requestedFlags))
701
689
  .catch(err => emitter.maybeReportError(err));
702
690
  }
@@ -705,7 +693,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
705
693
 
706
694
  function finishInitWithPolling() {
707
695
  return requestor
708
- .fetchFlagSettings(ident.getUser(), hash)
696
+ .fetchFlagSettings(ident.getContext(), hash)
709
697
  .then(requestedFlags => {
710
698
  flags = requestedFlags || {};
711
699
 
@@ -721,14 +709,14 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
721
709
 
722
710
  function initFromStateProvider(state) {
723
711
  environment = state.environment;
724
- ident.setUser(state.user);
712
+ ident.setContext(state.context);
725
713
  flags = { ...state.flags };
726
714
  utils.onNextTick(signalSuccessfulInit);
727
715
  }
728
716
 
729
717
  function updateFromStateProvider(state) {
730
- if (state.user) {
731
- ident.setUser(state.user);
718
+ if (state.context) {
719
+ ident.setContext(state.context);
732
720
  }
733
721
  if (state.flags) {
734
722
  replaceAllFlags(state.flags); // don't wait for this Promise to be resolved
@@ -788,11 +776,10 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
788
776
  waitForInitialization: () => initializationStateTracker.getInitializationPromise(),
789
777
  waitUntilReady: () => initializationStateTracker.getReadyPromise(),
790
778
  identify: identify,
791
- getUser: getUser,
779
+ getContext: getContext,
792
780
  variation: variation,
793
781
  variationDetail: variationDetail,
794
782
  track: track,
795
- alias: alias,
796
783
  on: on,
797
784
  off: off,
798
785
  setStreaming: setStreaming,
@@ -805,7 +792,7 @@ function initialize(env, user, specifiedOptions, platform, extraOptionDefs) {
805
792
  client: client, // The client object containing all public methods.
806
793
  options: options, // The validated configuration object, including all defaults.
807
794
  emitter: emitter, // The event emitter which can be used to log errors or trigger events.
808
- ident: ident, // The Identity object that manages the current user.
795
+ ident: ident, // The Identity object that manages the current context.
809
796
  logger: logger, // The logging abstraction.
810
797
  requestor: requestor, // The Requestor object.
811
798
  start: start, // Starts the client once the environment is ready.
package/src/messages.js CHANGED
@@ -25,7 +25,7 @@ const eventCapacityExceeded = function() {
25
25
  return 'Exceeded event queue capacity. Increase capacity to avoid dropping events.';
26
26
  };
27
27
 
28
- const eventWithoutUser = function() {
28
+ const eventWithoutContext = function() {
29
29
  return 'Be sure to call `identify` in the LaunchDarkly client: https://docs.launchdarkly.com/sdk/features/identify#javascript';
30
30
  };
31
31
 
@@ -60,12 +60,12 @@ const errorFetchingFlags = function(err) {
60
60
  return 'Error fetching flag settings: ' + errorString(err);
61
61
  };
62
62
 
63
- const userNotSpecified = function() {
64
- return 'No user specified.' + docLink;
63
+ const contextNotSpecified = function() {
64
+ return 'No context specified.' + docLink;
65
65
  };
66
66
 
67
- const invalidUser = function() {
68
- return 'Invalid user specified.' + docLink;
67
+ const invalidContext = function() {
68
+ return 'Invalid context specified.' + docLink;
69
69
  };
70
70
 
71
71
  const invalidData = function() {
@@ -210,7 +210,7 @@ module.exports = {
210
210
  environmentNotSpecified,
211
211
  errorFetchingFlags,
212
212
  eventCapacityExceeded,
213
- eventWithoutUser,
213
+ eventWithoutContext,
214
214
  httpErrorMessage,
215
215
  httpUnavailable,
216
216
  identifyDisabled,
@@ -219,8 +219,8 @@ module.exports = {
219
219
  invalidData,
220
220
  invalidInspector,
221
221
  invalidKey,
222
+ invalidContext,
222
223
  invalidTagValue,
223
- invalidUser,
224
224
  localStorageUnavailable,
225
225
  networkError,
226
226
  optionBelowMinimum,
@@ -230,8 +230,8 @@ module.exports = {
230
230
  tagValueTooLong,
231
231
  unknownCustomEventKey,
232
232
  unknownOption,
233
+ contextNotSpecified,
233
234
  unrecoverableStreamError,
234
- userNotSpecified,
235
235
  wrongOptionType,
236
236
  wrongOptionTypeBoolean,
237
237
  };
package/src/utils.js CHANGED
@@ -120,7 +120,7 @@ function transformVersionedValuesToValues(flagsState) {
120
120
  * @param {Array[Object}]} events queue of events to divide
121
121
  * @returns Array[Array[Object]]
122
122
  */
123
- function chunkUserEventsForUrl(maxLength, events) {
123
+ function chunkEventsForUrl(maxLength, events) {
124
124
  const allEvents = events.slice(0);
125
125
  const allChunks = [];
126
126
  let remainingSpace = maxLength;
@@ -165,34 +165,37 @@ function objectHasOwnProperty(object, name) {
165
165
  return Object.prototype.hasOwnProperty.call(object, name);
166
166
  }
167
167
 
168
- function sanitizeUser(user) {
169
- if (!user) {
170
- return user;
168
+ function sanitizeContext(context) {
169
+ if (!context) {
170
+ return context;
171
171
  }
172
- let newUser;
173
- for (const i in userAttrsToStringify) {
174
- const attr = userAttrsToStringify[i];
175
- const value = user[attr];
176
- if (value !== undefined && typeof value !== 'string') {
177
- newUser = newUser || { ...user };
178
- newUser[attr] = String(value);
179
- }
172
+ let newContext;
173
+ // Only stringify user attributes for legacy users.
174
+ if (context.kind === null || context.kind === undefined) {
175
+ userAttrsToStringify.forEach(attr => {
176
+ const value = context[attr];
177
+ if (value !== undefined && typeof value !== 'string') {
178
+ newContext = newContext || { ...context };
179
+ newContext[attr] = String(value);
180
+ }
181
+ });
180
182
  }
181
- return newUser || user;
183
+
184
+ return newContext || context;
182
185
  }
183
186
 
184
187
  module.exports = {
185
188
  appendUrlPath,
186
189
  base64URLEncode,
187
190
  btoa,
188
- chunkUserEventsForUrl,
191
+ chunkEventsForUrl,
189
192
  clone,
190
193
  deepEquals,
191
194
  extend,
192
195
  getLDUserAgentString,
193
196
  objectHasOwnProperty,
194
197
  onNextTick,
195
- sanitizeUser,
198
+ sanitizeContext,
196
199
  transformValuesToVersionedValues,
197
200
  transformVersionedValuesToValues,
198
201
  wrapPromiseCallback,
package/test-types.ts CHANGED
@@ -7,7 +7,7 @@ import * as ld from 'launchdarkly-js-sdk-common';
7
7
  var userWithKeyOnly: ld.LDUser = { key: 'user' };
8
8
  var anonUserWithNoKey: ld.LDUser = { anonymous: true };
9
9
  var anonUserWithKey: ld.LDUser = { key: 'anon-user', anonymous: true };
10
- var user: ld.LDUser = {
10
+ var user: ld.LDContext = {
11
11
  key: 'user',
12
12
  secondary: 'otherkey',
13
13
  name: 'name',
@@ -41,9 +41,7 @@ var allBaseOptions: ld.LDOptionsBase = {
41
41
  evaluationReasons: true,
42
42
  sendEvents: true,
43
43
  allAttributesPrivate: true,
44
- privateAttributeNames: [ 'x' ],
45
- inlineUsersInEvents: true,
46
- allowFrequentDuplicateEvents: true,
44
+ privateAttributes: [ 'x' ],
47
45
  sendEventsOnlyForVariation: true,
48
46
  flushInterval: 1,
49
47
  streamReconnectDelay: 1,
@@ -63,9 +61,7 @@ client.identify(user).then(() => {});
63
61
  client.identify(user, undefined, () => {});
64
62
  client.identify(user, 'hash').then(() => {});
65
63
 
66
- client.alias(user, anonUserWithKey);
67
-
68
- var user: ld.LDUser = client.getUser();
64
+ var user: ld.LDContext = client.getContext();
69
65
 
70
66
  client.flush(() => {});
71
67
  client.flush().then(() => {});