Skip to content

Commit

Permalink
prepare 7.0.0 release (launchdarkly#265)
Browse files Browse the repository at this point in the history
* add mention of singleton usage

* update diagnostic event info for OS name, data store type, Node version

* standardize linting

* disallow window and document

* fix null/undef checks

* misc linting fixes

* inlineUsersInEvents is not an unknown option

* drop node-sha1 dependency

* don't omit streamInits.failed when it's false

* bump request dependency to get security patch; loosen some exact dependencies

* remove request package; improve polling cache logic + add test

* bump typescript version to fix build error in Node 6

* update @types/node to fix TypeScript check step

* lint

* make sure we keep polling regardless of whether we got new data

* use launchdarkly-eventsource, make stream retry behavior consistent

* stream retry delay option should be in seconds & should be included in diagnostics

* minor test fix

* fix: Throw an error on malformed user-supplied logger

* don't call unref() on Redis client; ensure that database integration tests close the store

* update Redis driver to major version 3

* add test case

* allow redisOpts parameter to be omitted

* add logger adapter shim + tests

* minor cleanup and comments for ch74741 fix (logger wrapper)

* fix proxy tunnel configuration and make sure it's used in streaming

* change some string concatenation expressions to use interpolation

* feat: upgrade winston (launchdarkly#189)

* fix merge

* remove support for indirect/patch and indirect/put (launchdarkly#182)

* reuse same Promise and same event listeners for all waitForInitialization calls

* better docs for waitForInitialization + misc doc cleanup (launchdarkly#184)

* update js-eventsource to 1.3.1 for stream parsing bugfix (launchdarkly#185)

* fix broken logger format (launchdarkly#186)

* retroactively update changelog for bugfix in 5.13.2 release

* allow get/getAll Redis queries to be queued if Redis client hasn't yet connected

* set stream read timeout

* adding the alias functionality (launchdarkly#190)

* Removed the guides link

* remove monkey-patching of setImmediate

* Persist contextKind property during feature and custom event transformations (launchdarkly#194)

* add inlineUsersInEvents option in TypeScript

* Add support for seed to bucketUser

* Add note for incorporating seed into evaluation

* Send events when the evaluation is from an experiment

* Use seed to evaluate.

* Clean up test descriptions

* Rename variable to be less confusing

* Use ternary to eliminate mutation

* Make return signature more consistent

* Un-prettier the tests

* redis lower bounds bump (launchdarkly#199)

* update launchdarkly-js-test-helpers to fix TLS tests (launchdarkly#200)

* update js-eventsource to remove vulnerability warning (launchdarkly#201)

* add CI jobs for all compatible Node versions

* CI fixes

* more CI fixes

* comment

* use default value to simplify config

* (6.0 - #1) stop saying we're compatible with Node <12 (launchdarkly#203)

* add CI jobs for all compatible Node versions (launchdarkly#202)

* (6.0 - #2) remove Redis integration (launchdarkly#204)

* allow feature store to be specified as a factory (so it can get our logger)

* (6.0 - launchdarkly#3) remove Winston (launchdarkly#205)

* remove deprecated things for 6.0 (launchdarkly#206)

* update node-cache to 5.x (drops old Node compat)

* update semver to 7.x (drops old Node compat)

* update uuid to 8.x (Node compat, perf improvements, bugfixes)

* update dev dependencies

* linter

* replace lrucache package with lru-cache (launchdarkly#209)

* make yaml dependency optional (launchdarkly#210)

* update release metadata to include maintenance branch

* remove package-lock.json (launchdarkly#211)

* rm prerelease changelog

* (big segments #1) add interfaces for big segments (launchdarkly#212)

* (big segments #2) add all components for big segments except evaluation (launchdarkly#213)

* (big segments launchdarkly#3) implement big segments in flag evaluation (launchdarkly#214)

* (big segments launchdarkly#4) add standard test suite for big segment store tests + refactor feature store tests (launchdarkly#215)

* move new interfaces to a module instead of a namespace (launchdarkly#216)

* fix TS export of CachingStoreWrapper

* use Releaser v2 config

* fix overly specific test expectation that breaks in Node 17

* Initial work on FlagBuilder (launchdarkly#219)

* Add TestData factory(with some dummy methods); Initial work on FlagBuilder

* fixed indentation and linter errors; fixed an error in update; fixed incorrect test label

* fixed typo in TestData store

* converted boolean variation constants to be file variables instead of class variables

Co-authored-by: charukiewicz <[email protected]>
Co-authored-by: belevy <[email protected]>

* implemented FlagRuleBuilder; added .build() methods to FlagBuilder/FlagRuleBuilder and changed tests to avoid using private interface

* converted _targets to be Map instead of object literal; changed variationForBoolean to be a module-scoped function instead class-scoped

* Implement stream processor(data source) interface for test data

* Add TestData to index.js and write out the types for TestData and friends

* added testdata documentation to index.d.ts; fix linter errors; changed flag default behavior to create boolean flag

* Fix the interface file: reindented to 2 spaces, corrected definition of functions from properties to functions in interfaces; corrected issues in JSDoc comments

* modify tests to fix capitalization and actually test the test datasource works as an LDClient updateProcessor.

* Fix linter error on defaulted callback

* explicitly enable JSDOM types in TypeScript build to avoid errors when jsdom is referenced for some reason

* capitalize Big Segments in docs & logs

* documentation comment fixes for TestData

* pin TypeScript to 4.4.x

* move TestData and FIleDataSource to integrations module

* lint

* rename types used by TestData for clarity (launchdarkly#229)

* use varargs semantics for TestFlagBuilder.variations() and add it to the TS interface (launchdarkly#230)

* don't ever use for...in (launchdarkly#232)

* don't ever use for...in

* add null guard

* bump launchdarkly-eventsource dependency for sc-136154 fix

* use TestData in our own tests (launchdarkly#231)

* use TestData in our own tests

* update TS interface

* lint

* typo

* fix allFlagsState behavior regarding experimentation

* lint

* allow "secondary" to be referenced in clauses

* don't throw an exception for non-string in semver comparison

* correctly handle "client not ready" condition in allFlagsState

* lint

* Flags with a version of 0 reported as 'unknown' in summary events. (launchdarkly#239)

* Initial draft of typescript types. (launchdarkly#236)

* Implement attribute reference support.

* implement contract test service, not including big segments (launchdarkly#242)

Co-authored-by: Eli Bishop <[email protected]>

* Implement Application tags for the node SDK. (launchdarkly#241)

* update js-eventsource to 1.4.4 for security fix

* remove package-lock.json

* adjust test expectation about error message to work in recent Node versions

* launchdarkly#3 Add context filtering and legacy to single kind conversion. (launchdarkly#238)

Co-authored-by: Eli Bishop <[email protected]>

* launchdarkly#4 Switch from user to context for events. (launchdarkly#244)

Co-authored-by: Eli Bishop <[email protected]>

* launchdarkly#5 Rlamb/sc 142950/implement u2c evaluation (launchdarkly#248)

Co-authored-by: Eli Bishop <[email protected]>

* launchdarkly#6 Rlamb/sc 145767/attribute reference improvements (launchdarkly#250)

Co-authored-by: Eli Bishop <[email protected]>

* launchdarkly#7 Rlamb/sc 146614/do not support bucketby for experiments (launchdarkly#251)

Co-authored-by: Eli Bishop <[email protected]>

* launchdarkly#8 Rlamb/sc 147263/treat cyclic segements as errors (launchdarkly#252)

Co-authored-by: Eli Bishop <[email protected]>

* Do not use the secondary key for experiments. (launchdarkly#256)

* Resolve issues with V2 test harness. (launchdarkly#258)

* Adds link to Relay Proxy docs

* Update index.d.ts

Co-authored-by: Eli Bishop <[email protected]>

* ensure setTimeout task is cleared when polling is stopped

* fix some flaky tests using async blocking logic

* rm unused

* simplify polling implementation using setInterval

* Update the test data source for U2C. (launchdarkly#257)

* use newer js-test-helpers for async tests

* add request number to timeout message

* Enforce 64 character limit for application tag values. (launchdarkly#263)

* Changed transient back to anonymous. (launchdarkly#264)

* Fixed operator field key name in TestDataRuleBuilder (launchdarkly#246)

* Do not set `inExperiment`  if there is not a context for the specified kind. (launchdarkly#266)

* [sc-160948] Switch to partial URL encoding. (launchdarkly#265)

* Update event schema version. (launchdarkly#267)

* [sc-171125] Do now allow indexing into an array with an attribute reference. (launchdarkly#268)

* [sc-174033] Remove support for secondary. (launchdarkly#269)

* Treat 'kind' and '/kind' the same. (launchdarkly#270)

* [sc-176598] Update node U2C with latest changes from main. (launchdarkly#272)

* [sc-176599] Update documentation for privateAttributes _meta attribute of contexts. (launchdarkly#271)

* Remove copy/paste error. (launchdarkly#274)

* [sc-177983] Add support for executing old style user tests. (launchdarkly#275)

* Update release metadata.

* Do not generate events for bad contexts. (launchdarkly#277)

Co-authored-by: Yusinto Ngadiman <[email protected]>

* Handle nested segment dependencies. (launchdarkly#278)

Co-authored-by: LaunchDarklyCI <[email protected]>
Co-authored-by: Eli Bishop <[email protected]>
Co-authored-by: Maxwell Gerber <[email protected]>
Co-authored-by: Chris West <[email protected]>
Co-authored-by: Ben Woskow <[email protected]>
Co-authored-by: Mike Zorn <[email protected]>
Co-authored-by: Ben Woskow <[email protected]>
Co-authored-by: Robert J. Neal <[email protected]>
Co-authored-by: Ben Levy <[email protected]>
Co-authored-by: charukiewicz <[email protected]>
Co-authored-by: belevy <[email protected]>
Co-authored-by: charukiewicz <[email protected]>
Co-authored-by: LaunchDarklyReleaseBot <[email protected]>
Co-authored-by: Ryan Lamb <[email protected]>
Co-authored-by: Ember Stevens <[email protected]>
Co-authored-by: Ember Stevens <[email protected]>
Co-authored-by: Yusinto Ngadiman <[email protected]>
  • Loading branch information
18 people authored Dec 7, 2022
1 parent b3dc03b commit 5aa56fc
Show file tree
Hide file tree
Showing 44 changed files with 4,361 additions and 1,314 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ junit.xml
npm-debug.log
test-types.js
.vscode
coverage/
3 changes: 2 additions & 1 deletion .ldrelease/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ repo:

branches:
- name: main
description: 6.x
description: 7.x
- name: 6.x
- name: 5.x

publications:
Expand Down
217 changes: 217 additions & 0 deletions attribute_reference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
/**
* Take a key string and escape the characters to allow it to be used as a reference.
* @param {string} key
* @returns {string} The processed key.
*/
function processEscapeCharacters(key) {
return key.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
* @param {string} reference The reference to get the components of.
* @returns {string[]} The components of the reference. Escape characters will be converted to their representative values.
*/
function getComponents(reference) {
const referenceWithoutPrefix = reference.startsWith('/') ? reference.substring(1) : reference;
return referenceWithoutPrefix
.split('/')
.map(component => (component.indexOf('~') >= 0 ? component.replace(/~1/g, '/').replace(/~0/g, '~') : component));
}

/**
* @param {string} reference The reference to check if it is a literal.
* @returns true if the reference is a literal.
*/
function isLiteral(reference) {
return !reference.startsWith('/');
}

/**
* Get an attribute value from a literal.
* @param {Object} target
* @param {string} literal
*/
function getFromLiteral(target, literal) {
if (target !== null && target !== undefined && Object.prototype.hasOwnProperty.call(target, literal)) {
return target[literal];
}
}

/**
* Gets the `target` object's value at the `reference`'s location.
*
* This method method follows the rules for accessing attributes for use
* in evaluating clauses.
*
* Accessing the root of the target will always result in undefined.
*
* @param {Object} target
* @param {string} reference
* @returns The `target` object's value at the `reference`'s location.
* Undefined if the field does not exist or if the reference is not valid.
*/
function get(target, reference) {
if (reference === '' || reference === '/') {
return undefined;
}

if (isLiteral(reference)) {
return getFromLiteral(target, reference);
}

const components = getComponents(reference);
let current = target;
for (const component of components) {
if (
current !== null &&
current !== undefined &&
typeof current === 'object' &&
// We do not want to allow indexing into an array.
!Array.isArray(current) &&
// For arrays and strings, in addition to objects, a hasOwnProperty check
// will be true for indexes (as strings or numbers), which are present
// in the object/string/array.
Object.prototype.hasOwnProperty.call(current, component)
) {
current = current[component];
} else {
return undefined;
}
}

return current;
}

/**
* Compare two references and determine if they are equivalent.
* @param {string} a
* @param {string} b
*/
function compare(a, b) {
const aIsLiteral = isLiteral(a);
const bIsLiteral = isLiteral(b);
if (aIsLiteral && bIsLiteral) {
return a === b;
}
if (aIsLiteral) {
const bComponents = getComponents(b);
if (bComponents.length !== 1) {
return false;
}
return a === bComponents[0];
}
if (bIsLiteral) {
const aComponents = getComponents(a);
if (aComponents.length !== 1) {
return false;
}
return b === aComponents[0];
}
return a === b;
}

/**
* @param {string} a
* @param {string} b
* @returns The two strings joined by '/'.
*/
function join(a, b) {
return `${a}/${b}`;
}

/**
* There are cases where a field could have been named with a preceeding '/'.
* If that attribute was private, then the literal would appear to be a reference.
* This method can be used to convert a literal to a reference in such situations.
* @param {string} literal The literal to convert to a reference.
* @returns A literal which has been converted to a reference.
*/
function literalToReference(literal) {
return `/${processEscapeCharacters(literal)}`;
}

/**
* Clone an object excluding the values referenced by a list of references.
* @param {Object} target The object to clone.
* @param {string[]} references A list of references from the cloned object.
* @returns {{cloned: Object, excluded: string[]}} The cloned object and a list of excluded values.
*/
function cloneExcluding(target, references) {
const stack = [];
const cloned = {};
const excluded = [];

stack.push(
...Object.keys(target).map(key => ({
key,
ptr: literalToReference(key),
source: target,
parent: cloned,
visited: [target],
}))
);

while (stack.length) {
const item = stack.pop();
if (!references.some(ptr => compare(ptr, item.ptr))) {
const value = item.source[item.key];

// Handle null because it overlaps with object, which we will want to handle later.
if (value === null) {
item.parent[item.key] = value;
} else if (Array.isArray(value)) {
item.parent[item.key] = [...value];
} else if (typeof value === 'object') {
//Arrays and null must already be handled.

//Prevent cycles by not visiting the same object
//with in the same branch. Parallel branches
//may contain the same object.
if (item.visited.includes(value)) {
continue;
}

item.parent[item.key] = {};

stack.push(
...Object.keys(value).map(key => ({
key,
ptr: join(item.ptr, processEscapeCharacters(key)),
source: value,
parent: item.parent[item.key],
visited: [...item.visited, value],
}))
);
} else {
item.parent[item.key] = value;
}
} else {
excluded.push(item.ptr);
}
}
return { cloned, excluded: excluded.sort() };
}

function isValidReference(reference) {
return !reference.match(/\/\/|(^\/.*~[^0|^1])|~$/);
}

/**
* Check if the given attribute reference is for the "kind" attribute.
* @param {string} reference String containing an attribute reference.
*/
function isKind(reference) {
// There are only 2 valid ways to specify the kind attribute,
// so this just checks them. Given the current flow of evaluation
// this is much less intense a process than doing full validation and parsing.
return reference === 'kind' || reference === '/kind';
}

module.exports = {
cloneExcluding,
compare,
get,
isValidReference,
literalToReference,
isKind,
};
13 changes: 8 additions & 5 deletions configuration.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ module.exports = (function () {
offline: false,
useLdd: false,
allAttributesPrivate: false,
privateAttributeNames: [],
inlineUsersInEvents: false,
userKeysCapacity: 1000,
userKeysFlushInterval: 300,
privateAttributes: [],
contextKeysCapacity: 1000,
contextKeysFlushInterval: 300,
diagnosticOptOut: false,
diagnosticRecordingInterval: 900,
featureStore: InMemoryFeatureStore(),
Expand Down Expand Up @@ -89,7 +88,10 @@ module.exports = (function () {
};

/* eslint-disable camelcase */
const deprecatedOptions = {};
const deprecatedOptions = {
userKeysCapacity: 'contextKeysCapacity',
userKeysFlushInterval: 'contextKeysFlushInterval',
};
/* eslint-enable camelcase */

function checkDeprecatedOptions(configIn) {
Expand Down Expand Up @@ -137,6 +139,7 @@ module.exports = (function () {
}
return 'object';
};

Object.keys(config).forEach(name => {
const value = config[name];
if (value !== null && value !== undefined) {
Expand Down
98 changes: 98 additions & 0 deletions context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/**
* Validate a context kind.
* @param {string} kind
* @returns true if the kind is valid.
*/
function validKind(kind) {
return typeof kind === 'string' && kind !== 'kind' && kind.match(/^(\w|\.|-)+$/);
}

/**
* Validate a context key.
* @param {string} key
* @returns true if the key is valid.
*/
function validKey(key) {
return key !== undefined && key !== null && key !== '' && typeof key === 'string';
}

/**
* Perform a check of basic context requirements.
* @param {Object} context
* @param {boolean} allowLegacyKey If true, then a legacy user can have an
* empty or non-string key. A legacy user is a context without a kind.
* @returns true if the context meets basic requirements.
*/
function checkContext(context, allowLegacyKey) {
if (context) {
if (allowLegacyKey && (context.kind === undefined || context.kind === null)) {
return context.key !== undefined && context.key !== null;
}
const key = context.key;
const kind = context.kind === undefined ? 'user' : context.kind;
const kindValid = validKind(kind);
const keyValid = kind === 'multi' || validKey(key);
if (kind === 'multi') {
const kinds = Object.keys(context).filter(key => key !== 'kind');
return keyValid && kinds.every(key => validKind(key)) && kinds.every(key => validKey(context[key].key));
}
return keyValid && kindValid;
}
return false;
}

/**
* The partial URL encoding is needed because : is a valid character in context keys.
*
* Partial encoding is the replacement of all colon (:) characters with the URL
* encoded equivalent (%3A) and all percent (%) characters with the URL encoded
* equivalent (%25).
* @param {string} key The key to encode.
* @returns {string} Partially URL encoded key.
*/
function encodeKey(key) {
if (key.includes('%') || key.includes(':')) {
return key.replace(/%/g, '%25').replace(/:/g, '%3A');
}
return key;
}

/**
* For a given context get a list of context kinds.
* @param {Object} context
* @returns A list of kinds in the context.
*/
function getContextKinds(context) {
if (context) {
if (context.kind === null || context.kind === undefined) {
return ['user'];
}
if (context.kind !== 'multi') {
return [context.kind];
}
return Object.keys(context).filter(kind => kind !== 'kind');
}
return [];
}

function getCanonicalKey(context) {
if (context) {
if ((context.kind === undefined || context.kind === null || context.kind === 'user') && context.key) {
return context.key;
} else if (context.kind !== 'multi' && context.key) {
return `${context.kind}:${encodeKey(context.key)}`;
} else if (context.kind === 'multi') {
return Object.keys(context)
.sort()
.filter(key => key !== 'kind')
.map(key => `${key}:${encodeKey(context[key].key)}`)
.join(':');
}
}
}

module.exports = {
checkContext,
getContextKinds,
getCanonicalKey,
};
Loading

0 comments on commit 5aa56fc

Please sign in to comment.