diff --git a/packages/ldclient-js-common/src/__tests__/LDClient-localstorage-test.js b/packages/ldclient-js-common/src/__tests__/LDClient-localstorage-test.js index 5dd2c2ca..13e0f2c3 100644 --- a/packages/ldclient-js-common/src/__tests__/LDClient-localstorage-test.js +++ b/packages/ldclient-js-common/src/__tests__/LDClient-localstorage-test.js @@ -28,6 +28,18 @@ describe('LDClient local storage', () => { } describe('bootstrapping from local storage', () => { + it('does not try to use local storage if the platform says it is unavailable', () => { + const platform = stubPlatform.defaults(); + platform.localStorage = null; + + platform.testing.makeClient(envName, user, { bootstrap: 'localstorage', fetchGoals: false }); + + // should see a flag request to the server right away, as if bootstrap was not specified + expect(server.requests.length).toEqual(1); + + expect(platform.testing.logger.output.warn).toEqual([messages.localStorageUnavailable()]); + }); + it('uses cached flags if available and requests flags from server after ready', done => { const platform = stubPlatform.defaults(); const json = '{"flag-key": 1}'; diff --git a/packages/ldclient-js-common/src/__tests__/LDClient-streaming-test.js b/packages/ldclient-js-common/src/__tests__/LDClient-streaming-test.js index 42e4f0d3..63496f38 100644 --- a/packages/ldclient-js-common/src/__tests__/LDClient-streaming-test.js +++ b/packages/ldclient-js-common/src/__tests__/LDClient-streaming-test.js @@ -564,6 +564,42 @@ describe('LDClient', () => { }); }); + it('handles delete message for unknown flag by storing placeholder', done => { + const client = platform.testing.makeClient(envName, user, { bootstrap: {} }); + + client.on('ready', () => { + client.on('change', () => {}); + + streamEvents().delete({ + data: '{"key":"mystery","version":3}', + }); + + // The following patch message should be ignored because it has a lower version than the deleted placeholder + streamEvents().patch({ + data: '{"key":"mystery","value":"yes","version":2}', + }); + + expect(client.variation('mystery')).toBeUndefined(); + done(); + }); + }); + + it('ignores delete message with lower version', done => { + const bootstrapData = { flag: 'yes', $flagsState: { flag: { version: 3 } } }; + const client = platform.testing.makeClient(envName, user, { bootstrap: bootstrapData }); + + client.on('ready', () => { + client.on('change', () => {}); + + streamEvents().delete({ + data: '{"key":"flag","version":2}', + }); + + expect(client.variation('flag')).toEqual('yes'); + done(); + }); + }); + it('fires global change event when flag is deleted', done => { const client = platform.testing.makeClient(envName, user, { bootstrap: { 'enable-foo': true } }); diff --git a/packages/ldclient-js-common/src/index.js b/packages/ldclient-js-common/src/index.js index 701fbd63..12cf2e9c 100644 --- a/packages/ldclient-js-common/src/index.js +++ b/packages/ldclient-js-common/src/index.js @@ -518,11 +518,17 @@ export function initialize(env, user, specifiedOptions, platform, extraDefaults) } } + if (typeof options.bootstrap === 'string' && options.bootstrap.toUpperCase() === 'LOCALSTORAGE') { + if (store) { + useLocalStorage = true; + } else { + logger.warn(messages.localStorageUnavailable()); + } + } + if (typeof options.bootstrap === 'object') { utils.onNextTick(signalSuccessfulInit); - } else if (typeof options.bootstrap === 'string' && options.bootstrap.toUpperCase() === 'LOCALSTORAGE' && store) { - useLocalStorage = true; - + } else if (useLocalStorage) { store.loadFlags((err, storedFlags) => { if (storedFlags === null || storedFlags === undefined) { flags = {}; diff --git a/packages/ldclient-js-common/src/messages.js b/packages/ldclient-js-common/src/messages.js index e34a3133..071f0da3 100644 --- a/packages/ldclient-js-common/src/messages.js +++ b/packages/ldclient-js-common/src/messages.js @@ -107,7 +107,7 @@ export const debugStreamDelete = function(key) { return 'received streaming deletion for flag "' + key + '"'; }; -export const debugStreamingDeleteIgnored = function(key) { +export const debugStreamDeleteIgnored = function(key) { return 'received streaming deletion for flag "' + key + '" but ignored due to version check'; }; diff --git a/packages/ldclient-js-common/test-types.ts b/packages/ldclient-js-common/test-types.ts index 634ea9c5..75952bc5 100644 --- a/packages/ldclient-js-common/test-types.ts +++ b/packages/ldclient-js-common/test-types.ts @@ -1,3 +1,45 @@ // This file exists only so that we can run the TypeScript compiler in the CI build // to validate our typings.d.ts file. + +import * as ld from 'ldclient-js-common'; + +var ver: string = ld.version; + +var logger: ld.LDLogger = ld.createConsoleLogger("info"); +var userWithKeyOnly: ld.LDUser = { key: 'user' }; +var user: ld.LDUser = { + key: 'user', + name: 'name', + firstName: 'first', + lastName: 'last', + email: 'test@example.com', + avatar: 'http://avatar.url', + ip: '1.1.1.1', + country: 'us', + anonymous: true, + custom: { + 'a': 's', + 'b': true, + 'c': 3, + 'd': [ 'x', 'y' ], + 'e': [ true, false ], + 'f': [ 1, 2 ] + }, + privateAttributeNames: [ 'name', 'email' ] +}; + +var client: ld.LDClientBase = {} as ld.LDClientBase; // wouldn't do this in real life, it's just so the following statements will compile + +var boolFlagValue: ld.LDFlagValue = client.variation('key', false); +var numberFlagValue: ld.LDFlagValue = client.variation('key', 2); +var stringFlagValue: ld.LDFlagValue = client.variation('key', 'default'); +var jsonFlagValue: ld.LDFlagValue = client.variation('key', [ 'a', 'b' ]); + +var detail: ld.LDEvaluationDetail = client.variationDetail('key', 'default'); +var detailValue: ld.LDFlagValue = detail.value; +var detailIndex: number | undefined = detail.variationIndex; +var detailReason: ld.LDEvaluationReason = detail.reason; + +var flagSet: ld.LDFlagSet = client.allFlags(); +var flagSetValue: ld.LDFlagValue = flagSet['key']; diff --git a/packages/ldclient-js-common/typings.d.ts b/packages/ldclient-js-common/typings.d.ts index d3b13aa3..032ca85c 100644 --- a/packages/ldclient-js-common/typings.d.ts +++ b/packages/ldclient-js-common/typings.d.ts @@ -18,19 +18,19 @@ declare module 'ldclient-js-common' { /** * A map of feature flags from their keys to their values. */ - export type LDFlagSet = { + export interface LDFlagSet { [key: string]: LDFlagValue; - }; + } /** * A map of feature flag keys to objects holding changes in their values. */ - export type LDFlagChangeset = { + export interface LDFlagChangeset { [key: string]: { current: LDFlagValue; previous: LDFlagValue; }; - }; + } /** * The minimal interface for any object that LDClient can use for logging. @@ -154,7 +154,7 @@ declare module 'ldclient-js-common' { /** * Whether all user attributes (except the user key) should be marked as private, and - * not sent to LaunchDarkly. + * not sent to LaunchDarkly in analytics events. * * By default, this is false. */ @@ -162,7 +162,8 @@ declare module 'ldclient-js-common' { /** * The names of user attributes that should be marked as private, and not sent - * to LaunchDarkly. + * to LaunchDarkly in analytics events. You can also specify this on a per-user basis + * with [[LDUser.privateAttributeNames]]. */ privateAttributeNames?: Array; @@ -211,7 +212,7 @@ declare module 'ldclient-js-common' { /** * A LaunchDarkly user object. */ - export type LDUser = { + export interface LDUser { /** * A unique string identifying a user. */ @@ -268,13 +269,21 @@ declare module 'ldclient-js-common' { custom?: { [key: string]: string | boolean | number | Array; }; + + /** + * Specifies a list of attribute names (either built-in or custom) which should be + * marked as private, and not sent to LaunchDarkly in analytics events. This is in + * addition to any private attributes designated in the global configuration + * with [[LDOptions.privateAttributeNames]] or [[LDOptions.allAttributesPrivate]]. + */ + privateAttributeNames?: Array; } /** * Describes the reason that a flag evaluation produced a particular value. This is * part of the [[LDEvaluationDetail]] object returned by [[LDClient.variationDetail]]. */ - export type LDEvaluationReason = { + export interface LDEvaluationReason { /** * The general category of the reason: * @@ -308,7 +317,7 @@ declare module 'ldclient-js-common' { * The key of the failed prerequisite flag, if the kind was `'PREREQUISITE_FAILED'`. */ prerequisiteKey?: string; - }; + } /** * An object that combines the result of a feature flag evaluation with information about @@ -318,7 +327,7 @@ declare module 'ldclient-js-common' { * * For more information, see the [SDK reference guide](https://docs.launchdarkly.com/docs/evaluation-reasons). */ - export type LDEvaluationDetail = { + export interface LDEvaluationDetail { /** * The result of the flag evaluation. This will be either one of the flag's variations or * the default value that was passed to [[LDClient.variationDetail]]. @@ -335,7 +344,7 @@ declare module 'ldclient-js-common' { * An object describing the main factor that influenced the flag evaluation value. */ reason: LDEvaluationReason; - }; + } /** * The basic interface for the LaunchDarkly client. The browser SDK and the Electron SDK both diff --git a/packages/ldclient-js/src/__tests__/browserPlatform-test.js b/packages/ldclient-js/src/__tests__/browserPlatform-test.js index d8d2b585..1e8558af 100644 --- a/packages/ldclient-js/src/__tests__/browserPlatform-test.js +++ b/packages/ldclient-js/src/__tests__/browserPlatform-test.js @@ -83,6 +83,35 @@ describe('browserPlatform', () => { }); }); }); + + it('reports local storage as being unavailable if window.localStorage is missing', () => { + const oldLocalStorage = window.localStorage; + try { + delete window.localStorage; + const testPlatform = browserPlatform(); + expect(testPlatform.localStorage).not.toBe(expect.anything()); + } finally { + window.localStorage = oldLocalStorage; + } + }); + + it('reports local storage as being unavailable if accessing window.localStorage throws an exception', () => { + const oldLocalStorage = window.localStorage; + try { + delete window.localStorage; + Object.defineProperty(window, 'localStorage', { + configurable: true, + get: () => { + throw new Error('should not see this error'); + }, + }); + const testPlatform = browserPlatform(); + expect(testPlatform.localStorage).not.toBe(expect.anything()); + } finally { + delete window.localStorage; + window.localStorage = oldLocalStorage; + } + }); }); describe('EventSource', () => { diff --git a/packages/ldclient-js/src/browserPlatform.js b/packages/ldclient-js/src/browserPlatform.js index ae3671b7..221e5060 100644 --- a/packages/ldclient-js/src/browserPlatform.js +++ b/packages/ldclient-js/src/browserPlatform.js @@ -29,32 +29,38 @@ export default function makeBrowserPlatform() { return flag === 1 || flag === true || flag === '1' || flag === 'yes'; }; - if (window.localStorage) { - ret.localStorage = { - get: (key, callback) => { - try { - callback(null, window.localStorage.getItem(key)); - } catch (ex) { - callback(ex); - } - }, - set: (key, value, callback) => { - try { - window.localStorage.setItem(key, value); - callback(null); - } catch (ex) { - callback(ex); - } - }, - clear: (key, callback) => { - try { - window.localStorage.removeItem(key); - callback(null); - } catch (ex) { - callback(ex); - } - }, - }; + try { + if (window.localStorage) { + ret.localStorage = { + get: (key, callback) => { + try { + callback(null, window.localStorage.getItem(key)); + } catch (ex) { + callback(ex); + } + }, + set: (key, value, callback) => { + try { + window.localStorage.setItem(key, value); + callback(null); + } catch (ex) { + callback(ex); + } + }, + clear: (key, callback) => { + try { + window.localStorage.removeItem(key); + callback(null); + } catch (ex) { + callback(ex); + } + }, + }; + } + } catch (e) { + // In some browsers (such as Chrome), even looking at window.localStorage at all will cause a + // security error if the feature is disabled. + ret.localStorage = null; } // If EventSource does not exist, the absence of eventSourceFactory will make us not try to open streams diff --git a/packages/ldclient-js/src/index.js b/packages/ldclient-js/src/index.js index d3d212c4..30e3e797 100644 --- a/packages/ldclient-js/src/index.js +++ b/packages/ldclient-js/src/index.js @@ -38,31 +38,9 @@ export function initialize(env, user, options = {}) { } window.addEventListener('beforeunload', clientVars.stop); - enableClickEventUIHandshake(validatedOptions.baseUrl); - return client; } -function enableClickEventUIHandshake(baseUrl) { - // The following event listener is used for handshaking with the LaunchDarkly application UI when - // the user's page is being loaded within a frame, for setting up a click event. - window.addEventListener('message', handleMessage); - function handleMessage(event) { - if (event.origin !== baseUrl) { - return; - } - if (event.data.type === 'SYN') { - window.editorClientBaseUrl = baseUrl; - const editorTag = document.createElement('script'); - editorTag.type = 'text/javascript'; - editorTag.async = true; - editorTag.src = baseUrl + event.data.editorClientUrl; - const s = document.getElementsByTagName('script')[0]; - s.parentNode.insertBefore(editorTag, s); - } - } -} - export const createConsoleLogger = common.createConsoleLogger; export const version = common.version; diff --git a/packages/ldclient-js/test-types.ts b/packages/ldclient-js/test-types.ts index 66a4760e..8f296ab7 100644 --- a/packages/ldclient-js/test-types.ts +++ b/packages/ldclient-js/test-types.ts @@ -2,11 +2,13 @@ // This file exists only so that we can run the TypeScript compiler in the CI build // to validate our typings.d.ts file. -import { createConsoleLogger, LDClient, LDLogger, LDOptions, LDUser, initialize } from 'ldclient-js'; +import * as ld from 'ldclient-js'; -var emptyOptions: LDOptions = {}; -var logger: LDLogger = createConsoleLogger("info"); -var allOptions: LDOptions = { +var ver: string = ld.version; + +var emptyOptions: ld.LDOptions = {}; +var logger: ld.LDLogger = ld.createConsoleLogger("info"); +var allOptions: ld.LDOptions = { bootstrap: { }, hash: '', baseUrl: '', @@ -27,5 +29,37 @@ var allOptions: LDOptions = { streamReconnectDelay: 1, logger: logger }; -var user: LDUser = { key: 'user' }; -var client: LDClient = initialize('env', user, allOptions); +var userWithKeyOnly: ld.LDUser = { key: 'user' }; +var user: ld.LDUser = { + key: 'user', + name: 'name', + firstName: 'first', + lastName: 'last', + email: 'test@example.com', + avatar: 'http://avatar.url', + ip: '1.1.1.1', + country: 'us', + anonymous: true, + custom: { + 'a': 's', + 'b': true, + 'c': 3, + 'd': [ 'x', 'y' ], + 'e': [ true, false ], + 'f': [ 1, 2 ] + }, + privateAttributeNames: [ 'name', 'email' ] +}; +var client: ld.LDClient = ld.initialize('env', user, allOptions); + +var boolFlagValue: ld.LDFlagValue = client.variation('key', false); +var numberFlagValue: ld.LDFlagValue = client.variation('key', 2); +var stringFlagValue: ld.LDFlagValue = client.variation('key', 'default'); + +var detail: ld.LDEvaluationDetail = client.variationDetail('key', 'default'); +var detailValue: ld.LDFlagValue = detail.value; +var detailIndex: number | undefined = detail.variationIndex; +var detailReason: ld.LDEvaluationReason = detail.reason; + +var flagSet: ld.LDFlagSet = client.allFlags(); +var flagSetValue: ld.LDFlagValue = flagSet['key']; diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 00000000..f9e93286 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# This script updates the version for the js-client packages and releases them to npm. +# It requires you to have valid NPM credentials associated with the packages. + +# It takes exactly one argument: the new version. +# It should be run from the root of this git repo like this: +# ./scripts/release.sh 4.0.9 + +# When done you should commit and push the changes made. + +set -uxe +echo "Starting js-client release." + +VERSION=$1 + +PROJECT_DIR=`pwd` + +# Make the lerna command available. + +npm install + +# Update version in all packages. Explanation of options: +# --no-git-tag-version: lerna creates tags in the wrong format ("v2.0.0" instead of "2.0.0"). The +# release job that calls this script will create a tag anyway. +# --no-push: The release job takes care of committing and pushing. +# -y: Suppresses interactive prompts. + +./node_modules/.bin/lerna version $VERSION --no-git-tag-version --no-push -y + +# Publish all packages. +# +# Supposedly, we should be able to do them all at once like so: +# ./node_modules/.bin/lerna publish $VERSION --from-package --no-git-reset --no-git-tag-version -y +# +# However, we haven't been able to get that to work. The packages get built, but the tarballs uploaded +# to npm do not contain the build products. In other words, it is *not* equivalent to just running +# "npm publish" in each package directory. So, for now, we'll just do the latter. + +for package in ldclient-js-common ldclient-js ldclient-react; do + cd $PROJECT_DIR/packages/$package + npm publish +done + +echo "Done with js-client release"