Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Config v6 #96

Merged
merged 28 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6fef8e4
Update config JSON model to v6 + fix hashing of strings containing as…
adams85 Oct 20, 2023
f48ac85
Simplify checks for both null and undefined
adams85 Oct 25, 2023
482fe85
Refactor evaluator and evaluation logging to prepare it for the new f…
adams85 Oct 27, 2023
fb30ccb
Implement segment condition evaluation
adams85 Oct 30, 2023
582abfc
Implement prerequisite flag condition evaluation
adams85 Oct 30, 2023
db7b6dc
Implement new comparison operators
adams85 Oct 30, 2023
4e2390a
Implement SDK key format validation + fix broken tests
adams85 Oct 30, 2023
595c0f2
Add helper methods for converting numeric/datetime/string array value…
adams85 Oct 30, 2023
30881fe
Refactor matrix to load configs directly from CDN instead of local sn…
adams85 Oct 30, 2023
da5e933
Fix float number parsing
adams85 Oct 31, 2023
e32ec1e
Add matrix tests
adams85 Oct 30, 2023
74277a9
Add evaluation log tests
adams85 Oct 31, 2023
446001f
Add other tests
adams85 Oct 31, 2023
72dbf0b
Attempt at fixing code coverage OOM
adams85 Oct 31, 2023
1ff37b2
Minor fixes and improvements
adams85 Nov 5, 2023
2eb47a3
Downgrade nyc to make code coverage analysis work (nyc v15.x seems to…
adams85 Nov 5, 2023
ab538c7
Extract EvaluateLogBuilder into a separate module
adams85 Nov 6, 2023
a43ef4c
Extract User into a separate module
adams85 Nov 6, 2023
b273ee5
Handle non-string User Object attributes
adams85 Nov 6, 2023
6b6e047
As per spec, allow leading/trailing whitespace in user object attribu…
adams85 Nov 13, 2023
5247ed6
As per spec, rename IEvaluationDetails.matchedEvaluationRule/matchedE…
adams85 Nov 13, 2023
fb7d7aa
Improve error handling consistency in prerequisite flag evaluation
adams85 Nov 15, 2023
329a813
Return null instead of throwing when User Object attribute helper is …
adams85 Nov 15, 2023
6bd699f
Increase code coverage
adams85 Nov 15, 2023
911f8ea
Don't force users to pass user object attributes as strings
adams85 Nov 15, 2023
0d3a293
Allow passing NaN values to number/datetime comparisons
adams85 Nov 20, 2023
309569e
Fix typo
adams85 Nov 21, 2023
6b0866b
Merge branch 'master' into config-v6
adams85 Nov 23, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@ dist/
lib/
node_modules/
src/Semver.ts
src/Sha1.ts
src/Hash.ts
src/lib.*.d.ts
3,593 changes: 1,992 additions & 1,601 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"types": "lib/index.d.ts",
"module": "lib/esm/index.js",
"scripts": {
"coverage": "nyc npm run test",
"coverage": "cross-env nyc npm run test",
"build": "tsc -p tsconfig.build.cjs.json && tsc -p tsconfig.build.esm.json",
"prepare": "npm run build",
"test": "cross-env TS_NODE_PROJECT=./tsconfig.mocha.json node --expose-gc node_modules/mocha/bin/_mocha --require ts-node/register 'test/**/*.ts' --exit --timeout 30000",
Expand Down Expand Up @@ -37,6 +37,7 @@
"@types/chai": "4.3.4",
"@types/mocha": "^10.0.1",
"@types/node": "^18.11.18",
"@types/tunnel": "^0.0.5",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
"chai": "^4.3.7",
Expand All @@ -45,9 +46,10 @@
"eslint-plugin-import": "^2.27.5",
"mocha": "^10.2.0",
"moq.ts": "^7.4.1",
"nyc": "^15.1.0",
"nyc": "^14.1.1",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.1",
"tunnel": "^0.0.6",
"typescript": "^4.0.2"
},
"repository": {
Expand All @@ -68,8 +70,8 @@
"src"
],
"exclude": [
"src/Semver.ts",
"src/Sha1.ts"
"src/Hash.ts",
"src/Semver.ts"
]
},
"sideEffects": false
Expand Down
4 changes: 3 additions & 1 deletion samples/deno-sandbox/import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@
"../../src/ConfigFetcher": "../../src/ConfigFetcher.ts",
"../../src/ConfigServiceBase": "../../src/ConfigServiceBase.ts",
"../../src/DefaultEventEmitter": "../../src/DefaultEventEmitter.ts",
"../../src/EvaluateLogBuilder": "../../src/EvaluateLogBuilder.ts",
"../../src/EventEmitter": "../../src/EventEmitter.ts",
"../../src/FlagOverrides": "../../src/FlagOverrides.ts",
"../../src/Hash": "../../src/Hash.ts",
"../../src/Hooks": "../../src/Hooks.ts",
"../../src/LazyLoadConfigService": "../../src/LazyLoadConfigService.ts",
"../../src/ManualPollConfigService": "../../src/ManualPollConfigService.ts",
"../../src/Polyfills": "../../src/Polyfills.ts",
"../../src/ProjectConfig": "../../src/ProjectConfig.ts",
"../../src/RolloutEvaluator": "../../src/RolloutEvaluator.ts",
"../../src/Semver": "../../src/Semver.ts",
"../../src/Sha1": "../../src/Sha1.ts",
"../../src/User": "../../src/User.ts",
"../../src/Utils": "../../src/Utils.ts"
}
}
120 changes: 119 additions & 1 deletion samples/deno-sandbox/sample.json
Original file line number Diff line number Diff line change
@@ -1 +1,119 @@
{"p":{"u":"https://cdn-global.configcat.com","r":0},"f":{"isAwesomeFeatureEnabled":{"v":true,"i":"ca36009d","t":0,"p":[{"o":0,"v":true,"p":0,"i":"ca36009d"},{"o":1,"v":false,"p":100,"i":"2bdcee75"}],"r":[{"o":0,"a":"Identifier","t":0,"c":"gdf","v":false,"i":"2bdcee75"},{"o":1,"a":"Identifier","t":0,"c":"dfgdf","v":false,"i":"2bdcee75"}]},"isPOCFeatureEnabled":{"v":false,"i":"430bded3","t":0,"p":[],"r":[{"o":0,"a":"Email","t":2,"c":"@something.com","v":false,"i":"430bded3"},{"o":1,"a":"Email","t":2,"c":"@example.com","v":true,"i":"9f21c24c"}]}}}
{
"p": {
"u": "https://cdn-global.configcat.com",
"r": 0,
"s": "unEpMhsXD/Zv9MH0gkBqS0Hr97LCGUKpHHOhEfzTcY4="
},
"f": {
"isAwesomeFeatureEnabled": {
"t": 0,
"r": [
{
"c": [
{
"u": {
"a": "Identifier",
"c": 0,
"l": [
"gdf"
]
}
}
],
"s": {
"v": {
"b": false
},
"i": "2bdcee75"
}
},
{
"c": [
{
"u": {
"a": "Identifier",
"c": 0,
"l": [
"dfgdf"
]
}
}
],
"s": {
"v": {
"b": false
},
"i": "2bdcee75"
}
}
],
"p": [
{
"p": 0,
"v": {
"b": true
},
"i": "ca36009d"
},
{
"p": 100,
"v": {
"b": false
},
"i": "2bdcee75"
}
],
"v": {
"b": true
},
"i": "ca36009d"
},
"isPOCFeatureEnabled": {
"t": 0,
"r": [
{
"c": [
{
"u": {
"a": "Email",
"c": 2,
"l": [
"@something.com"
]
}
}
],
"s": {
"v": {
"b": false
},
"i": "430bded3"
}
},
{
"c": [
{
"u": {
"a": "Email",
"c": 2,
"l": [
"@example.com"
]
}
}
],
"s": {
"v": {
"b": true
},
"i": "9f21c24c"
}
}
],
"v": {
"b": false
},
"i": "430bded3"
}
}
}
2 changes: 1 addition & 1 deletion src/ConfigCatCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export class ExternalConfigCache implements IConfigCache {
}

private updateCachedConfig(externalSerializedConfig: string | null | undefined): void {
if (externalSerializedConfig === null || externalSerializedConfig === void 0 || externalSerializedConfig === this.cachedSerializedConfig) {
if (externalSerializedConfig == null || externalSerializedConfig === this.cachedSerializedConfig) {
return;
}

Expand Down
69 changes: 50 additions & 19 deletions src/ConfigCatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
import { LazyLoadConfigService } from "./LazyLoadConfigService";
import { ManualPollConfigService } from "./ManualPollConfigService";
import { getWeakRefStub, isWeakRefAvailable } from "./Polyfills";
import type { IConfig, ProjectConfig, RolloutPercentageItem, RolloutRule, Setting, SettingValue } from "./ProjectConfig";
import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf, User } from "./RolloutEvaluator";
import type { IConfig, PercentageOption, ProjectConfig, Setting, SettingValue } from "./ProjectConfig";
import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf } from "./RolloutEvaluator";
import { RolloutEvaluator, checkSettingsAvailable, evaluate, evaluateAll, evaluationDetailsFromDefaultValue, getTimestampAsDate, isAllowedValue } from "./RolloutEvaluator";
import { errorToString } from "./Utils";
import type { User } from "./User";
import { errorToString, isArray, throwError } from "./Utils";

/** ConfigCat SDK client. */
export interface IConfigCatClient extends IProvidesHooks {
Expand Down Expand Up @@ -242,18 +243,23 @@
private static get instanceCache() { return clientInstanceCache; }

static get<TMode extends PollingMode>(sdkKey: string, pollingMode: TMode, options: OptionsForPollingMode<TMode> | undefined | null, configCatKernel: IConfigCatKernel): IConfigCatClient {
const invalidSdkKeyError = "Invalid 'sdkKey' value";
if (!sdkKey) {
throw new Error("Invalid 'sdkKey' value");
throw new Error(invalidSdkKeyError);

Check warning on line 248 in src/ConfigCatClient.ts

View check run for this annotation

Codecov / codecov/patch

src/ConfigCatClient.ts#L248

Added line #L248 was not covered by tests
}

const optionsClass =
pollingMode === PollingMode.AutoPoll ? AutoPollOptions :
pollingMode === PollingMode.ManualPoll ? ManualPollOptions :
pollingMode === PollingMode.LazyLoad ? LazyLoadOptions :
(() => { throw new Error("Invalid 'pollingMode' value"); })();
throwError(new Error("Invalid 'pollingMode' value"));

Check warning on line 255 in src/ConfigCatClient.ts

View check run for this annotation

Codecov / codecov/patch

src/ConfigCatClient.ts#L255

Added line #L255 was not covered by tests

const actualOptions = new optionsClass(sdkKey, configCatKernel.sdkType, configCatKernel.sdkVersion, options, configCatKernel.defaultCacheFactory, configCatKernel.eventEmitterFactory);

if (actualOptions.flagOverrides?.behaviour !== OverrideBehaviour.LocalOnly && !isValidSdkKey(sdkKey, actualOptions.baseUrlOverriden)) {
throw new Error(invalidSdkKeyError);
}

const [instance, instanceAlreadyCreated] = clientInstanceCache.getOrCreate(actualOptions, configCatKernel);

if (instanceAlreadyCreated && options) {
Expand Down Expand Up @@ -298,7 +304,7 @@
options instanceof AutoPollOptions ? new AutoPollConfigService(configCatKernel.configFetcher, options) :
options instanceof ManualPollOptions ? new ManualPollConfigService(configCatKernel.configFetcher, options) :
options instanceof LazyLoadOptions ? new LazyLoadConfigService(configCatKernel.configFetcher, options) :
(() => { throw new Error("Invalid 'options' value"); })();
throwError(new Error("Invalid 'options' value"));

Check warning on line 307 in src/ConfigCatClient.ts

View check run for this annotation

Codecov / codecov/patch

src/ConfigCatClient.ts#L307

Added line #L307 was not covered by tests
}
else {
this.hooks.emit("clientReady", ClientCacheState.HasLocalOverrideFlagDataOnly);
Expand Down Expand Up @@ -493,22 +499,30 @@
return new SettingKeyValue(settingKey, setting.value);
}

const rolloutRules = settings[settingKey].targetingRules;
if (rolloutRules && rolloutRules.length > 0) {
for (let i = 0; i < rolloutRules.length; i++) {
const rolloutRule: RolloutRule = rolloutRules[i];
if (variationId === rolloutRule.variationId) {
return new SettingKeyValue(settingKey, rolloutRule.value);
const targetingRules = settings[settingKey].targetingRules;
if (targetingRules && targetingRules.length > 0) {
for (let i = 0; i < targetingRules.length; i++) {
const then = targetingRules[i].then;
if (isArray(then)) {
for (let j = 0; j < then.length; j++) {
const percentageOption: PercentageOption = then[j];
if (variationId === percentageOption.variationId) {
return new SettingKeyValue(settingKey, percentageOption.value);
}
}
}
else if (variationId === then.variationId) {
return new SettingKeyValue(settingKey, then.value);
}
}
}

const percentageItems = settings[settingKey].percentageOptions;
if (percentageItems && percentageItems.length > 0) {
for (let i = 0; i < percentageItems.length; i++) {
const percentageItem: RolloutPercentageItem = percentageItems[i];
if (variationId === percentageItem.variationId) {
return new SettingKeyValue(settingKey, percentageItem.value);
const percentageOptions = settings[settingKey].percentageOptions;
if (percentageOptions && percentageOptions.length > 0) {
for (let i = 0; i < percentageOptions.length; i++) {
const percentageOption: PercentageOption = percentageOptions[i];
if (variationId === percentageOption.variationId) {
return new SettingKeyValue(settingKey, percentageOption.value);
}
}
}
Expand Down Expand Up @@ -751,14 +765,31 @@
public settingValue: TValue) { }
}

function isValidSdkKey(sdkKey: string, customBaseUrl: boolean) {
const proxyPrefix = "configcat-proxy/";

// NOTE: String.prototype.startsWith was introduced after ES5. We'd rather work around it instead of polyfilling it.
if (customBaseUrl && sdkKey.length > proxyPrefix.length && sdkKey.lastIndexOf(proxyPrefix, 0) === 0) {
return true;
}

const components = sdkKey.split("/");
const keyLength = 22;
switch (components.length) {
case 2: return components[0].length === keyLength && components[1].length === keyLength;
case 3: return components[0] === "configcat-sdk-1" && components[1].length === keyLength && components[2].length === keyLength;
default: return false;
}
}

function validateKey(key: string): void {
if (!key) {
throw new Error("Invalid 'key' value");
}
}

function ensureAllowedDefaultValue(value: SettingValue): void {
if (!isAllowedValue(value)) {
if (value != null && !isAllowedValue(value)) {
throw new TypeError("The default value must be boolean, number, string, null or undefined.");
}
}
Expand Down
12 changes: 6 additions & 6 deletions src/ConfigCatClientOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import { DefaultEventEmitter } from "./DefaultEventEmitter";
import type { IEventEmitter } from "./EventEmitter";
import { NullEventEmitter } from "./EventEmitter";
import type { FlagOverrides } from "./FlagOverrides";
import { sha1 } from "./Hash";
import type { HookEvents, IProvidesHooks, SafeHooksWrapper } from "./Hooks";
import { Hooks } from "./Hooks";
import { getWeakRefStub, isWeakRefAvailable } from "./Polyfills";
import { ProjectConfig } from "./ProjectConfig";
import type { User } from "./RolloutEvaluator";
import { sha1 } from "./Sha1";
import type { User } from "./User";

/** Specifies the supported polling modes. */
export enum PollingMode {
Expand Down Expand Up @@ -130,7 +130,7 @@ export type OptionsForPollingMode<TMode extends PollingMode> =

export abstract class OptionsBase {

private static readonly configFileName = "config_v5.json";
private static readonly configFileName = "config_v6.json";

logger: LoggerWrapper;

Expand Down Expand Up @@ -266,11 +266,11 @@ export class AutoPollOptions extends OptionsBase {

if (options) {

if (options.pollIntervalSeconds !== void 0 && options.pollIntervalSeconds !== null) {
if (options.pollIntervalSeconds != null) {
this.pollIntervalSeconds = options.pollIntervalSeconds;
}

if (options.maxInitWaitTimeSeconds !== void 0 && options.maxInitWaitTimeSeconds !== null) {
if (options.maxInitWaitTimeSeconds != null) {
this.maxInitWaitTimeSeconds = options.maxInitWaitTimeSeconds;
}
}
Expand Down Expand Up @@ -309,7 +309,7 @@ export class LazyLoadOptions extends OptionsBase {
super(apiKey, sdkType + "/l-" + sdkVersion, options, defaultCacheFactory, eventEmitterFactory);

if (options) {
if (options.cacheTimeToLiveSeconds !== void 0 && options.cacheTimeToLiveSeconds !== null) {
if (options.cacheTimeToLiveSeconds != null) {
this.cacheTimeToLiveSeconds = options.cacheTimeToLiveSeconds;
}
}
Expand Down
Loading