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

Initial set up and evaluation logic for server side custom signals #2628

Merged
merged 7 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 33 additions & 3 deletions etc/firebase-admin.remote-config.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,44 @@ export interface AndCondition {
conditions?: Array<OneOfCondition>;
}

// @public
export interface CustomSignalCondition {
customSignalKey?: string;
customSignalOperator?: CustomSignalOperator;
targetCustomSignalValues?: string[];
}

// @public
export enum CustomSignalOperator {
NUMERIC_EQUAL = "NUMERIC_EQUAL",
NUMERIC_GREATER_EQUAL = "NUMERIC_GREATER_EQUAL",
NUMERIC_GREATER_THAN = "NUMERIC_GREATER_THAN",
NUMERIC_LESS_EQUAL = "NUMERIC_LESS_EQUAL",
NUMERIC_LESS_THAN = "NUMERIC_LESS_THAN",
NUMERIC_NOT_EQUAL = "NUMERIC_NOT_EQUAL",
SEMANTIC_VERSION_EQUAL = "SEMANTIC_VERSION_EQUAL",
SEMANTIC_VERSION_GREATER_EQUAL = "SEMANTIC_VERSION_GREATER_EQUAL",
SEMANTIC_VERSION_GREATER_THAN = "SEMANTIC_VERSION_GREATER_THAN",
SEMANTIC_VERSION_LESS_EQUAL = "SEMANTIC_VERSION_LESS_EQUAL",
SEMANTIC_VERSION_LESS_THAN = "SEMANTIC_VERSION_LESS_THAN",
SEMANTIC_VERSION_NOT_EQUAL = "SEMANTIC_VERSION_NOT_EQUAL",
STRING_CONTAINS = "STRING_CONTAINS",
STRING_CONTAINS_REGEX = "STRING_CONTAINS_REGEX",
STRING_DOES_NOT_CONTAIN = "STRING_DOES_NOT_CONTAIN",
STRING_EXACTLY_MATCHES = "STRING_EXACTLY_MATCHES",
UNKNOWN = "UNKNOWN"
}

// @public
export type DefaultConfig = {
[key: string]: string | number | boolean;
};

// Warning: (ae-forgotten-export) The symbol "UserProvidedSignals" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "PredefinedSignals" needs to be exported by the entry point index.d.ts
//
// @public
export type EvaluationContext = {
randomizationId?: string;
};
export type EvaluationContext = UserProvidedSignals & PredefinedSignals;

// @public
export interface ExplicitParameterValue {
Expand Down Expand Up @@ -78,6 +107,7 @@ export interface NamedCondition {
// @public
export interface OneOfCondition {
andCondition?: AndCondition;
customSignal?: CustomSignalCondition;
false?: Record<string, never>;
orCondition?: OrCondition;
percent?: PercentCondition;
Expand Down
74 changes: 72 additions & 2 deletions src/remote-config/condition-evaluator-internal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import {
NamedCondition,
OrCondition,
PercentCondition,
PercentConditionOperator
PercentConditionOperator,
CustomSignalCondition,
CustomSignalOperator,
} from './remote-config-api';
import * as farmhash from 'farmhash-modern';
import long = require('long');
Expand Down Expand Up @@ -77,6 +79,9 @@ export class ConditionEvaluator {
if (condition.percent) {
return this.evaluatePercentCondition(condition.percent, context);
}
if (condition.customSignal) {
return this.evaluateCustomSignalCondition(condition.customSignal, context);
}
// TODO: add logging once we have a wrapped logger.
return false;
}
Expand Down Expand Up @@ -158,7 +163,7 @@ export class ConditionEvaluator {
}

const instanceMicroPercentile = hash64.mod(100 * 1_000_000);

switch (percentOperator) {
case PercentConditionOperator.LESS_OR_EQUAL:
return instanceMicroPercentile.lte(normalizedMicroPercent);
Expand All @@ -175,4 +180,69 @@ export class ConditionEvaluator {
// TODO: add logging once we have a wrapped logger.
return false;
}

private evaluateCustomSignalCondition(
customSignalCondition: CustomSignalCondition,
context: EvaluationContext
): boolean {
const {
customSignalOperator,
customSignalKey,
targetCustomSignalValues,
} = customSignalCondition;

if (!customSignalOperator || !customSignalKey || !targetCustomSignalValues) {
// TODO: add logging once we have a wrapped logger.
return false;
}

if (!targetCustomSignalValues.length) {
return false;
}

// Extract the value of the signal from the evaluation context.
const actualCustomSignalValue = context[customSignalKey];

switch (customSignalOperator) {
case CustomSignalOperator.STRING_CONTAINS:
return compareStrings(
targetCustomSignalValues,
actualCustomSignalValue,
(target, actual) => actual.includes(target),
);
case CustomSignalOperator.STRING_DOES_NOT_CONTAIN:
return !compareStrings(
targetCustomSignalValues,
actualCustomSignalValue,
(target, actual) => actual.includes(target),
);
case CustomSignalOperator.STRING_EXACTLY_MATCHES:
return compareStrings(
targetCustomSignalValues,
actualCustomSignalValue,
(target, actual) => actual === target,
);
case CustomSignalOperator.STRING_CONTAINS_REGEX:
return compareStrings(
targetCustomSignalValues,
actualCustomSignalValue,
(target, actual) => new RegExp(target).test(actual),
);
// TODO: add comparison logic for additional operators here.
}

// TODO: add logging once we have a wrapped logger.
return false;
}
}

// Compares the actual string value of a signal against a list of target
// values. If any of the target values are a match, returns true.
function compareStrings(
targetValues: Array<string>,
actualValue: string|number,
predicateFn: (target: string, actual: string) => boolean
): boolean {
const actual = String(actualValue);
return targetValues.some((target) => predicateFn(target, actual));
}
2 changes: 2 additions & 0 deletions src/remote-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { RemoteConfig } from './remote-config';

export {
AndCondition,
CustomSignalCondition,
CustomSignalOperator,
DefaultConfig,
EvaluationContext,
ExplicitParameterValue,
Expand Down
136 changes: 134 additions & 2 deletions src/remote-config/remote-config-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export interface OneOfCondition {
* Makes this condition a percent condition.
*/
percent?: PercentCondition;

/**
* Makes this a custom signal condition.
*/
customSignal?: CustomSignalCondition;
}

/**
Expand Down Expand Up @@ -213,6 +218,123 @@ export interface PercentCondition {
microPercentRange?: MicroPercentRange;
}

/**
* Defines supported operators for custom signal conditions.
*/
export enum CustomSignalOperator {

/**
* A catchall error case.
*/
UNKNOWN = 'UNKNOWN',

/**
* The actual value is less than the target value.
*/
NUMERIC_LESS_THAN = 'NUMERIC_LESS_THAN',

/**
* The actual value is less than or equal to the target value.
*/
NUMERIC_LESS_EQUAL ='NUMERIC_LESS_EQUAL',

/**
* The actual value is equal to the target value.
*/
NUMERIC_EQUAL = 'NUMERIC_EQUAL',

/**
* The actual value is not equal to the target value.
*/
NUMERIC_NOT_EQUAL = 'NUMERIC_NOT_EQUAL',

/**
* The actual value is greater than the target value.
*/
NUMERIC_GREATER_THAN = 'NUMERIC_GREATER_THAN',

/**
* The actual value is greater than or equal to the target value.
*/
NUMERIC_GREATER_EQUAL = 'NUMERIC_GREATER_EQUAL',

/**
* AT LEAST ONE of the target values is a substring of the actual custom
* signal value. Eg: "abc" contains the string "a", "bc".
*/
STRING_CONTAINS = 'STRING_CONTAINS',

/**
* NONE of the target values is a substring of the actual custom signal value.
*/
STRING_DOES_NOT_CONTAIN = 'STRING_DOES_NOT_CONTAIN',

/**
* The actual value exactly matches AT LEAST ONE of the target values.
*/
STRING_EXACTLY_MATCHES = 'STRING_EXACTLY_MATCHES',

/**
* The target regular expression matches a portion of AT LEAST ONE of the
* actual values (or the entire string). The regex conforms to RE2 format.
* See https://github.com/google/re2/wiki/Syntax
*/
STRING_CONTAINS_REGEX = 'STRING_CONTAINS_REGEX',

/**
* The actual value is less than the target value.
*/
SEMANTIC_VERSION_LESS_THAN = 'SEMANTIC_VERSION_LESS_THAN',

/**
* The actual value is less than or equal to the target value.
*/
SEMANTIC_VERSION_LESS_EQUAL = 'SEMANTIC_VERSION_LESS_EQUAL',

/**
* The actual value is equal to the target value.
*/
SEMANTIC_VERSION_EQUAL = 'SEMANTIC_VERSION_EQUAL',

/**
* The actual value is not equal to the target value.
*/
SEMANTIC_VERSION_NOT_EQUAL = 'SEMANTIC_VERSION_NOT_EQUAL',

/**
* The actual value is greater than the target value.
*/
SEMANTIC_VERSION_GREATER_THAN = 'SEMANTIC_VERSION_GREATER_THAN',

/**
* The actual value is greater than or equal to the target value.
*/
SEMANTIC_VERSION_GREATER_EQUAL = 'SEMANTIC_VERSION_GREATER_EQUAL',
}

/**
* Represents a condition that compares provided signals against a target value.
*/
export interface CustomSignalCondition {

/**
* The choice of custom signal operator to determine how to compare targets
* to value(s).
*/
customSignalOperator?: CustomSignalOperator;

/**
* The key of the signal set in the EvaluationContext
*/
customSignalKey?: string;

/**
* A list of at most 100 target custom signal values. For numeric operators,
* this will have exactly ONE target value.
*/
targetCustomSignalValues?: string[];
}

/**
* Interface representing an explicit parameter value.
*/
Expand Down Expand Up @@ -414,9 +536,14 @@ export interface ServerTemplate {
}

/**
* Represents template evaluation input signals.
* Generic map of developer-defined signals used as evaluation input signals.
*/
export type UserProvidedSignals = {[key: string]: string|number};

/**
* Predefined template evaluation input signals.
*/
export type EvaluationContext = {
export type PredefinedSignals = {

/**
* Defines the identifier to use when splitting a group. For example,
Expand All @@ -425,6 +552,11 @@ export type EvaluationContext = {
randomizationId?: string
};

/**
* Represents template evaluation input signals.
*/
export type EvaluationContext = UserProvidedSignals & PredefinedSignals;

/**
* Interface representing a Remote Config user.
*/
Expand Down
Loading