launchdarkly-js-sdk-common 4.0.3 → 4.1.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 +8 -0
- package/package.json +1 -1
- package/src/EventProcessor.js +1 -1
- package/src/EventSender.js +3 -2
- package/src/Requestor.js +4 -3
- package/src/Stream.js +4 -3
- package/src/__tests__/Stream-test.js +1 -1
- package/src/__tests__/configuration-test.js +39 -0
- package/src/__tests__/headers-test.js +117 -0
- package/src/__tests__/utils-test.js +8 -73
- package/src/configuration.js +62 -1
- package/src/diagnosticEvents.js +2 -1
- package/src/headers.js +38 -0
- package/src/messages.js +3 -0
- package/src/utils.js +8 -23
- package/test-types.ts +5 -1
- package/typings.d.ts +33 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
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
|
+
## [4.0.3] - 2022-02-16
|
|
6
|
+
### Fixed:
|
|
7
|
+
- If the SDK receives invalid JSON data from a streaming connection (possibly as a result of the connection being cut off), it now uses its regular error-handling logic: the error is emitted as an `error` event or, if there are no `error` event listeners, it is logged. Previously, it would be thrown as an unhandled exception.
|
|
8
|
+
|
|
5
9
|
## [4.0.2] - 2022-01-25
|
|
6
10
|
### Removed:
|
|
7
11
|
- Removed the `version` export which was originally a constant inserted by the Rollup build, but was no longer usable since Rollup is no longer being used. The SDKs never used this export, since they have `version` properties of their own; the version string of `launchdarkly-js-sdk-common` was never meant to be exposed to applications.
|
|
@@ -19,6 +23,10 @@ All notable changes to the `launchdarkly-js-sdk-common` package will be document
|
|
|
19
23
|
- Removed the type `NonNullableLDEvaluationReason`, which was a side effect of the `LDEvaluationDetail.reason` being incorrectly defined before.
|
|
20
24
|
- Removed all types, properties, and functions that were deprecated as of the last 3.x release.
|
|
21
25
|
|
|
26
|
+
## [3.5.1] - 2022-02-17
|
|
27
|
+
### Fixed:
|
|
28
|
+
- If the SDK receives invalid JSON data from a streaming connection (possibly as a result of the connection being cut off), it now uses its regular error-handling logic: the error is emitted as an `error` event or, if there are no `error` event listeners, it is logged. Previously, it would be thrown as an unhandled exception.
|
|
29
|
+
|
|
22
30
|
## [3.5.0] - 2022-01-14
|
|
23
31
|
### Added:
|
|
24
32
|
- New configurable logger factory `commonBasicLogger` and `BasicLoggerOptions`. The `commonBasicLogger` method is not intended to be exported directly in the SDKs, but wrapped to provide platform-specific behavior.
|
package/package.json
CHANGED
package/src/EventProcessor.js
CHANGED
|
@@ -15,7 +15,7 @@ function EventProcessor(
|
|
|
15
15
|
) {
|
|
16
16
|
const processor = {};
|
|
17
17
|
const eventSender = sender || EventSender(platform, environmentId, options);
|
|
18
|
-
const mainEventsUrl = options.eventsUrl
|
|
18
|
+
const mainEventsUrl = utils.appendUrlPath(options.eventsUrl, '/events/bulk/' + environmentId);
|
|
19
19
|
const summarizer = EventSummarizer();
|
|
20
20
|
const userFilter = UserFilter(options);
|
|
21
21
|
const inlineUsers = options.inlineUsersInEvents;
|
package/src/EventSender.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
const errors = require('./errors');
|
|
2
2
|
const utils = require('./utils');
|
|
3
3
|
const { v1: uuidv1 } = require('uuid');
|
|
4
|
+
const { getLDHeaders, transformHeaders } = require('./headers');
|
|
4
5
|
|
|
5
6
|
const MAX_URL_LENGTH = 2000;
|
|
6
7
|
|
|
7
8
|
function EventSender(platform, environmentId, options) {
|
|
8
9
|
const imageUrlPath = '/a/' + environmentId + '.gif';
|
|
9
|
-
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' },
|
|
10
|
+
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, getLDHeaders(platform, options));
|
|
10
11
|
const httpFallbackPing = platform.httpFallbackPing; // this will be set for us if we're in the browser SDK
|
|
11
12
|
const sender = {};
|
|
12
13
|
|
|
@@ -34,7 +35,7 @@ function EventSender(platform, environmentId, options) {
|
|
|
34
35
|
'X-LaunchDarkly-Payload-ID': payloadId,
|
|
35
36
|
});
|
|
36
37
|
return platform
|
|
37
|
-
.httpRequest('POST', url,
|
|
38
|
+
.httpRequest('POST', url, transformHeaders(headers, options), jsonBody)
|
|
38
39
|
.promise.then(result => {
|
|
39
40
|
if (!result) {
|
|
40
41
|
// This was a response from a fire-and-forget request, so we won't have a status.
|
package/src/Requestor.js
CHANGED
|
@@ -2,6 +2,7 @@ const utils = require('./utils');
|
|
|
2
2
|
const errors = require('./errors');
|
|
3
3
|
const messages = require('./messages');
|
|
4
4
|
const promiseCoalescer = require('./promiseCoalescer');
|
|
5
|
+
const { transformHeaders, getLDHeaders } = require('./headers');
|
|
5
6
|
|
|
6
7
|
const jsonContentType = 'application/json';
|
|
7
8
|
|
|
@@ -31,7 +32,7 @@ function Requestor(platform, options, environment) {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
const method = body ? 'REPORT' : 'GET';
|
|
34
|
-
const headers =
|
|
35
|
+
const headers = getLDHeaders(platform, options);
|
|
35
36
|
if (body) {
|
|
36
37
|
headers['Content-Type'] = jsonContentType;
|
|
37
38
|
}
|
|
@@ -45,7 +46,7 @@ function Requestor(platform, options, environment) {
|
|
|
45
46
|
activeRequests[endpoint] = coalescer;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
const req = platform.httpRequest(method, endpoint,
|
|
49
|
+
const req = platform.httpRequest(method, endpoint, transformHeaders(headers, options), body);
|
|
49
50
|
const p = req.promise.then(
|
|
50
51
|
result => {
|
|
51
52
|
if (result.status === 200) {
|
|
@@ -75,7 +76,7 @@ function Requestor(platform, options, environment) {
|
|
|
75
76
|
// Performs a GET request to an arbitrary path under baseUrl. Returns a Promise which will resolve
|
|
76
77
|
// with the parsed JSON response, or will be rejected if the request failed.
|
|
77
78
|
requestor.fetchJSON = function(path) {
|
|
78
|
-
return fetchJSON(baseUrl
|
|
79
|
+
return fetchJSON(utils.appendUrlPath(baseUrl, path), null);
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
// Requests the current state of all flags for the given user from LaunchDarkly. Returns a Promise
|
package/src/Stream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const messages = require('./messages');
|
|
2
|
-
const {
|
|
2
|
+
const { appendUrlPath, base64URLEncode, objectHasOwnProperty } = require('./utils');
|
|
3
|
+
const { getLDHeaders, transformHeaders } = require('./headers');
|
|
3
4
|
|
|
4
5
|
// The underlying event source implementation is abstracted via the platform object, which should
|
|
5
6
|
// have these three properties:
|
|
@@ -20,7 +21,7 @@ function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
|
20
21
|
const baseUrl = config.streamUrl;
|
|
21
22
|
const logger = config.logger;
|
|
22
23
|
const stream = {};
|
|
23
|
-
const evalUrlPrefix = baseUrl
|
|
24
|
+
const evalUrlPrefix = appendUrlPath(baseUrl, '/eval/' + environment);
|
|
24
25
|
const useReport = config.useReport;
|
|
25
26
|
const withReasons = config.evaluationReasons;
|
|
26
27
|
const streamReconnectDelay = config.streamReconnectDelay;
|
|
@@ -98,7 +99,7 @@ function Stream(platform, config, environment, diagnosticsAccumulator) {
|
|
|
98
99
|
options.body = JSON.stringify(user);
|
|
99
100
|
} else {
|
|
100
101
|
// if we can't do REPORT, fall back to the old ping-based stream
|
|
101
|
-
url = baseUrl
|
|
102
|
+
url = appendUrlPath(baseUrl, '/ping/' + environment);
|
|
102
103
|
query = '';
|
|
103
104
|
}
|
|
104
105
|
} else {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DiagnosticsAccumulator } from '../diagnosticEvents';
|
|
2
2
|
import * as messages from '../messages';
|
|
3
3
|
import Stream from '../Stream';
|
|
4
|
-
import { getLDHeaders } from '../
|
|
4
|
+
import { getLDHeaders } from '../headers';
|
|
5
5
|
|
|
6
6
|
import { sleepAsync } from 'launchdarkly-js-test-helpers';
|
|
7
7
|
import EventSource from './EventSource-mock';
|
|
@@ -214,4 +214,43 @@ describe('configuration', () => {
|
|
|
214
214
|
expect(config.extraFunctionOption).toBe(fn);
|
|
215
215
|
await listener.expectError(messages.wrongOptionType('extraNumericOptionWithoutDefault', 'number', 'string'));
|
|
216
216
|
});
|
|
217
|
+
|
|
218
|
+
it('handles a valid application id', async () => {
|
|
219
|
+
const listener = errorListener();
|
|
220
|
+
const configIn = { application: { id: 'test-application' } };
|
|
221
|
+
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.id).toEqual(
|
|
222
|
+
'test-application'
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('logs a warning with an invalid application id', async () => {
|
|
227
|
+
const listener = errorListener();
|
|
228
|
+
const configIn = { application: { id: 'test #$#$#' } };
|
|
229
|
+
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.id).toBeUndefined();
|
|
230
|
+
await listener.expectWarningOnly(messages.invalidTagValue('application.id'));
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('handles a valid application version', async () => {
|
|
234
|
+
const listener = errorListener();
|
|
235
|
+
const configIn = { application: { version: 'test-version' } };
|
|
236
|
+
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.version).toEqual(
|
|
237
|
+
'test-version'
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('logs a warning with an invalid application version', async () => {
|
|
242
|
+
const listener = errorListener();
|
|
243
|
+
const configIn = { application: { version: 'test #$#$#' } };
|
|
244
|
+
expect(
|
|
245
|
+
configuration.validate(configIn, listener.emitter, null, listener.logger).application.version
|
|
246
|
+
).toBeUndefined();
|
|
247
|
+
await listener.expectWarningOnly(messages.invalidTagValue('application.version'));
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('includes application id and version in tags when present', async () => {
|
|
251
|
+
expect(configuration.getTags({ application: { id: 'test-id', version: 'test-version' } })).toEqual({
|
|
252
|
+
'application-id': ['test-id'],
|
|
253
|
+
'application-version': ['test-version'],
|
|
254
|
+
});
|
|
255
|
+
});
|
|
217
256
|
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { getLDHeaders, transformHeaders } from '../headers';
|
|
2
|
+
import { getLDUserAgentString } from '../utils';
|
|
3
|
+
import * as stubPlatform from './stubPlatform';
|
|
4
|
+
|
|
5
|
+
describe('getLDHeaders', () => {
|
|
6
|
+
it('sends no headers unless sendLDHeaders is true', () => {
|
|
7
|
+
const platform = stubPlatform.defaults();
|
|
8
|
+
const headers = getLDHeaders(platform, {});
|
|
9
|
+
expect(headers).toEqual({});
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('adds user-agent header', () => {
|
|
13
|
+
const platform = stubPlatform.defaults();
|
|
14
|
+
const headers = getLDHeaders(platform, { sendLDHeaders: true });
|
|
15
|
+
expect(headers).toMatchObject({ 'User-Agent': getLDUserAgentString(platform) });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('adds user-agent header with custom name', () => {
|
|
19
|
+
const platform = stubPlatform.defaults();
|
|
20
|
+
platform.userAgentHeaderName = 'X-Fake-User-Agent';
|
|
21
|
+
const headers = getLDHeaders(platform, { sendLDHeaders: true });
|
|
22
|
+
expect(headers).toMatchObject({ 'X-Fake-User-Agent': getLDUserAgentString(platform) });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('adds wrapper info if specified, without version', () => {
|
|
26
|
+
const platform = stubPlatform.defaults();
|
|
27
|
+
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK' });
|
|
28
|
+
expect(headers).toMatchObject({
|
|
29
|
+
'User-Agent': getLDUserAgentString(platform),
|
|
30
|
+
'X-LaunchDarkly-Wrapper': 'FakeSDK',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('adds wrapper info if specified, with version', () => {
|
|
35
|
+
const platform = stubPlatform.defaults();
|
|
36
|
+
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK', wrapperVersion: '9.9' });
|
|
37
|
+
expect(headers).toMatchObject({
|
|
38
|
+
'User-Agent': getLDUserAgentString(platform),
|
|
39
|
+
'X-LaunchDarkly-Wrapper': 'FakeSDK/9.9',
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('sets the X-LaunchDarkly-Tags header with valid id and version.', () => {
|
|
44
|
+
const platform = stubPlatform.defaults();
|
|
45
|
+
const headers = getLDHeaders(platform, {
|
|
46
|
+
sendLDHeaders: true,
|
|
47
|
+
application: {
|
|
48
|
+
id: 'test-application',
|
|
49
|
+
version: 'test-version',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
expect(headers).toMatchObject({
|
|
53
|
+
'User-Agent': getLDUserAgentString(platform),
|
|
54
|
+
'x-launchdarkly-tags': 'application-id/test-application application-version/test-version',
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('sets the X-LaunchDarkly-Tags header with just application id', () => {
|
|
59
|
+
const platform = stubPlatform.defaults();
|
|
60
|
+
const headers = getLDHeaders(platform, {
|
|
61
|
+
sendLDHeaders: true,
|
|
62
|
+
application: {
|
|
63
|
+
id: 'test-application',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
expect(headers).toMatchObject({
|
|
67
|
+
'User-Agent': getLDUserAgentString(platform),
|
|
68
|
+
'x-launchdarkly-tags': 'application-id/test-application',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('sets the X-LaunchDarkly-Tags header with just application version.', () => {
|
|
73
|
+
const platform = stubPlatform.defaults();
|
|
74
|
+
const headers = getLDHeaders(platform, {
|
|
75
|
+
sendLDHeaders: true,
|
|
76
|
+
application: {
|
|
77
|
+
version: 'test-version',
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
expect(headers).toMatchObject({
|
|
81
|
+
'User-Agent': getLDUserAgentString(platform),
|
|
82
|
+
'x-launchdarkly-tags': 'application-version/test-version',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe('transformHeaders', () => {
|
|
88
|
+
it('does not modify the headers if the option is not available', () => {
|
|
89
|
+
const inputHeaders = { a: '1', b: '2' };
|
|
90
|
+
const headers = transformHeaders(inputHeaders, {});
|
|
91
|
+
expect(headers).toEqual(inputHeaders);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('modifies the headers if the option has a transform', () => {
|
|
95
|
+
const inputHeaders = { c: '3', d: '4' };
|
|
96
|
+
const outputHeaders = { c: '9', d: '4', e: '5' };
|
|
97
|
+
const headerTransform = input => {
|
|
98
|
+
const output = { ...input };
|
|
99
|
+
output['c'] = '9';
|
|
100
|
+
output['e'] = '5';
|
|
101
|
+
return output;
|
|
102
|
+
};
|
|
103
|
+
const headers = transformHeaders(inputHeaders, { requestHeaderTransform: headerTransform });
|
|
104
|
+
expect(headers).toEqual(outputHeaders);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('cannot mutate the input header object', () => {
|
|
108
|
+
const inputHeaders = { f: '6' };
|
|
109
|
+
const expectedInputHeaders = { f: '6' };
|
|
110
|
+
const headerMutate = input => {
|
|
111
|
+
input['f'] = '7'; // eslint-disable-line no-param-reassign
|
|
112
|
+
return input;
|
|
113
|
+
};
|
|
114
|
+
transformHeaders(inputHeaders, { requestHeaderTransform: headerMutate });
|
|
115
|
+
expect(inputHeaders).toEqual(expectedInputHeaders);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
+
appendUrlPath,
|
|
2
3
|
base64URLEncode,
|
|
3
|
-
getLDHeaders,
|
|
4
|
-
transformHeaders,
|
|
5
4
|
getLDUserAgentString,
|
|
6
5
|
wrapPromiseCallback,
|
|
7
6
|
chunkUserEventsForUrl,
|
|
@@ -10,6 +9,13 @@ import {
|
|
|
10
9
|
import * as stubPlatform from './stubPlatform';
|
|
11
10
|
|
|
12
11
|
describe('utils', () => {
|
|
12
|
+
it('appendUrlPath', () => {
|
|
13
|
+
expect(appendUrlPath('http://base', '/path')).toEqual('http://base/path');
|
|
14
|
+
expect(appendUrlPath('http://base', 'path')).toEqual('http://base/path');
|
|
15
|
+
expect(appendUrlPath('http://base/', '/path')).toEqual('http://base/path');
|
|
16
|
+
expect(appendUrlPath('http://base/', '/path')).toEqual('http://base/path');
|
|
17
|
+
});
|
|
18
|
+
|
|
13
19
|
describe('wrapPromiseCallback', () => {
|
|
14
20
|
it('should resolve to the value', done => {
|
|
15
21
|
const promise = wrapPromiseCallback(Promise.resolve('woohoo'));
|
|
@@ -48,77 +54,6 @@ describe('utils', () => {
|
|
|
48
54
|
});
|
|
49
55
|
});
|
|
50
56
|
|
|
51
|
-
describe('getLDHeaders', () => {
|
|
52
|
-
it('sends no headers unless sendLDHeaders is true', () => {
|
|
53
|
-
const platform = stubPlatform.defaults();
|
|
54
|
-
const headers = getLDHeaders(platform, {});
|
|
55
|
-
expect(headers).toEqual({});
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('adds user-agent header', () => {
|
|
59
|
-
const platform = stubPlatform.defaults();
|
|
60
|
-
const headers = getLDHeaders(platform, { sendLDHeaders: true });
|
|
61
|
-
expect(headers).toMatchObject({ 'User-Agent': getLDUserAgentString(platform) });
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('adds user-agent header with custom name', () => {
|
|
65
|
-
const platform = stubPlatform.defaults();
|
|
66
|
-
platform.userAgentHeaderName = 'X-Fake-User-Agent';
|
|
67
|
-
const headers = getLDHeaders(platform, { sendLDHeaders: true });
|
|
68
|
-
expect(headers).toMatchObject({ 'X-Fake-User-Agent': getLDUserAgentString(platform) });
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('adds wrapper info if specified, without version', () => {
|
|
72
|
-
const platform = stubPlatform.defaults();
|
|
73
|
-
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK' });
|
|
74
|
-
expect(headers).toMatchObject({
|
|
75
|
-
'User-Agent': getLDUserAgentString(platform),
|
|
76
|
-
'X-LaunchDarkly-Wrapper': 'FakeSDK',
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('adds wrapper info if specified, with version', () => {
|
|
81
|
-
const platform = stubPlatform.defaults();
|
|
82
|
-
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK', wrapperVersion: '9.9' });
|
|
83
|
-
expect(headers).toMatchObject({
|
|
84
|
-
'User-Agent': getLDUserAgentString(platform),
|
|
85
|
-
'X-LaunchDarkly-Wrapper': 'FakeSDK/9.9',
|
|
86
|
-
});
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
describe('transformHeaders', () => {
|
|
91
|
-
it('does not modify the headers if the option is not available', () => {
|
|
92
|
-
const inputHeaders = { a: '1', b: '2' };
|
|
93
|
-
const headers = transformHeaders(inputHeaders, {});
|
|
94
|
-
expect(headers).toEqual(inputHeaders);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('modifies the headers if the option has a transform', () => {
|
|
98
|
-
const inputHeaders = { c: '3', d: '4' };
|
|
99
|
-
const outputHeaders = { c: '9', d: '4', e: '5' };
|
|
100
|
-
const headerTransform = input => {
|
|
101
|
-
const output = { ...input };
|
|
102
|
-
output['c'] = '9';
|
|
103
|
-
output['e'] = '5';
|
|
104
|
-
return output;
|
|
105
|
-
};
|
|
106
|
-
const headers = transformHeaders(inputHeaders, { requestHeaderTransform: headerTransform });
|
|
107
|
-
expect(headers).toEqual(outputHeaders);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('cannot mutate the input header object', () => {
|
|
111
|
-
const inputHeaders = { f: '6' };
|
|
112
|
-
const expectedInputHeaders = { f: '6' };
|
|
113
|
-
const headerMutate = input => {
|
|
114
|
-
input['f'] = '7'; // eslint-disable-line no-param-reassign
|
|
115
|
-
return input;
|
|
116
|
-
};
|
|
117
|
-
transformHeaders(inputHeaders, { requestHeaderTransform: headerMutate });
|
|
118
|
-
expect(inputHeaders).toEqual(expectedInputHeaders);
|
|
119
|
-
});
|
|
120
|
-
});
|
|
121
|
-
|
|
122
57
|
describe('getLDUserAgentString', () => {
|
|
123
58
|
it('uses platform user-agent and unknown version by default', () => {
|
|
124
59
|
const platform = stubPlatform.defaults();
|
package/src/configuration.js
CHANGED
|
@@ -38,8 +38,38 @@ const baseOptionDefs = {
|
|
|
38
38
|
wrapperVersion: { type: 'string' },
|
|
39
39
|
stateProvider: { type: 'object' }, // not a public option, used internally
|
|
40
40
|
autoAliasingOptOut: { default: false },
|
|
41
|
+
application: { validator: applicationConfigValidator },
|
|
41
42
|
};
|
|
42
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Expression to validate characters that are allowed in tag keys and values.
|
|
46
|
+
*/
|
|
47
|
+
const allowedTagCharacters = /^(\w|\.|-)+$/;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify that a value meets the requirements for a tag value.
|
|
51
|
+
* @param {Object} config
|
|
52
|
+
* @param {string} tagValue
|
|
53
|
+
*/
|
|
54
|
+
function validateTagValue(name, config, tagValue, logger) {
|
|
55
|
+
if (typeof tagValue !== 'string' || !tagValue.match(allowedTagCharacters)) {
|
|
56
|
+
logger.warn(messages.invalidTagValue(name));
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
return tagValue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function applicationConfigValidator(name, config, value, logger) {
|
|
63
|
+
const validated = {};
|
|
64
|
+
if (value.id) {
|
|
65
|
+
validated.id = validateTagValue(`${name}.id`, config, value.id, logger);
|
|
66
|
+
}
|
|
67
|
+
if (value.version) {
|
|
68
|
+
validated.version = validateTagValue(`${name}.version`, config, value.version, logger);
|
|
69
|
+
}
|
|
70
|
+
return validated;
|
|
71
|
+
}
|
|
72
|
+
|
|
43
73
|
function validate(options, emitter, extraOptionDefs, logger) {
|
|
44
74
|
const optionDefs = utils.extend({ logger: { default: logger } }, baseOptionDefs, extraOptionDefs);
|
|
45
75
|
|
|
@@ -104,7 +134,15 @@ function validate(options, emitter, extraOptionDefs, logger) {
|
|
|
104
134
|
reportArgumentError(messages.unknownOption(name));
|
|
105
135
|
} else {
|
|
106
136
|
const expectedType = optionDef.type || typeDescForValue(optionDef.default);
|
|
107
|
-
|
|
137
|
+
const validator = optionDef.validator;
|
|
138
|
+
if (validator) {
|
|
139
|
+
const validated = validator(name, config, config[name], logger);
|
|
140
|
+
if (validated !== undefined) {
|
|
141
|
+
ret[name] = validated;
|
|
142
|
+
} else {
|
|
143
|
+
delete ret[name];
|
|
144
|
+
}
|
|
145
|
+
} else if (expectedType !== 'any') {
|
|
108
146
|
const allowedTypes = expectedType.split('|');
|
|
109
147
|
const actualType = typeDescForValue(value);
|
|
110
148
|
if (allowedTypes.indexOf(actualType) < 0) {
|
|
@@ -145,7 +183,30 @@ function validate(options, emitter, extraOptionDefs, logger) {
|
|
|
145
183
|
return config;
|
|
146
184
|
}
|
|
147
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Get tags for the specified configuration.
|
|
188
|
+
*
|
|
189
|
+
* If any additional tags are added to the configuration, then the tags from
|
|
190
|
+
* this method should be extended with those.
|
|
191
|
+
* @param {Object} config The already valiated configuration.
|
|
192
|
+
* @returns {Object} The tag configuration.
|
|
193
|
+
*/
|
|
194
|
+
function getTags(config) {
|
|
195
|
+
const tags = {};
|
|
196
|
+
if (config) {
|
|
197
|
+
if (config.application && config.application.id !== undefined && config.application.id !== null) {
|
|
198
|
+
tags['application-id'] = [config.application.id];
|
|
199
|
+
}
|
|
200
|
+
if (config.application && config.application.version !== undefined && config.application.id !== null) {
|
|
201
|
+
tags['application-version'] = [config.application.version];
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return tags;
|
|
206
|
+
}
|
|
207
|
+
|
|
148
208
|
module.exports = {
|
|
149
209
|
baseOptionDefs,
|
|
150
210
|
validate,
|
|
211
|
+
getTags,
|
|
151
212
|
};
|
package/src/diagnosticEvents.js
CHANGED
|
@@ -5,6 +5,7 @@ const { v1: uuidv1 } = require('uuid');
|
|
|
5
5
|
|
|
6
6
|
const { baseOptionDefs } = require('./configuration');
|
|
7
7
|
const messages = require('./messages');
|
|
8
|
+
const { appendUrlPath } = require('./utils');
|
|
8
9
|
|
|
9
10
|
function DiagnosticId(sdkKey) {
|
|
10
11
|
const ret = {
|
|
@@ -80,7 +81,7 @@ function DiagnosticsManager(
|
|
|
80
81
|
) {
|
|
81
82
|
const combinedMode = !!platform.diagnosticUseCombinedEvent;
|
|
82
83
|
const localStorageKey = 'ld:' + environmentId + ':$diagnostics';
|
|
83
|
-
const diagnosticEventsUrl = config.eventsUrl
|
|
84
|
+
const diagnosticEventsUrl = appendUrlPath(config.eventsUrl, '/events/diagnostic/' + environmentId);
|
|
84
85
|
const periodicInterval = config.diagnosticRecordingInterval;
|
|
85
86
|
const acc = accumulator;
|
|
86
87
|
const initialEventSamplingInterval = 4; // used only in combined mode - see start()
|
package/src/headers.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { getLDUserAgentString } = require('./utils');
|
|
2
|
+
const configuration = require('./configuration');
|
|
3
|
+
|
|
4
|
+
function getLDHeaders(platform, options) {
|
|
5
|
+
if (options && !options.sendLDHeaders) {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
const h = {};
|
|
9
|
+
h[platform.userAgentHeaderName || 'User-Agent'] = getLDUserAgentString(platform);
|
|
10
|
+
if (options && options.wrapperName) {
|
|
11
|
+
h['X-LaunchDarkly-Wrapper'] = options.wrapperVersion
|
|
12
|
+
? options.wrapperName + '/' + options.wrapperVersion
|
|
13
|
+
: options.wrapperName;
|
|
14
|
+
}
|
|
15
|
+
const tags = configuration.getTags(options);
|
|
16
|
+
const tagKeys = Object.keys(tags);
|
|
17
|
+
if (tagKeys.length) {
|
|
18
|
+
h['x-launchdarkly-tags'] = tagKeys
|
|
19
|
+
.sort()
|
|
20
|
+
.flatMap(
|
|
21
|
+
key => (Array.isArray(tags[key]) ? tags[key].sort().map(value => `${key}/${value}`) : [`${key}/${tags[key]}`])
|
|
22
|
+
)
|
|
23
|
+
.join(' ');
|
|
24
|
+
}
|
|
25
|
+
return h;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function transformHeaders(headers, options) {
|
|
29
|
+
if (!options || !options.requestHeaderTransform) {
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
return options.requestHeaderTransform({ ...headers });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = {
|
|
36
|
+
getLDHeaders,
|
|
37
|
+
transformHeaders,
|
|
38
|
+
};
|
package/src/messages.js
CHANGED
|
@@ -180,6 +180,8 @@ const debugPostingDiagnosticEvent = function(event) {
|
|
|
180
180
|
return 'sending diagnostic event (' + event.kind + ')';
|
|
181
181
|
};
|
|
182
182
|
|
|
183
|
+
const invalidTagValue = name => `Config option "${name}" must only contain letters, numbers, ., _ or -.`;
|
|
184
|
+
|
|
183
185
|
module.exports = {
|
|
184
186
|
bootstrapInvalid,
|
|
185
187
|
bootstrapOldFormat,
|
|
@@ -207,6 +209,7 @@ module.exports = {
|
|
|
207
209
|
invalidContentType,
|
|
208
210
|
invalidData,
|
|
209
211
|
invalidKey,
|
|
212
|
+
invalidTagValue,
|
|
210
213
|
invalidUser,
|
|
211
214
|
localStorageUnavailable,
|
|
212
215
|
networkError,
|
package/src/utils.js
CHANGED
|
@@ -3,6 +3,13 @@ const fastDeepEqual = require('fast-deep-equal');
|
|
|
3
3
|
|
|
4
4
|
const userAttrsToStringify = ['key', 'secondary', 'ip', 'country', 'email', 'firstName', 'lastName', 'avatar', 'name'];
|
|
5
5
|
|
|
6
|
+
function appendUrlPath(baseUrl, path) {
|
|
7
|
+
// Ensure that URL concatenation is done correctly regardless of whether the
|
|
8
|
+
// base URL has a trailing slash or not.
|
|
9
|
+
const trimBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl;
|
|
10
|
+
return trimBaseUrl + (path.startsWith('/') ? '' : '/') + path;
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
// See http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html
|
|
7
14
|
function btoa(s) {
|
|
8
15
|
const escaped = unescape(encodeURIComponent(s));
|
|
@@ -150,27 +157,6 @@ function getLDUserAgentString(platform) {
|
|
|
150
157
|
return platform.userAgent + '/' + version;
|
|
151
158
|
}
|
|
152
159
|
|
|
153
|
-
function getLDHeaders(platform, options) {
|
|
154
|
-
if (options && !options.sendLDHeaders) {
|
|
155
|
-
return {};
|
|
156
|
-
}
|
|
157
|
-
const h = {};
|
|
158
|
-
h[platform.userAgentHeaderName || 'User-Agent'] = getLDUserAgentString(platform);
|
|
159
|
-
if (options && options.wrapperName) {
|
|
160
|
-
h['X-LaunchDarkly-Wrapper'] = options.wrapperVersion
|
|
161
|
-
? options.wrapperName + '/' + options.wrapperVersion
|
|
162
|
-
: options.wrapperName;
|
|
163
|
-
}
|
|
164
|
-
return h;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function transformHeaders(headers, options) {
|
|
168
|
-
if (!options || !options.requestHeaderTransform) {
|
|
169
|
-
return headers;
|
|
170
|
-
}
|
|
171
|
-
return options.requestHeaderTransform({ ...headers });
|
|
172
|
-
}
|
|
173
|
-
|
|
174
160
|
function extend(...objects) {
|
|
175
161
|
return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {});
|
|
176
162
|
}
|
|
@@ -196,18 +182,17 @@ function sanitizeUser(user) {
|
|
|
196
182
|
}
|
|
197
183
|
|
|
198
184
|
module.exports = {
|
|
185
|
+
appendUrlPath,
|
|
199
186
|
base64URLEncode,
|
|
200
187
|
btoa,
|
|
201
188
|
chunkUserEventsForUrl,
|
|
202
189
|
clone,
|
|
203
190
|
deepEquals,
|
|
204
191
|
extend,
|
|
205
|
-
getLDHeaders,
|
|
206
192
|
getLDUserAgentString,
|
|
207
193
|
objectHasOwnProperty,
|
|
208
194
|
onNextTick,
|
|
209
195
|
sanitizeUser,
|
|
210
|
-
transformHeaders,
|
|
211
196
|
transformValuesToVersionedValues,
|
|
212
197
|
transformVersionedValuesToValues,
|
|
213
198
|
wrapPromiseCallback,
|
package/test-types.ts
CHANGED
|
@@ -47,7 +47,11 @@ var allBaseOptions: ld.LDOptionsBase = {
|
|
|
47
47
|
sendEventsOnlyForVariation: true,
|
|
48
48
|
flushInterval: 1,
|
|
49
49
|
streamReconnectDelay: 1,
|
|
50
|
-
logger: logger
|
|
50
|
+
logger: logger,
|
|
51
|
+
application: {
|
|
52
|
+
version: 'version',
|
|
53
|
+
id: 'id'
|
|
54
|
+
}
|
|
51
55
|
};
|
|
52
56
|
|
|
53
57
|
var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile
|
package/typings.d.ts
CHANGED
|
@@ -115,10 +115,14 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
115
115
|
/**
|
|
116
116
|
* Whether or not to include custom HTTP headers when requesting flags from LaunchDarkly.
|
|
117
117
|
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
118
|
+
* These are used to send metadata about the SDK (such as the version). They
|
|
119
|
+
* are also used to send the application.id and application.version set in
|
|
120
|
+
* the options.
|
|
121
|
+
*
|
|
122
|
+
* This defaults to true (custom headers will be sent). One reason you might
|
|
123
|
+
* want to set it to false is that the presence of custom headers causes
|
|
124
|
+
* browsers to make an extra OPTIONS request (a CORS preflight check) before
|
|
125
|
+
* each flag request, which could affect performance.
|
|
122
126
|
*/
|
|
123
127
|
sendLDHeaders?: boolean;
|
|
124
128
|
|
|
@@ -255,6 +259,31 @@ declare module 'launchdarkly-js-sdk-common' {
|
|
|
255
259
|
* The default value is `false`.
|
|
256
260
|
*/
|
|
257
261
|
autoAliasingOptOut?: boolean;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Information about the application where the LaunchDarkly SDK is running.
|
|
265
|
+
*/
|
|
266
|
+
application?: {
|
|
267
|
+
/**
|
|
268
|
+
* A unique identifier representing the application where the LaunchDarkly SDK is running.
|
|
269
|
+
*
|
|
270
|
+
* This can be specified as any string value as long as it only uses the following characters: ASCII letters,
|
|
271
|
+
* ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored.
|
|
272
|
+
*
|
|
273
|
+
* Example: `authentication-service`
|
|
274
|
+
*/
|
|
275
|
+
id?: string;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* A unique identifier representing the version of the application where the LaunchDarkly SDK is running.
|
|
279
|
+
*
|
|
280
|
+
* This can be specified as any string value as long as it only uses the following characters: ASCII letters,
|
|
281
|
+
* ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored.
|
|
282
|
+
*
|
|
283
|
+
* Example: `1.0.0` (standard version string) or `abcdef` (sha prefix)
|
|
284
|
+
*/
|
|
285
|
+
version?: string;
|
|
286
|
+
}
|
|
258
287
|
}
|
|
259
288
|
|
|
260
289
|
/**
|