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/package.json +3 -1
- package/src/AnonymousContextProcessor.js +95 -0
- package/src/ContextFilter.js +147 -0
- package/src/EventProcessor.js +30 -13
- package/src/EventSender.js +1 -1
- package/src/Identity.js +10 -11
- package/src/PersistentFlagStore.js +3 -3
- package/src/Requestor.js +4 -4
- package/src/Stream.js +5 -5
- package/src/__tests__/ContextFilter-test.js +470 -0
- package/src/__tests__/EventProcessor-test.js +50 -121
- package/src/__tests__/LDClient-events-test.js +9 -152
- package/src/__tests__/LDClient-inspectors-test.js +1 -1
- package/src/__tests__/LDClient-test.js +18 -15
- package/src/__tests__/TransientContextProcessor-test.js +115 -0
- package/src/__tests__/attributeReference-test.js +400 -0
- package/src/__tests__/configuration-test.js +20 -21
- package/src/__tests__/context-test.js +93 -0
- package/src/__tests__/diagnosticEvents-test.js +0 -4
- package/src/__tests__/utils-test.js +3 -9
- package/src/attributeReference.js +143 -0
- package/src/configuration.js +4 -8
- package/src/context.js +96 -0
- package/src/diagnosticEvents.js +0 -2
- package/src/index.js +76 -89
- package/src/messages.js +8 -8
- package/src/utils.js +18 -15
- package/test-types.ts +3 -7
- package/typings.d.ts +140 -76
- package/src/UserFilter.js +0 -75
- package/src/UserValidator.js +0 -56
- package/src/__tests__/UserFilter-test.js +0 -93
- package/src/__tests__/UserValidator-test.js +0 -57
|
@@ -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('
|
|
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 =
|
|
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
|
+
};
|
package/src/configuration.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
83
|
-
//
|
|
84
|
-
// the
|
|
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
|
+
};
|
package/src/diagnosticEvents.js
CHANGED
|
@@ -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
|