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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchdarkly-js-sdk-common",
3
- "version": "4.0.3",
3
+ "version": "4.1.0",
4
4
  "description": "LaunchDarkly SDK for JavaScript - common code",
5
5
  "author": "LaunchDarkly <team@launchdarkly.com>",
6
6
  "license": "Apache-2.0",
@@ -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 + '/events/bulk/' + environmentId;
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;
@@ -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' }, utils.getLDHeaders(platform, options));
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, utils.transformHeaders(headers, options), jsonBody)
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 = utils.getLDHeaders(platform, options);
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, utils.transformHeaders(headers, options), body);
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 + path, null);
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 { base64URLEncode, getLDHeaders, transformHeaders, objectHasOwnProperty } = require('./utils');
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 + '/eval/' + environment;
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 + '/ping/' + environment;
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 '../utils';
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();
@@ -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
- if (expectedType !== 'any') {
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
  };
@@ -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 + '/events/diagnostic/' + environmentId;
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
- * Currently these are used to track what version of the SDK is active. This defaults to true
119
- * (custom headers will be sent). One reason you might want to set it to false is that the presence
120
- * of custom headers causes browsers to make an extra OPTIONS request (a CORS preflight check)
121
- * before each flag request, which could affect performance.
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
  /**