launchdarkly-js-sdk-common
Advanced tools
Comparing version 4.0.3 to 4.1.0
@@ -5,2 +5,6 @@ # Change log | ||
## [4.0.3] - 2022-02-16 | ||
### Fixed: | ||
- 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. | ||
## [4.0.2] - 2022-01-25 | ||
@@ -23,2 +27,6 @@ ### Removed: | ||
## [3.5.1] - 2022-02-17 | ||
### Fixed: | ||
- 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. | ||
## [3.5.0] - 2022-01-14 | ||
@@ -25,0 +33,0 @@ ### Added: |
{ | ||
"name": "launchdarkly-js-sdk-common", | ||
"version": "4.0.3", | ||
"version": "4.1.0", | ||
"description": "LaunchDarkly SDK for JavaScript - common code", | ||
@@ -5,0 +5,0 @@ "author": "LaunchDarkly <team@launchdarkly.com>", |
@@ -217,2 +217,41 @@ import { sleepAsync, eventSink } from 'launchdarkly-js-test-helpers'; | ||
}); | ||
it('handles a valid application id', async () => { | ||
const listener = errorListener(); | ||
const configIn = { application: { id: 'test-application' } }; | ||
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.id).toEqual( | ||
'test-application' | ||
); | ||
}); | ||
it('logs a warning with an invalid application id', async () => { | ||
const listener = errorListener(); | ||
const configIn = { application: { id: 'test #$#$#' } }; | ||
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.id).toBeUndefined(); | ||
await listener.expectWarningOnly(messages.invalidTagValue('application.id')); | ||
}); | ||
it('handles a valid application version', async () => { | ||
const listener = errorListener(); | ||
const configIn = { application: { version: 'test-version' } }; | ||
expect(configuration.validate(configIn, listener.emitter, null, listener.logger).application.version).toEqual( | ||
'test-version' | ||
); | ||
}); | ||
it('logs a warning with an invalid application version', async () => { | ||
const listener = errorListener(); | ||
const configIn = { application: { version: 'test #$#$#' } }; | ||
expect( | ||
configuration.validate(configIn, listener.emitter, null, listener.logger).application.version | ||
).toBeUndefined(); | ||
await listener.expectWarningOnly(messages.invalidTagValue('application.version')); | ||
}); | ||
it('includes application id and version in tags when present', async () => { | ||
expect(configuration.getTags({ application: { id: 'test-id', version: 'test-version' } })).toEqual({ | ||
'application-id': ['test-id'], | ||
'application-version': ['test-version'], | ||
}); | ||
}); | ||
}); |
import { DiagnosticsAccumulator } from '../diagnosticEvents'; | ||
import * as messages from '../messages'; | ||
import Stream from '../Stream'; | ||
import { getLDHeaders } from '../utils'; | ||
import { getLDHeaders } from '../headers'; | ||
@@ -6,0 +6,0 @@ import { sleepAsync } from 'launchdarkly-js-test-helpers'; |
import { | ||
appendUrlPath, | ||
base64URLEncode, | ||
getLDHeaders, | ||
transformHeaders, | ||
getLDUserAgentString, | ||
@@ -13,2 +12,9 @@ wrapPromiseCallback, | ||
describe('utils', () => { | ||
it('appendUrlPath', () => { | ||
expect(appendUrlPath('http://base', '/path')).toEqual('http://base/path'); | ||
expect(appendUrlPath('http://base', 'path')).toEqual('http://base/path'); | ||
expect(appendUrlPath('http://base/', '/path')).toEqual('http://base/path'); | ||
expect(appendUrlPath('http://base/', '/path')).toEqual('http://base/path'); | ||
}); | ||
describe('wrapPromiseCallback', () => { | ||
@@ -52,73 +58,2 @@ it('should resolve to the value', done => { | ||
describe('getLDHeaders', () => { | ||
it('sends no headers unless sendLDHeaders is true', () => { | ||
const platform = stubPlatform.defaults(); | ||
const headers = getLDHeaders(platform, {}); | ||
expect(headers).toEqual({}); | ||
}); | ||
it('adds user-agent header', () => { | ||
const platform = stubPlatform.defaults(); | ||
const headers = getLDHeaders(platform, { sendLDHeaders: true }); | ||
expect(headers).toMatchObject({ 'User-Agent': getLDUserAgentString(platform) }); | ||
}); | ||
it('adds user-agent header with custom name', () => { | ||
const platform = stubPlatform.defaults(); | ||
platform.userAgentHeaderName = 'X-Fake-User-Agent'; | ||
const headers = getLDHeaders(platform, { sendLDHeaders: true }); | ||
expect(headers).toMatchObject({ 'X-Fake-User-Agent': getLDUserAgentString(platform) }); | ||
}); | ||
it('adds wrapper info if specified, without version', () => { | ||
const platform = stubPlatform.defaults(); | ||
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK' }); | ||
expect(headers).toMatchObject({ | ||
'User-Agent': getLDUserAgentString(platform), | ||
'X-LaunchDarkly-Wrapper': 'FakeSDK', | ||
}); | ||
}); | ||
it('adds wrapper info if specified, with version', () => { | ||
const platform = stubPlatform.defaults(); | ||
const headers = getLDHeaders(platform, { sendLDHeaders: true, wrapperName: 'FakeSDK', wrapperVersion: '9.9' }); | ||
expect(headers).toMatchObject({ | ||
'User-Agent': getLDUserAgentString(platform), | ||
'X-LaunchDarkly-Wrapper': 'FakeSDK/9.9', | ||
}); | ||
}); | ||
}); | ||
describe('transformHeaders', () => { | ||
it('does not modify the headers if the option is not available', () => { | ||
const inputHeaders = { a: '1', b: '2' }; | ||
const headers = transformHeaders(inputHeaders, {}); | ||
expect(headers).toEqual(inputHeaders); | ||
}); | ||
it('modifies the headers if the option has a transform', () => { | ||
const inputHeaders = { c: '3', d: '4' }; | ||
const outputHeaders = { c: '9', d: '4', e: '5' }; | ||
const headerTransform = input => { | ||
const output = { ...input }; | ||
output['c'] = '9'; | ||
output['e'] = '5'; | ||
return output; | ||
}; | ||
const headers = transformHeaders(inputHeaders, { requestHeaderTransform: headerTransform }); | ||
expect(headers).toEqual(outputHeaders); | ||
}); | ||
it('cannot mutate the input header object', () => { | ||
const inputHeaders = { f: '6' }; | ||
const expectedInputHeaders = { f: '6' }; | ||
const headerMutate = input => { | ||
input['f'] = '7'; // eslint-disable-line no-param-reassign | ||
return input; | ||
}; | ||
transformHeaders(inputHeaders, { requestHeaderTransform: headerMutate }); | ||
expect(inputHeaders).toEqual(expectedInputHeaders); | ||
}); | ||
}); | ||
describe('getLDUserAgentString', () => { | ||
@@ -125,0 +60,0 @@ it('uses platform user-agent and unknown version by default', () => { |
@@ -41,4 +41,34 @@ const errors = require('./errors'); | ||
autoAliasingOptOut: { default: false }, | ||
application: { validator: applicationConfigValidator }, | ||
}; | ||
/** | ||
* Expression to validate characters that are allowed in tag keys and values. | ||
*/ | ||
const allowedTagCharacters = /^(\w|\.|-)+$/; | ||
/** | ||
* Verify that a value meets the requirements for a tag value. | ||
* @param {Object} config | ||
* @param {string} tagValue | ||
*/ | ||
function validateTagValue(name, config, tagValue, logger) { | ||
if (typeof tagValue !== 'string' || !tagValue.match(allowedTagCharacters)) { | ||
logger.warn(messages.invalidTagValue(name)); | ||
return undefined; | ||
} | ||
return tagValue; | ||
} | ||
function applicationConfigValidator(name, config, value, logger) { | ||
const validated = {}; | ||
if (value.id) { | ||
validated.id = validateTagValue(`${name}.id`, config, value.id, logger); | ||
} | ||
if (value.version) { | ||
validated.version = validateTagValue(`${name}.version`, config, value.version, logger); | ||
} | ||
return validated; | ||
} | ||
function validate(options, emitter, extraOptionDefs, logger) { | ||
@@ -108,3 +138,11 @@ const optionDefs = utils.extend({ logger: { default: logger } }, baseOptionDefs, extraOptionDefs); | ||
const expectedType = optionDef.type || typeDescForValue(optionDef.default); | ||
if (expectedType !== 'any') { | ||
const validator = optionDef.validator; | ||
if (validator) { | ||
const validated = validator(name, config, config[name], logger); | ||
if (validated !== undefined) { | ||
ret[name] = validated; | ||
} else { | ||
delete ret[name]; | ||
} | ||
} else if (expectedType !== 'any') { | ||
const allowedTypes = expectedType.split('|'); | ||
@@ -150,5 +188,28 @@ const actualType = typeDescForValue(value); | ||
/** | ||
* Get tags for the specified configuration. | ||
* | ||
* If any additional tags are added to the configuration, then the tags from | ||
* this method should be extended with those. | ||
* @param {Object} config The already valiated configuration. | ||
* @returns {Object} The tag configuration. | ||
*/ | ||
function getTags(config) { | ||
const tags = {}; | ||
if (config) { | ||
if (config.application && config.application.id !== undefined && config.application.id !== null) { | ||
tags['application-id'] = [config.application.id]; | ||
} | ||
if (config.application && config.application.version !== undefined && config.application.id !== null) { | ||
tags['application-version'] = [config.application.version]; | ||
} | ||
} | ||
return tags; | ||
} | ||
module.exports = { | ||
baseOptionDefs, | ||
validate, | ||
getTags, | ||
}; |
@@ -8,2 +8,3 @@ const { v1: uuidv1 } = require('uuid'); | ||
const messages = require('./messages'); | ||
const { appendUrlPath } = require('./utils'); | ||
@@ -84,3 +85,3 @@ function DiagnosticId(sdkKey) { | ||
const localStorageKey = 'ld:' + environmentId + ':$diagnostics'; | ||
const diagnosticEventsUrl = config.eventsUrl + '/events/diagnostic/' + environmentId; | ||
const diagnosticEventsUrl = appendUrlPath(config.eventsUrl, '/events/diagnostic/' + environmentId); | ||
const periodicInterval = config.diagnosticRecordingInterval; | ||
@@ -87,0 +88,0 @@ const acc = accumulator; |
@@ -18,3 +18,3 @@ const EventSender = require('./EventSender'); | ||
const eventSender = sender || EventSender(platform, environmentId, options); | ||
const mainEventsUrl = options.eventsUrl + '/events/bulk/' + environmentId; | ||
const mainEventsUrl = utils.appendUrlPath(options.eventsUrl, '/events/bulk/' + environmentId); | ||
const summarizer = EventSummarizer(); | ||
@@ -21,0 +21,0 @@ const userFilter = UserFilter(options); |
const errors = require('./errors'); | ||
const utils = require('./utils'); | ||
const { v1: uuidv1 } = require('uuid'); | ||
const { getLDHeaders, transformHeaders } = require('./headers'); | ||
@@ -9,3 +10,3 @@ const MAX_URL_LENGTH = 2000; | ||
const imageUrlPath = '/a/' + environmentId + '.gif'; | ||
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, utils.getLDHeaders(platform, options)); | ||
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, getLDHeaders(platform, options)); | ||
const httpFallbackPing = platform.httpFallbackPing; // this will be set for us if we're in the browser SDK | ||
@@ -38,3 +39,3 @@ const sender = {}; | ||
return platform | ||
.httpRequest('POST', url, utils.transformHeaders(headers, options), jsonBody) | ||
.httpRequest('POST', url, transformHeaders(headers, options), jsonBody) | ||
.promise.then(result => { | ||
@@ -41,0 +42,0 @@ if (!result) { |
@@ -183,2 +183,4 @@ const errors = require('./errors'); | ||
const invalidTagValue = name => `Config option "${name}" must only contain letters, numbers, ., _ or -.`; | ||
module.exports = { | ||
@@ -211,2 +213,3 @@ bootstrapInvalid, | ||
invalidKey, | ||
invalidTagValue, | ||
invalidUser, | ||
@@ -213,0 +216,0 @@ localStorageUnavailable, |
@@ -5,2 +5,3 @@ const utils = require('./utils'); | ||
const promiseCoalescer = require('./promiseCoalescer'); | ||
const { transformHeaders, getLDHeaders } = require('./headers'); | ||
@@ -35,3 +36,3 @@ const jsonContentType = 'application/json'; | ||
const method = body ? 'REPORT' : 'GET'; | ||
const headers = utils.getLDHeaders(platform, options); | ||
const headers = getLDHeaders(platform, options); | ||
if (body) { | ||
@@ -50,3 +51,3 @@ headers['Content-Type'] = jsonContentType; | ||
const req = platform.httpRequest(method, endpoint, utils.transformHeaders(headers, options), body); | ||
const req = platform.httpRequest(method, endpoint, transformHeaders(headers, options), body); | ||
const p = req.promise.then( | ||
@@ -81,3 +82,3 @@ result => { | ||
requestor.fetchJSON = function(path) { | ||
return fetchJSON(baseUrl + path, null); | ||
return fetchJSON(utils.appendUrlPath(baseUrl, path), null); | ||
}; | ||
@@ -84,0 +85,0 @@ |
const messages = require('./messages'); | ||
const { base64URLEncode, getLDHeaders, transformHeaders, objectHasOwnProperty } = require('./utils'); | ||
const { appendUrlPath, base64URLEncode, objectHasOwnProperty } = require('./utils'); | ||
const { getLDHeaders, transformHeaders } = require('./headers'); | ||
@@ -23,3 +24,3 @@ // The underlying event source implementation is abstracted via the platform object, which should | ||
const stream = {}; | ||
const evalUrlPrefix = baseUrl + '/eval/' + environment; | ||
const evalUrlPrefix = appendUrlPath(baseUrl, '/eval/' + environment); | ||
const useReport = config.useReport; | ||
@@ -102,3 +103,3 @@ const withReasons = config.evaluationReasons; | ||
// if we can't do REPORT, fall back to the old ping-based stream | ||
url = baseUrl + '/ping/' + environment; | ||
url = appendUrlPath(baseUrl, '/ping/' + environment); | ||
query = ''; | ||
@@ -105,0 +106,0 @@ } |
@@ -6,2 +6,9 @@ const base64 = require('base64-js'); | ||
function appendUrlPath(baseUrl, path) { | ||
// Ensure that URL concatenation is done correctly regardless of whether the | ||
// base URL has a trailing slash or not. | ||
const trimBaseUrl = baseUrl.endsWith('/') ? baseUrl.substring(0, baseUrl.length - 1) : baseUrl; | ||
return trimBaseUrl + (path.startsWith('/') ? '' : '/') + path; | ||
} | ||
// See http://ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html | ||
@@ -154,23 +161,2 @@ function btoa(s) { | ||
function getLDHeaders(platform, options) { | ||
if (options && !options.sendLDHeaders) { | ||
return {}; | ||
} | ||
const h = {}; | ||
h[platform.userAgentHeaderName || 'User-Agent'] = getLDUserAgentString(platform); | ||
if (options && options.wrapperName) { | ||
h['X-LaunchDarkly-Wrapper'] = options.wrapperVersion | ||
? options.wrapperName + '/' + options.wrapperVersion | ||
: options.wrapperName; | ||
} | ||
return h; | ||
} | ||
function transformHeaders(headers, options) { | ||
if (!options || !options.requestHeaderTransform) { | ||
return headers; | ||
} | ||
return options.requestHeaderTransform({ ...headers }); | ||
} | ||
function extend(...objects) { | ||
@@ -201,2 +187,3 @@ return objects.reduce((acc, obj) => ({ ...acc, ...obj }), {}); | ||
module.exports = { | ||
appendUrlPath, | ||
base64URLEncode, | ||
@@ -208,3 +195,2 @@ btoa, | ||
extend, | ||
getLDHeaders, | ||
getLDUserAgentString, | ||
@@ -214,3 +200,2 @@ objectHasOwnProperty, | ||
sanitizeUser, | ||
transformHeaders, | ||
transformValuesToVersionedValues, | ||
@@ -217,0 +202,0 @@ transformVersionedValuesToValues, |
@@ -50,3 +50,7 @@ | ||
streamReconnectDelay: 1, | ||
logger: logger | ||
logger: logger, | ||
application: { | ||
version: 'version', | ||
id: 'id' | ||
} | ||
}; | ||
@@ -53,0 +57,0 @@ |
@@ -118,6 +118,10 @@ /** | ||
* | ||
* Currently these are used to track what version of the SDK is active. This defaults to true | ||
* (custom headers will be sent). One reason you might want to set it to false is that the presence | ||
* of custom headers causes browsers to make an extra OPTIONS request (a CORS preflight check) | ||
* before each flag request, which could affect performance. | ||
* These are used to send metadata about the SDK (such as the version). They | ||
* are also used to send the application.id and application.version set in | ||
* the options. | ||
* | ||
* This defaults to true (custom headers will be sent). One reason you might | ||
* want to set it to false is that the presence of custom headers causes | ||
* browsers to make an extra OPTIONS request (a CORS preflight check) before | ||
* each flag request, which could affect performance. | ||
*/ | ||
@@ -259,2 +263,27 @@ sendLDHeaders?: boolean; | ||
autoAliasingOptOut?: boolean; | ||
/** | ||
* Information about the application where the LaunchDarkly SDK is running. | ||
*/ | ||
application?: { | ||
/** | ||
* A unique identifier representing the application where the LaunchDarkly SDK is running. | ||
* | ||
* This can be specified as any string value as long as it only uses the following characters: ASCII letters, | ||
* ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored. | ||
* | ||
* Example: `authentication-service` | ||
*/ | ||
id?: string; | ||
/** | ||
* A unique identifier representing the version of the application where the LaunchDarkly SDK is running. | ||
* | ||
* This can be specified as any string value as long as it only uses the following characters: ASCII letters, | ||
* ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored. | ||
* | ||
* Example: `1.0.0` (standard version string) or `abcdef` (sha prefix) | ||
*/ | ||
version?: string; | ||
} | ||
} | ||
@@ -261,0 +290,0 @@ |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
375187
66
8471