launchdarkly-js-sdk-common 5.0.3 → 5.2.0
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/CHANGELOG.md +15 -0
- package/CODEOWNERS +2 -0
- package/README.md +2 -2
- package/SECURITY.md +5 -0
- package/package.json +4 -3
- package/src/ContextFilter.js +17 -12
- package/src/EventProcessor.js +4 -2
- package/src/EventSender.js +6 -30
- package/src/__tests__/ContextFilter-test.js +33 -0
- package/src/__tests__/EventProcessor-test.js +8 -5
- package/src/__tests__/EventSender-test.js +14 -70
- package/src/__tests__/EventSummarizer-test.js +4 -1
- package/src/__tests__/LDClient-events-test.js +41 -29
- package/src/__tests__/LDClient-localstorage-test.js +8 -8
- package/src/__tests__/LDClient-streaming-test.js +41 -41
- package/src/__tests__/LDClient-test.js +37 -37
- package/src/__tests__/LDClient-timeout-test.js +90 -0
- package/src/__tests__/diagnosticEvents-test.js +8 -2
- package/src/__tests__/stubPlatform.js +0 -4
- package/src/__tests__/testUtils.js +1 -1
- package/src/__tests__/utils-test.js +1 -12
- package/src/errors.js +2 -0
- package/src/headers.js +2 -2
- package/src/index.js +40 -1
- package/src/messages.js +4 -0
- package/src/timedPromise.js +17 -0
- package/src/utils.js +0 -41
- package/test-types.ts +1 -1
- package/typings.d.ts +81 -71
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `launchdarkly-js-sdk-common` package will be documented in this file. Changes that affect the dependent SDKs such as `launchdarkly-js-client-sdk` should also be logged in those projects, in the next release that uses the updated version of this package. This project adheres to [Semantic Versioning](http://semver.org).
|
|
4
4
|
|
|
5
|
+
## [5.1.0] - 2024-03-19
|
|
6
|
+
### Changed:
|
|
7
|
+
- Redact anonymous attributes within feature events
|
|
8
|
+
- Always inline contexts for feature events
|
|
9
|
+
|
|
10
|
+
### Fixed:
|
|
11
|
+
- Pin dev version of node to compatible types.
|
|
12
|
+
|
|
13
|
+
### Removed:
|
|
14
|
+
- HTTP fallback ping
|
|
15
|
+
|
|
16
|
+
## [5.0.3] - 2023-03-21
|
|
17
|
+
### Changed:
|
|
18
|
+
- Update `LDContext` to allow for key to be optional. This is used when making an anonymous context with a generated key.
|
|
19
|
+
|
|
5
20
|
## [5.0.2] - 2023-02-15
|
|
6
21
|
### Changed:
|
|
7
22
|
- Removed usage of optional chaining (`?.`) to improve compatibility with projects which are using older transpilation tooling.
|
package/CODEOWNERS
ADDED
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## LaunchDarkly overview
|
|
6
6
|
|
|
7
|
-
[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves
|
|
7
|
+
[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!
|
|
8
8
|
|
|
9
9
|
[](https://twitter.com/intent/follow?screen_name=launchdarkly)
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ We encourage pull requests and other contributions from the community. Check out
|
|
|
27
27
|
* Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
|
|
28
28
|
* Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
|
|
29
29
|
* Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
|
|
30
|
-
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies.
|
|
30
|
+
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
|
|
31
31
|
* Explore LaunchDarkly
|
|
32
32
|
* [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
|
|
33
33
|
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Reporting and Fixing Security Issues
|
|
2
|
+
|
|
3
|
+
Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty.
|
|
4
|
+
|
|
5
|
+
Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "launchdarkly-js-sdk-common",
|
|
3
|
-
"version": "5.0
|
|
3
|
+
"version": "5.2.0",
|
|
4
4
|
"description": "LaunchDarkly SDK for JavaScript - common code",
|
|
5
5
|
"author": "LaunchDarkly <team@launchdarkly.com>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"@babel/runtime": "7.6.3",
|
|
29
29
|
"@rollup/plugin-replace": "^2.2.0",
|
|
30
30
|
"@types/jest": "^27.4.1",
|
|
31
|
+
"@types/node": "12.12.6",
|
|
31
32
|
"babel-eslint": "^10.1.0",
|
|
32
33
|
"babel-jest": "^25.1.0",
|
|
33
34
|
"cross-env": "^5.1.4",
|
|
@@ -37,10 +38,10 @@
|
|
|
37
38
|
"eslint-formatter-pretty": "^1.3.0",
|
|
38
39
|
"eslint-plugin-babel": "^5.0.0",
|
|
39
40
|
"eslint-plugin-prettier": "^2.6.0",
|
|
40
|
-
"jest": "^
|
|
41
|
+
"jest": "^26.6.3",
|
|
41
42
|
"jsdom": "^11.11.0",
|
|
42
43
|
"launchdarkly-js-test-helpers": "1.1.0",
|
|
43
|
-
"prettier": "1.
|
|
44
|
+
"prettier": "1.19.1",
|
|
44
45
|
"readline-sync": "^1.4.9",
|
|
45
46
|
"typescript": "~4.4.4"
|
|
46
47
|
},
|
package/src/ContextFilter.js
CHANGED
|
@@ -16,23 +16,27 @@ function ContextFilter(config) {
|
|
|
16
16
|
* @param {Object} context
|
|
17
17
|
* @returns {string[]} A list of the attributes to filter.
|
|
18
18
|
*/
|
|
19
|
-
const getAttributesToFilter = context =>
|
|
20
|
-
(allAttributesPrivate
|
|
19
|
+
const getAttributesToFilter = (context, redactAnonymous) =>
|
|
20
|
+
(allAttributesPrivate || (redactAnonymous && context.anonymous)
|
|
21
21
|
? Object.keys(context)
|
|
22
22
|
: [...privateAttributes, ...((context._meta && context._meta.privateAttributes) || [])]
|
|
23
23
|
).filter(attr => !protectedAttributes.some(protectedAttr => AttributeReference.compare(attr, protectedAttr)));
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* @param {Object} context
|
|
27
|
+
* @param {boolean} redactAnonymous
|
|
27
28
|
* @returns {Object} A copy of the context with private attributes removed,
|
|
28
29
|
* and the redactedAttributes meta populated.
|
|
29
30
|
*/
|
|
30
|
-
const filterSingleKind = context => {
|
|
31
|
+
const filterSingleKind = (context, redactAnonymous) => {
|
|
31
32
|
if (typeof context !== 'object' || context === null || Array.isArray(context)) {
|
|
32
33
|
return undefined;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const { cloned, excluded } = AttributeReference.cloneExcluding(
|
|
36
|
+
const { cloned, excluded } = AttributeReference.cloneExcluding(
|
|
37
|
+
context,
|
|
38
|
+
getAttributesToFilter(context, redactAnonymous)
|
|
39
|
+
);
|
|
36
40
|
cloned.key = String(cloned.key);
|
|
37
41
|
if (excluded.length) {
|
|
38
42
|
if (!cloned._meta) {
|
|
@@ -57,10 +61,11 @@ function ContextFilter(config) {
|
|
|
57
61
|
|
|
58
62
|
/**
|
|
59
63
|
* @param {Object} context
|
|
64
|
+
* @param {boolean} redactAnonymous
|
|
60
65
|
* @returns {Object} A copy of the context with the private attributes removed,
|
|
61
66
|
* and the redactedAttributes meta populated for each sub-context.
|
|
62
67
|
*/
|
|
63
|
-
const filterMultiKind = context => {
|
|
68
|
+
const filterMultiKind = (context, redactAnonymous) => {
|
|
64
69
|
const filtered = {
|
|
65
70
|
kind: context.kind,
|
|
66
71
|
};
|
|
@@ -68,7 +73,7 @@ function ContextFilter(config) {
|
|
|
68
73
|
|
|
69
74
|
for (const contextKey of contextKeys) {
|
|
70
75
|
if (contextKey !== 'kind') {
|
|
71
|
-
const filteredContext = filterSingleKind(context[contextKey]);
|
|
76
|
+
const filteredContext = filterSingleKind(context[contextKey], redactAnonymous);
|
|
72
77
|
if (filteredContext) {
|
|
73
78
|
filtered[contextKey] = filteredContext;
|
|
74
79
|
}
|
|
@@ -113,21 +118,21 @@ function ContextFilter(config) {
|
|
|
113
118
|
filtered._meta = filtered._meta || {};
|
|
114
119
|
// If any private attributes started with '/' we need to convert them to references, otherwise the '/' will
|
|
115
120
|
// cause the literal to incorrectly be treated as a reference.
|
|
116
|
-
filtered._meta.privateAttributes = user.privateAttributeNames.map(
|
|
117
|
-
literal
|
|
121
|
+
filtered._meta.privateAttributes = user.privateAttributeNames.map(literal =>
|
|
122
|
+
literal.startsWith('/') ? AttributeReference.literalToReference(literal) : literal
|
|
118
123
|
);
|
|
119
124
|
}
|
|
120
125
|
|
|
121
126
|
return filtered;
|
|
122
127
|
};
|
|
123
128
|
|
|
124
|
-
filter.filter = context => {
|
|
129
|
+
filter.filter = (context, redactAnonymous = false) => {
|
|
125
130
|
if (context.kind === undefined || context.kind === null) {
|
|
126
|
-
return filterSingleKind(legacyToSingleKind(context));
|
|
131
|
+
return filterSingleKind(legacyToSingleKind(context), redactAnonymous);
|
|
127
132
|
} else if (context.kind === 'multi') {
|
|
128
|
-
return filterMultiKind(context);
|
|
133
|
+
return filterMultiKind(context, redactAnonymous);
|
|
129
134
|
} else {
|
|
130
|
-
return filterSingleKind(context);
|
|
135
|
+
return filterSingleKind(context, redactAnonymous);
|
|
131
136
|
}
|
|
132
137
|
};
|
|
133
138
|
|
package/src/EventProcessor.js
CHANGED
|
@@ -50,6 +50,9 @@ function EventProcessor(
|
|
|
50
50
|
if (e.kind === 'identify') {
|
|
51
51
|
// identify events always have an inline context
|
|
52
52
|
ret.context = contextFilter.filter(e.context);
|
|
53
|
+
} else if (e.kind === 'feature') {
|
|
54
|
+
// feature events always have an inline context
|
|
55
|
+
ret.context = contextFilter.filter(e.context, true);
|
|
53
56
|
} else {
|
|
54
57
|
ret.contextKeys = getContextKeysFromEvent(e);
|
|
55
58
|
delete ret['context'];
|
|
@@ -136,8 +139,7 @@ function EventProcessor(
|
|
|
136
139
|
}
|
|
137
140
|
queue = [];
|
|
138
141
|
logger.debug(messages.debugPostingEvents(eventsToSend.length));
|
|
139
|
-
return eventSender.sendEvents(eventsToSend, mainEventsUrl).then(
|
|
140
|
-
const responseInfo = responses && responses[0];
|
|
142
|
+
return eventSender.sendEvents(eventsToSend, mainEventsUrl).then(responseInfo => {
|
|
141
143
|
if (responseInfo) {
|
|
142
144
|
if (responseInfo.serverTime) {
|
|
143
145
|
lastKnownPastTime = responseInfo.serverTime;
|
package/src/EventSender.js
CHANGED
|
@@ -3,12 +3,8 @@ const utils = require('./utils');
|
|
|
3
3
|
const { v1: uuidv1 } = require('uuid');
|
|
4
4
|
const { getLDHeaders, transformHeaders } = require('./headers');
|
|
5
5
|
|
|
6
|
-
const MAX_URL_LENGTH = 2000;
|
|
7
|
-
|
|
8
6
|
function EventSender(platform, environmentId, options) {
|
|
9
|
-
const imageUrlPath = '/a/' + environmentId + '.gif';
|
|
10
7
|
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, getLDHeaders(platform, options));
|
|
11
|
-
const httpFallbackPing = platform.httpFallbackPing; // this will be set for us if we're in the browser SDK
|
|
12
8
|
const sender = {};
|
|
13
9
|
|
|
14
10
|
function getResponseInfo(result) {
|
|
@@ -23,7 +19,11 @@ function EventSender(platform, environmentId, options) {
|
|
|
23
19
|
return ret;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
|
-
sender.
|
|
22
|
+
sender.sendEvents = (events, url, isDiagnostic) => {
|
|
23
|
+
if (!platform.httpRequest) {
|
|
24
|
+
return Promise.resolve();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
27
|
const jsonBody = JSON.stringify(events);
|
|
28
28
|
const payloadId = isDiagnostic ? null : uuidv1();
|
|
29
29
|
|
|
@@ -55,31 +55,7 @@ function EventSender(platform, environmentId, options) {
|
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
return doPostRequest(true).catch(() => {});
|
|
60
|
-
} else {
|
|
61
|
-
httpFallbackPing && httpFallbackPing(url + imageUrlPath + '?d=' + utils.base64URLEncode(jsonBody));
|
|
62
|
-
return Promise.resolve(); // we don't wait for this request to complete, it's just a one-way ping
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
sender.sendEvents = function(events, url, isDiagnostic) {
|
|
67
|
-
if (!platform.httpRequest) {
|
|
68
|
-
return Promise.resolve();
|
|
69
|
-
}
|
|
70
|
-
const canPost = platform.httpAllowsPost();
|
|
71
|
-
let chunks;
|
|
72
|
-
if (canPost) {
|
|
73
|
-
// no need to break up events into chunks if we can send a POST
|
|
74
|
-
chunks = [events];
|
|
75
|
-
} else {
|
|
76
|
-
chunks = utils.chunkEventsForUrl(MAX_URL_LENGTH - url.length, events);
|
|
77
|
-
}
|
|
78
|
-
const results = [];
|
|
79
|
-
for (let i = 0; i < chunks.length; i++) {
|
|
80
|
-
results.push(sender.sendChunk(chunks[i], url, isDiagnostic, canPost));
|
|
81
|
-
}
|
|
82
|
-
return Promise.all(results);
|
|
58
|
+
return doPostRequest(true).catch(() => {});
|
|
83
59
|
};
|
|
84
60
|
|
|
85
61
|
return sender;
|
|
@@ -283,6 +283,11 @@ describe('when handling single kind contexts', () => {
|
|
|
283
283
|
expect(uf.filter(anonymousContext)).toEqual(contextWithAllAttrsHidden);
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
it('all attributes are redacted when anonymous', () => {
|
|
287
|
+
const uf = ContextFilter({});
|
|
288
|
+
expect(uf.filter(anonymousContext, true)).toEqual(contextWithAllAttrsHidden);
|
|
289
|
+
});
|
|
290
|
+
|
|
286
291
|
it('converts non-boolean anonymous to boolean.', () => {
|
|
287
292
|
const uf = ContextFilter({});
|
|
288
293
|
expect(uf.filter({ kind: 'user', key: 'user', anonymous: 'string' })).toEqual({
|
|
@@ -330,6 +335,7 @@ describe('when handling mult-kind contexts', () => {
|
|
|
330
335
|
user: {
|
|
331
336
|
key: 'abc',
|
|
332
337
|
name: 'alphabet',
|
|
338
|
+
anonymous: true,
|
|
333
339
|
letters: ['a', 'b', 'c'],
|
|
334
340
|
order: 3,
|
|
335
341
|
object: {
|
|
@@ -342,6 +348,25 @@ describe('when handling mult-kind contexts', () => {
|
|
|
342
348
|
},
|
|
343
349
|
};
|
|
344
350
|
|
|
351
|
+
const orgAndUserContextWithAnonymousRedaction = {
|
|
352
|
+
kind: 'multi',
|
|
353
|
+
organization: {
|
|
354
|
+
key: 'LD',
|
|
355
|
+
rocks: true,
|
|
356
|
+
name: 'name',
|
|
357
|
+
department: {
|
|
358
|
+
name: 'sdk',
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
user: {
|
|
362
|
+
key: 'abc',
|
|
363
|
+
anonymous: true,
|
|
364
|
+
_meta: {
|
|
365
|
+
redactedAttributes: ['/letters', '/name', '/object', '/order'],
|
|
366
|
+
},
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
|
|
345
370
|
const orgAndUserContextAllPrivate = {
|
|
346
371
|
kind: 'multi',
|
|
347
372
|
organization: {
|
|
@@ -352,6 +377,7 @@ describe('when handling mult-kind contexts', () => {
|
|
|
352
377
|
},
|
|
353
378
|
user: {
|
|
354
379
|
key: 'abc',
|
|
380
|
+
anonymous: true,
|
|
355
381
|
_meta: {
|
|
356
382
|
redactedAttributes: ['/letters', '/name', '/object', '/order'],
|
|
357
383
|
},
|
|
@@ -373,6 +399,7 @@ describe('when handling mult-kind contexts', () => {
|
|
|
373
399
|
user: {
|
|
374
400
|
key: 'abc',
|
|
375
401
|
order: 3,
|
|
402
|
+
anonymous: true,
|
|
376
403
|
object: {
|
|
377
404
|
a: 'a',
|
|
378
405
|
},
|
|
@@ -395,6 +422,7 @@ describe('when handling mult-kind contexts', () => {
|
|
|
395
422
|
user: {
|
|
396
423
|
key: 'abc',
|
|
397
424
|
name: 'alphabet',
|
|
425
|
+
anonymous: true,
|
|
398
426
|
order: 3,
|
|
399
427
|
object: {
|
|
400
428
|
a: 'a',
|
|
@@ -415,6 +443,11 @@ describe('when handling mult-kind contexts', () => {
|
|
|
415
443
|
expect(uf.filter(orgAndUserContext)).toEqual(orgAndUserContextAllPrivate);
|
|
416
444
|
});
|
|
417
445
|
|
|
446
|
+
it('it should remove attributes from all anonymous contexts', () => {
|
|
447
|
+
const uf = ContextFilter({});
|
|
448
|
+
expect(uf.filter(orgAndUserContext, true)).toEqual(orgAndUserContextWithAnonymousRedaction);
|
|
449
|
+
});
|
|
450
|
+
|
|
418
451
|
it('it should apply private attributes from the context to the context.', () => {
|
|
419
452
|
const uf = ContextFilter({});
|
|
420
453
|
expect(uf.filter(orgAndUserContext)).toEqual(orgAndUserContextIncludedPrivate);
|
|
@@ -10,7 +10,10 @@ import { MockEventSender } from './testUtils';
|
|
|
10
10
|
// tests; here, we use a mock EventSender.
|
|
11
11
|
|
|
12
12
|
describe.each([
|
|
13
|
-
[
|
|
13
|
+
[
|
|
14
|
+
{ key: 'userKey', name: 'Red' },
|
|
15
|
+
{ key: 'userKey', kind: 'user', _meta: { redactedAttributes: ['/name'] } },
|
|
16
|
+
],
|
|
14
17
|
[
|
|
15
18
|
{ kind: 'user', key: 'userKey', name: 'Red' },
|
|
16
19
|
{ key: 'userKey', kind: 'user', _meta: { redactedAttributes: ['/name'] } },
|
|
@@ -75,7 +78,7 @@ describe.each([
|
|
|
75
78
|
expect(e.value).toEqual(source.value);
|
|
76
79
|
expect(e.default).toEqual(source.default);
|
|
77
80
|
expect(e.reason).toEqual(source.reason);
|
|
78
|
-
|
|
81
|
+
expect(e.context).toEqual(inlineUser);
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
function checkCustomEvent(e, source) {
|
|
@@ -136,7 +139,7 @@ describe.each([
|
|
|
136
139
|
expect(mockEventSender.calls.length()).toEqual(1);
|
|
137
140
|
const output = (await mockEventSender.calls.take()).events;
|
|
138
141
|
expect(output.length).toEqual(2);
|
|
139
|
-
checkFeatureEvent(output[0], event, false);
|
|
142
|
+
checkFeatureEvent(output[0], event, false, eventContext);
|
|
140
143
|
checkSummaryEvent(output[1]);
|
|
141
144
|
});
|
|
142
145
|
});
|
|
@@ -159,7 +162,7 @@ describe.each([
|
|
|
159
162
|
expect(mockEventSender.calls.length()).toEqual(1);
|
|
160
163
|
const output = (await mockEventSender.calls.take()).events;
|
|
161
164
|
expect(output.length).toEqual(2);
|
|
162
|
-
checkFeatureEvent(output[0], event, false);
|
|
165
|
+
checkFeatureEvent(output[0], event, false, eventContext);
|
|
163
166
|
checkSummaryEvent(output[1]);
|
|
164
167
|
});
|
|
165
168
|
});
|
|
@@ -236,7 +239,7 @@ describe.each([
|
|
|
236
239
|
expect(mockEventSender.calls.length()).toEqual(1);
|
|
237
240
|
const output = (await mockEventSender.calls.take()).events;
|
|
238
241
|
expect(output.length).toEqual(3);
|
|
239
|
-
checkFeatureEvent(output[0], e, false);
|
|
242
|
+
checkFeatureEvent(output[0], e, false, { ...context, kind: context.kind || 'user' });
|
|
240
243
|
checkFeatureEvent(output[1], e, true, { ...context, kind: context.kind || 'user' });
|
|
241
244
|
checkSummaryEvent(output[2]);
|
|
242
245
|
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import EventSender from '../EventSender';
|
|
2
2
|
import * as utils from '../utils';
|
|
3
3
|
|
|
4
|
-
import * as base64 from 'base64-js';
|
|
5
|
-
|
|
6
4
|
import { respond, networkError } from './mockHttp';
|
|
7
5
|
import * as stubPlatform from './stubPlatform';
|
|
8
6
|
|
|
@@ -20,74 +18,6 @@ describe('EventSender', () => {
|
|
|
20
18
|
platform = stubPlatform.defaults();
|
|
21
19
|
});
|
|
22
20
|
|
|
23
|
-
function fakeImageCreator() {
|
|
24
|
-
const ret = function(url) {
|
|
25
|
-
ret.urls.push(url);
|
|
26
|
-
};
|
|
27
|
-
ret.urls = [];
|
|
28
|
-
return ret;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function base64URLDecode(str) {
|
|
32
|
-
let s = str;
|
|
33
|
-
while (s.length % 4 !== 0) {
|
|
34
|
-
s = s + '=';
|
|
35
|
-
}
|
|
36
|
-
s = s.replace(/_/g, '/').replace(/-/g, '+');
|
|
37
|
-
const decodedBytes = base64.toByteArray(s);
|
|
38
|
-
const decodedStr = String.fromCharCode.apply(String, decodedBytes);
|
|
39
|
-
return decodeURIComponent(escape(decodedStr));
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function decodeOutputFromUrl(url, baseUrl) {
|
|
43
|
-
const prefix = baseUrl + '/a/' + envId + '.gif?d=';
|
|
44
|
-
if (!url.startsWith(prefix)) {
|
|
45
|
-
throw 'URL "' + url + '" did not have expected prefix "' + prefix + '"';
|
|
46
|
-
}
|
|
47
|
-
return JSON.parse(base64URLDecode(url.substring(prefix.length)));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('using image endpoint when CORS is not available', () => {
|
|
51
|
-
it('should encode events in a single chunk if they fit', async () => {
|
|
52
|
-
const server = platform.testing.http.newServer();
|
|
53
|
-
const imageCreator = fakeImageCreator();
|
|
54
|
-
const platformWithoutCors = { ...platform, httpAllowsPost: () => false, httpFallbackPing: imageCreator };
|
|
55
|
-
const sender = EventSender(platformWithoutCors, envId);
|
|
56
|
-
const event1 = { kind: 'identify', key: 'userKey1' };
|
|
57
|
-
const event2 = { kind: 'identify', key: 'userKey2' };
|
|
58
|
-
const events = [event1, event2];
|
|
59
|
-
|
|
60
|
-
await sender.sendEvents(events, server.url);
|
|
61
|
-
|
|
62
|
-
const urls = imageCreator.urls;
|
|
63
|
-
expect(urls.length).toEqual(1);
|
|
64
|
-
expect(decodeOutputFromUrl(urls[0], server.url)).toEqual(events);
|
|
65
|
-
|
|
66
|
-
expect(server.requests.length()).toEqual(0);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should send events in multiple chunks if necessary', async () => {
|
|
70
|
-
const server = platform.testing.http.newServer();
|
|
71
|
-
const imageCreator = fakeImageCreator();
|
|
72
|
-
const platformWithoutCors = { ...platform, httpAllowsPost: () => false, httpFallbackPing: imageCreator };
|
|
73
|
-
const sender = EventSender(platformWithoutCors, envId);
|
|
74
|
-
const events = [];
|
|
75
|
-
for (let i = 0; i < 80; i++) {
|
|
76
|
-
events.push({ kind: 'identify', key: 'thisIsALongUserKey' + i });
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
await sender.sendEvents(events, server.url);
|
|
80
|
-
|
|
81
|
-
const urls = imageCreator.urls;
|
|
82
|
-
expect(urls.length).toEqual(3);
|
|
83
|
-
expect(decodeOutputFromUrl(urls[0], server.url)).toEqual(events.slice(0, 31));
|
|
84
|
-
expect(decodeOutputFromUrl(urls[1], server.url)).toEqual(events.slice(31, 61));
|
|
85
|
-
expect(decodeOutputFromUrl(urls[2], server.url)).toEqual(events.slice(61, 80));
|
|
86
|
-
|
|
87
|
-
expect(server.requests.length()).toEqual(0);
|
|
88
|
-
});
|
|
89
|
-
});
|
|
90
|
-
|
|
91
21
|
describe('using POST when CORS is available', () => {
|
|
92
22
|
it('should send all events in request body', async () => {
|
|
93
23
|
const server = platform.testing.http.newServer();
|
|
@@ -239,6 +169,20 @@ describe('EventSender', () => {
|
|
|
239
169
|
});
|
|
240
170
|
});
|
|
241
171
|
|
|
172
|
+
describe('verify sendEvents response format', () => {
|
|
173
|
+
it('includes date header', async () => {
|
|
174
|
+
const options = { sendLDHeaders: true };
|
|
175
|
+
const server = platform.testing.http.newServer();
|
|
176
|
+
server.byDefault(respond(202, { date: 'Wed, 21 Oct 2015 07:28:00 GMT' }, '{}'));
|
|
177
|
+
|
|
178
|
+
const sender = EventSender(platform, envId, options);
|
|
179
|
+
const event = { kind: 'identify', key: 'userKey' };
|
|
180
|
+
const responseInfo = await sender.sendEvents([event], server.url);
|
|
181
|
+
|
|
182
|
+
expect(responseInfo.serverTime).toEqual(1445412480000);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
242
186
|
describe('When HTTP requests are not available at all', () => {
|
|
243
187
|
it('should silently discard events', async () => {
|
|
244
188
|
const server = platform.testing.http.newServer();
|
|
@@ -99,7 +99,10 @@ describe('EventSummarizer', () => {
|
|
|
99
99
|
key1: {
|
|
100
100
|
contextKinds: ['user'],
|
|
101
101
|
default: 111,
|
|
102
|
-
counters: [
|
|
102
|
+
counters: [
|
|
103
|
+
{ variation: 0, value: 100, version: 11, count: 1 },
|
|
104
|
+
{ value: 111, version: 11, count: 2 },
|
|
105
|
+
],
|
|
103
106
|
},
|
|
104
107
|
};
|
|
105
108
|
expect(data.features).toEqual(expectedFeatures);
|