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.
@@ -0,0 +1,93 @@
1
+ const { checkContext, getContextKinds, getCanonicalKey } = require('../context');
2
+
3
+ describe.each([{ key: 'test' }, { kind: 'user', key: 'test' }, { kind: 'multi', user: { key: 'test' } }])(
4
+ 'given a context which contains a single kind',
5
+ context => {
6
+ it('should get the context kind', () => {
7
+ expect(getContextKinds(context)).toEqual(['user']);
8
+ });
9
+
10
+ it('should be valid', () => {
11
+ expect(checkContext(context, false)).toBeTruthy();
12
+ });
13
+ }
14
+ );
15
+
16
+ describe('given a valid multi-kind context', () => {
17
+ const context = {
18
+ kind: 'multi',
19
+ user: {
20
+ key: 'user',
21
+ },
22
+ org: {
23
+ key: 'org',
24
+ },
25
+ };
26
+
27
+ it('should get a list of the kinds', () => {
28
+ expect(getContextKinds(context).sort()).toEqual(['org', 'user']);
29
+ });
30
+
31
+ it('should be valid', () => {
32
+ expect(checkContext(context, false)).toBeTruthy();
33
+ });
34
+ });
35
+
36
+ // A sample of invalid characters.
37
+ const invalidSampleChars = [
38
+ ...`#$%&'()*+,/:;<=>?@[\\]^\`{|}~ ¡¢£¤¥¦§¨©ª«¬­®¯°±²
39
+ ³´µ¶·¸¹º»¼½¾¿À汉字`,
40
+ ];
41
+ const badKinds = invalidSampleChars.map(char => ({ kind: char, key: 'test' }));
42
+
43
+ describe.each([
44
+ {}, // An empty object is not a valid context.
45
+ { key: '' }, // If allowLegacyKey is not true, then this should be invalid.
46
+ { kind: 'kind', key: 'kind' }, // The kind cannot be kind.
47
+ { kind: 'user' }, // The context needs to have a key.
48
+ { kind: 'org', key: '' }, // For a non-legacy context the key cannot be empty.
49
+ { kind: ' ', key: 'test' }, // Kind cannot be whitespace only.
50
+ { kind: 'cat dog', key: 'test' }, // Kind cannot contain whitespace
51
+ { kind: '~!@#$%^&*()_+', key: 'test' }, // Special characters are not valid.
52
+ ...badKinds,
53
+ ])('given invalid contexts', context => {
54
+ it('should not be valid', () => {
55
+ expect(checkContext(context, false)).toBeFalsy();
56
+ });
57
+ });
58
+
59
+ const validChars = ['0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_.'];
60
+ const goodKinds = validChars.map(char => [{ kind: char, key: 'test' }, false]);
61
+
62
+ describe.each([
63
+ [{ key: '' }, true], // Allow a legacy context with an empty key.
64
+ ...goodKinds,
65
+ ])('given valid contexts', (context, allowLegacyKey) => {
66
+ it('should be valid and can get context kinds', () => {
67
+ expect(checkContext(context, allowLegacyKey)).toBeTruthy();
68
+ expect(getContextKinds(context)).toEqual([context.kind || 'user']);
69
+ });
70
+ });
71
+
72
+ describe('when determining canonical keys', () => {
73
+ it.each([
74
+ [{ key: 'test' }, 'test'],
75
+ [{ kind: 'user', key: 'test' }, 'test'],
76
+ [{ kind: 'org', key: 'orgtest' }, 'org:orgtest'],
77
+ [{ kind: 'multi', user: { key: 'usertest' } }, 'user:usertest'],
78
+ [{ kind: 'multi', user: { key: 'usertest' }, org: { key: 'orgtest' } }, 'org:orgtest:user:usertest'],
79
+ [{ kind: 'multi', user: { key: 'user:test' }, org: { key: 'org:test' } }, 'org:org%3Atest:user:user%3Atest'],
80
+ [{ kind: 'multi', user: { key: 'user%test' }, org: { key: 'org%test' } }, 'org:org%25test:user:user%25test'],
81
+ [
82
+ { kind: 'multi', user: { key: 'user%:test' }, org: { key: 'org%:test' } },
83
+ 'org:org%25%3Atest:user:user%25%3Atest',
84
+ ],
85
+ ])('produces a canonical key for valid contexts', (context, canonicalKey) => {
86
+ expect(getCanonicalKey(context)).toEqual(canonicalKey);
87
+ });
88
+
89
+ it('does not break with an null/undefined context', () => {
90
+ expect(getCanonicalKey(undefined)).toBeUndefined();
91
+ expect(getCanonicalKey(null)).toBeUndefined();
92
+ });
93
+ });
@@ -101,7 +101,6 @@ describe('DiagnosticsManager', () => {
101
101
  };
102
102
  const defaultConfigInEvent = {
103
103
  allAttributesPrivate: false,
104
- autoAliasingOptOut: false,
105
104
  bootstrapMode: false,
106
105
  customBaseURI: false,
107
106
  customEventsURI: false,
@@ -110,7 +109,6 @@ describe('DiagnosticsManager', () => {
110
109
  eventsCapacity: defaultConfig.eventCapacity,
111
110
  eventsFlushIntervalMillis: defaultConfig.flushInterval,
112
111
  fetchGoalsDisabled: false,
113
- inlineUsersInEvents: false,
114
112
  reconnectTimeMillis: defaultConfig.streamReconnectDelay,
115
113
  sendEventsOnlyForVariation: false,
116
114
  streamingDisabled: true,
@@ -196,12 +194,10 @@ describe('DiagnosticsManager', () => {
196
194
  [{ eventCapacity: 222 }, { eventsCapacity: 222 }],
197
195
  [{ flushInterval: 2222 }, { eventsFlushIntervalMillis: 2222 }],
198
196
  [{ fetchGoals: false }, { fetchGoalsDisabled: true }],
199
- [{ inlineUsersInEvents: true }, { inlineUsersInEvents: true }],
200
197
  [{ streamReconnectDelay: 2222 }, { reconnectTimeMillis: 2222 }],
201
198
  [{ sendEventsOnlyForVariation: true }, { sendEventsOnlyForVariation: true }],
202
199
  [{ streaming: true }, { streamingDisabled: false }],
203
200
  [{ hash: 'x' }, { usingSecureMode: true }],
204
- [{ autoAliasingOptOut: true }, { autoAliasingOptOut: true }],
205
201
  ];
206
202
  for (const i in configAndResultValues) {
207
203
  const configOverrides = configAndResultValues[i][0];
@@ -1,10 +1,4 @@
1
- import {
2
- appendUrlPath,
3
- base64URLEncode,
4
- getLDUserAgentString,
5
- wrapPromiseCallback,
6
- chunkUserEventsForUrl,
7
- } from '../utils';
1
+ import { appendUrlPath, base64URLEncode, getLDUserAgentString, wrapPromiseCallback, chunkEventsForUrl } from '../utils';
8
2
 
9
3
  import * as stubPlatform from './stubPlatform';
10
4
 
@@ -70,13 +64,13 @@ describe('utils', () => {
70
64
  });
71
65
  });
72
66
 
73
- describe('chunkUserEventsForUrl', () => {
67
+ describe('chunkEventsForUrl', () => {
74
68
  it('should properly chunk the list of events', () => {
75
69
  const user = { key: 'foo' };
76
70
  const event = { kind: 'identify', key: user.key };
77
71
  const eventLength = base64URLEncode(JSON.stringify(event)).length;
78
72
  const events = [event, event, event, event, event];
79
- const chunks = chunkUserEventsForUrl(eventLength * 2, events);
73
+ const chunks = chunkEventsForUrl(eventLength * 2, events);
80
74
  expect(chunks).toEqual([[event, event], [event, event], [event]]);
81
75
  });
82
76
  });
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Take a key string and escape the characters to allow it to be used as a reference.
3
+ * @param {string} key
4
+ * @returns {string} The processed key.
5
+ */
6
+ function processEscapeCharacters(key) {
7
+ return key.replace(/~/g, '~0').replace(/\//g, '~1');
8
+ }
9
+
10
+ /**
11
+ * @param {string} reference The reference to get the components of.
12
+ * @returns {string[]} The components of the reference. Escape characters will be converted to their representative values.
13
+ */
14
+ function getComponents(reference) {
15
+ const referenceWithoutPrefix = reference.startsWith('/') ? reference.substring(1) : reference;
16
+ return referenceWithoutPrefix
17
+ .split('/')
18
+ .map(component => (component.indexOf('~') >= 0 ? component.replace(/~1/g, '/').replace(/~0/g, '~') : component));
19
+ }
20
+
21
+ /**
22
+ * @param {string} reference The reference to check if it is a literal.
23
+ * @returns true if the reference is a literal.
24
+ */
25
+ function isLiteral(reference) {
26
+ return !reference.startsWith('/');
27
+ }
28
+
29
+ /**
30
+ * Compare two references and determine if they are equivalent.
31
+ * @param {string} a
32
+ * @param {string} b
33
+ */
34
+ function compare(a, b) {
35
+ const aIsLiteral = isLiteral(a);
36
+ const bIsLiteral = isLiteral(b);
37
+ if (aIsLiteral && bIsLiteral) {
38
+ return a === b;
39
+ }
40
+ if (aIsLiteral) {
41
+ const bComponents = getComponents(b);
42
+ if (bComponents.length !== 1) {
43
+ return false;
44
+ }
45
+ return a === bComponents[0];
46
+ }
47
+ if (bIsLiteral) {
48
+ const aComponents = getComponents(a);
49
+ if (aComponents.length !== 1) {
50
+ return false;
51
+ }
52
+ return b === aComponents[0];
53
+ }
54
+ return a === b;
55
+ }
56
+
57
+ /**
58
+ * @param {string} a
59
+ * @param {string} b
60
+ * @returns The two strings joined by '/'.
61
+ */
62
+ function join(a, b) {
63
+ return `${a}/${b}`;
64
+ }
65
+
66
+ /**
67
+ * There are cases where a field could have been named with a preceeding '/'.
68
+ * If that attribute was private, then the literal would appear to be a reference.
69
+ * This method can be used to convert a literal to a reference in such situations.
70
+ * @param {string} literal The literal to convert to a reference.
71
+ * @returns A literal which has been converted to a reference.
72
+ */
73
+ function literalToReference(literal) {
74
+ return `/${processEscapeCharacters(literal)}`;
75
+ }
76
+
77
+ /**
78
+ * Clone an object excluding the values referenced by a list of references.
79
+ * @param {Object} target The object to clone.
80
+ * @param {string[]} references A list of references from the cloned object.
81
+ * @returns {{cloned: Object, excluded: string[]}} The cloned object and a list of excluded values.
82
+ */
83
+ function cloneExcluding(target, references) {
84
+ const stack = [];
85
+ const cloned = {};
86
+ const excluded = [];
87
+
88
+ stack.push(
89
+ ...Object.keys(target).map(key => ({
90
+ key,
91
+ ptr: literalToReference(key),
92
+ source: target,
93
+ parent: cloned,
94
+ visited: [target],
95
+ }))
96
+ );
97
+
98
+ while (stack.length) {
99
+ const item = stack.pop();
100
+ if (!references.some(ptr => compare(ptr, item.ptr))) {
101
+ const value = item.source[item.key];
102
+
103
+ // Handle null because it overlaps with object, which we will want to handle later.
104
+ if (value === null) {
105
+ item.parent[item.key] = value;
106
+ } else if (Array.isArray(value)) {
107
+ item.parent[item.key] = [...value];
108
+ } else if (typeof value === 'object') {
109
+ //Arrays and null must already be handled.
110
+
111
+ //Prevent cycles by not visiting the same object
112
+ //with in the same branch. Parallel branches
113
+ //may contain the same object.
114
+ if (item.visited.includes(value)) {
115
+ continue;
116
+ }
117
+
118
+ item.parent[item.key] = {};
119
+
120
+ stack.push(
121
+ ...Object.keys(value).map(key => ({
122
+ key,
123
+ ptr: join(item.ptr, processEscapeCharacters(key)),
124
+ source: value,
125
+ parent: item.parent[item.key],
126
+ visited: [...item.visited, value],
127
+ }))
128
+ );
129
+ } else {
130
+ item.parent[item.key] = value;
131
+ }
132
+ } else {
133
+ excluded.push(item.ptr);
134
+ }
135
+ }
136
+ return { cloned, excluded: excluded.sort() };
137
+ }
138
+
139
+ module.exports = {
140
+ cloneExcluding,
141
+ compare,
142
+ literalToReference,
143
+ };
@@ -20,8 +20,6 @@ const baseOptionDefs = {
20
20
  streaming: { type: 'boolean' }, // default for this is undefined, which is different from false
21
21
  sendLDHeaders: { default: true },
22
22
  requestHeaderTransform: { type: 'function' },
23
- inlineUsersInEvents: { default: false },
24
- allowFrequentDuplicateEvents: { default: false },
25
23
  sendEventsOnlyForVariation: { default: false },
26
24
  useReport: { default: false },
27
25
  evaluationReasons: { default: false },
@@ -30,14 +28,13 @@ const baseOptionDefs = {
30
28
  samplingInterval: { default: 0, minimum: 0 },
31
29
  streamReconnectDelay: { default: 1000, minimum: 0 },
32
30
  allAttributesPrivate: { default: false },
33
- privateAttributeNames: { default: [] },
31
+ privateAttributes: { default: [] },
34
32
  bootstrap: { type: 'string|object' },
35
33
  diagnosticRecordingInterval: { default: 900000, minimum: 2000 },
36
34
  diagnosticOptOut: { default: false },
37
35
  wrapperName: { type: 'string' },
38
36
  wrapperVersion: { type: 'string' },
39
37
  stateProvider: { type: 'object' }, // not a public option, used internally
40
- autoAliasingOptOut: { default: false },
41
38
  application: { validator: applicationConfigValidator },
42
39
  inspectors: { default: [] },
43
40
  };
@@ -79,10 +76,9 @@ function validate(options, emitter, extraOptionDefs, logger) {
79
76
  const optionDefs = utils.extend({ logger: { default: logger } }, baseOptionDefs, extraOptionDefs);
80
77
 
81
78
  const deprecatedOptions = {
82
- // The property name is the deprecated name, and the property value is the preferred name if
83
- // any, or null/undefined if there is no replacement. This should be removed, along with
84
- // the option, in the next major version.
85
- allowFrequentDuplicateEvents: undefined,
79
+ // As of the latest major version, there are no deprecated options. Next time we deprecate
80
+ // something, add an item here where the property name is the deprecated name, and the
81
+ // property value is the preferred name if any, or null/undefined if there is no replacement.
86
82
  };
87
83
 
88
84
  function checkDeprecatedOptions(config) {
package/src/context.js ADDED
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Validate a context kind.
3
+ * @param {string} kind
4
+ * @returns true if the kind is valid.
5
+ */
6
+ function validKind(kind) {
7
+ return typeof kind === 'string' && kind !== 'kind' && kind.match(/^(\w|\.|-)+$/);
8
+ }
9
+
10
+ /**
11
+ * Perform a check of basic context requirements.
12
+ * @param {Object} context
13
+ * @param {boolean} allowLegacyKey If true, then a legacy user can have an
14
+ * empty or non-string key. A legacy user is a context without a kind.
15
+ * @returns true if the context meets basic requirements.
16
+ */
17
+ function checkContext(context, allowLegacyKey) {
18
+ if (context) {
19
+ if (allowLegacyKey && (context.kind === undefined || context.kind === null)) {
20
+ return context.key !== undefined && context.key !== null;
21
+ }
22
+ const key = context.key;
23
+ const kind = context.kind === undefined ? 'user' : context.kind;
24
+ const kindValid = validKind(kind);
25
+ const keyValid = kind === 'multi' || (key !== undefined && key !== null && key !== '');
26
+ if (kind === 'multi') {
27
+ const kinds = Object.keys(context).filter(key => key !== 'kind');
28
+ return (
29
+ keyValid &&
30
+ kinds.every(key => validKind(key)) &&
31
+ kinds.every(key => {
32
+ const contextKey = context[key].key;
33
+ return contextKey !== undefined && contextKey !== null && contextKey !== '';
34
+ })
35
+ );
36
+ }
37
+ return keyValid && kindValid;
38
+ }
39
+ return false;
40
+ }
41
+
42
+ /**
43
+ * For a given context get a list of context kinds.
44
+ * @param {Object} context
45
+ * @returns A list of kinds in the context.
46
+ */
47
+ function getContextKinds(context) {
48
+ if (context) {
49
+ if (context.kind === null || context.kind === undefined) {
50
+ return ['user'];
51
+ }
52
+ if (context.kind !== 'multi') {
53
+ return [context.kind];
54
+ }
55
+ return Object.keys(context).filter(kind => kind !== 'kind');
56
+ }
57
+ return [];
58
+ }
59
+
60
+ /**
61
+ * The partial URL encoding is needed because : is a valid character in context keys.
62
+ *
63
+ * Partial encoding is the replacement of all colon (:) characters with the URL
64
+ * encoded equivalent (%3A) and all percent (%) characters with the URL encoded
65
+ * equivalent (%25).
66
+ * @param {string} key The key to encode.
67
+ * @returns {string} Partially URL encoded key.
68
+ */
69
+ function encodeKey(key) {
70
+ if (key.includes('%') || key.includes(':')) {
71
+ return key.replace(/%/g, '%25').replace(/:/g, '%3A');
72
+ }
73
+ return key;
74
+ }
75
+
76
+ function getCanonicalKey(context) {
77
+ if (context) {
78
+ if ((context.kind === undefined || context.kind === null || context.kind === 'user') && context.key) {
79
+ return context.key;
80
+ } else if (context.kind !== 'multi' && context.key) {
81
+ return `${context.kind}:${encodeKey(context.key)}`;
82
+ } else if (context.kind === 'multi') {
83
+ return Object.keys(context)
84
+ .sort()
85
+ .filter(key => key !== 'kind')
86
+ .map(key => `${key}:${encodeKey(context[key].key)}`)
87
+ .join(':');
88
+ }
89
+ }
90
+ }
91
+
92
+ module.exports = {
93
+ checkContext,
94
+ getContextKinds,
95
+ getCanonicalKey,
96
+ };
@@ -195,14 +195,12 @@ function DiagnosticsManager(
195
195
  reconnectTimeMillis: config.streamReconnectDelay,
196
196
  streamingDisabled: !streamingEnabled,
197
197
  allAttributesPrivate: !!config.allAttributesPrivate,
198
- inlineUsersInEvents: !!config.inlineUsersInEvents,
199
198
  diagnosticRecordingIntervalMillis: config.diagnosticRecordingInterval,
200
199
  // The following extra properties are only provided by client-side JS SDKs:
201
200
  usingSecureMode: !!config.hash,
202
201
  bootstrapMode: !!config.bootstrap,
203
202
  fetchGoalsDisabled: !config.fetchGoals,
204
203
  sendEventsOnlyForVariation: !!config.sendEventsOnlyForVariation,
205
- autoAliasingOptOut: !!config.autoAliasingOptOut,
206
204
  };
207
205
  // Client-side JS SDKs do not have the following properties which other SDKs have:
208
206
  // connectTimeoutMillis