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 14 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
520 changes: 372 additions & 148 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion 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 NODE_OPTIONS=--max-old-space-size=16384 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 @@ -48,6 +49,7 @@
"nyc": "^15.1.0",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.1",
"tunnel": "^0.0.6",
"typescript": "^4.0.2"
},
"repository": {
Expand Down
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
66 changes: 48 additions & 18 deletions src/ConfigCatClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import type { HookEvents, Hooks, IProvidesHooks } from "./Hooks";
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 { IConfig, PercentageOption, ProjectConfig, Setting, SettingValue } from "./ProjectConfig";
import type { IEvaluationDetails, IRolloutEvaluator, SettingTypeOf, User } from "./RolloutEvaluator";
import { RolloutEvaluator, checkSettingsAvailable, evaluate, evaluateAll, evaluationDetailsFromDefaultValue, getTimestampAsDate, isAllowedValue } from "./RolloutEvaluator";
import { errorToString } from "./Utils";
import { errorToString, isArray, throwError } from "./Utils";

/** ConfigCat SDK client. */
export interface IConfigCatClient extends IProvidesHooks {
Expand Down Expand Up @@ -241,18 +241,23 @@ export class ConfigCatClient implements IConfigCatClient {
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);
}

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"));

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 @@ -294,7 +299,7 @@ export class ConfigCatClient implements IConfigCatClient {
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"));
}
else {
this.options.hooks.emit("clientReady", ClientCacheState.HasLocalOverrideFlagDataOnly);
Expand Down Expand Up @@ -489,22 +494,30 @@ export class ConfigCatClient implements IConfigCatClient {
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 @@ -746,14 +759,31 @@ export class SettingKeyValue<TValue = SettingValue> {
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
10 changes: 5 additions & 5 deletions src/ConfigCatClientOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import type { ClientCacheState } from "./ConfigServiceBase";
import { DefaultEventEmitter } from "./DefaultEventEmitter";
import type { IEventEmitter } from "./EventEmitter";
import type { FlagOverrides } from "./FlagOverrides";
import { sha1 } from "./Hash";
import type { IProvidesHooks } from "./Hooks";
import { Hooks } from "./Hooks";
import { ProjectConfig } from "./ProjectConfig";
import type { User } from "./RolloutEvaluator";
import { sha1 } from "./Sha1";

/** Specifies the supported polling modes. */
export enum PollingMode {
Expand Down Expand Up @@ -129,7 +129,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 @@ -253,11 +253,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 @@ -296,7 +296,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
44 changes: 40 additions & 4 deletions src/ConfigCatLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,13 @@ export class LoggerWrapper implements IConfigCatLogger {
private readonly hooks?: Hooks) {
}

private isLogLevelEnabled(logLevel: LogLevel): boolean {
isEnabled(logLevel: LogLevel): boolean {
return this.level >= logLevel;
}

/** @inheritdoc */
log(level: LogLevel, eventId: LogEventId, message: LogMessage, exception?: any): LogMessage {
if (this.isLogLevelEnabled(level)) {
if (this.isEnabled(level)) {
this.logger.log(level, eventId, message, exception);
}

Expand Down Expand Up @@ -259,7 +259,7 @@ export class LoggerWrapper implements IConfigCatLogger {
);
}

targetingIsNotPossible(key: string): LogMessage {
userObjectIsMissing(key: string): LogMessage {
return this.log(
LogLevel.Warn, 3001,
FormattableLogMessage.from(
Expand All @@ -275,6 +275,42 @@ export class LoggerWrapper implements IConfigCatLogger {
);
}

userObjectAttributeIsMissingPercentage(key: string, attributeName: string): LogMessage {
return this.log(
LogLevel.Warn, 3003,
FormattableLogMessage.from(
"KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_NAME"
)`Cannot evaluate % options for setting '${key}' (the User.${attributeName} attribute is missing). You should set the User.${attributeName} attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/`
);
}

userObjectAttributeIsMissingCondition(condition: string, key: string, attributeName: string): LogMessage {
return this.log(
LogLevel.Warn, 3003,
FormattableLogMessage.from(
"CONDITION", "KEY", "ATTRIBUTE_NAME", "ATTRIBUTE_NAME"
)`Cannot evaluate condition (${condition}) for setting '${key}' (the User.${attributeName} attribute is missing). You should set the User.${attributeName} attribute in order to make targeting work properly. Read more: https://configcat.com/docs/advanced/user-object/`
);
}

userObjectAttributeIsInvalid(condition: string, key: string, reason: string, attributeName: string): LogMessage {
return this.log(
LogLevel.Warn, 3004,
FormattableLogMessage.from(
"CONDITION", "KEY", "REASON", "ATTRIBUTE_NAME"
)`Cannot evaluate condition (${condition}) for setting '${key}' (${reason}). Please check the User.${attributeName} attribute and make sure that its value corresponds to the comparison operator.`
);
}

circularDependencyDetected(condition: string, key: string, dependencyCycle: string): LogMessage {
return this.log(
LogLevel.Warn, 3005,
FormattableLogMessage.from(
"CONDITION", "KEY", "DEPENDENCY_CYCLE"
)`Cannot evaluate condition (${condition}) for setting '${key}' (circular dependency detected between the following depending flags: ${dependencyCycle}). Please check your feature flag definition and eliminate the circular dependency.`
);
}

configServiceCannotInitiateHttpCalls(): LogMessage {
return this.log(
LogLevel.Warn, 3200,
Expand Down Expand Up @@ -304,7 +340,7 @@ export class LoggerWrapper implements IConfigCatLogger {

/* Common info messages (5000-5999) */

settingEvaluated(evaluateLog: object): LogMessage {
settingEvaluated(evaluateLog: string): LogMessage {
return this.log(
LogLevel.Info, 5000,
FormattableLogMessage.from(
Expand Down
Loading
Loading