vwo-fme-node-sdk
Version:
VWO Node/JavaScript SDK for Feature Management and Experimentation
332 lines (317 loc) • 14.2 kB
text/typescript
/**
* Copyright 2024-2025 Wingify Software Pvt. Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { getKeyValue, matchWithRegex } from '../utils/SegmentUtil';
import { SegmentOperandValueEnum } from '../enums/SegmentOperandValueEnum';
import { SegmentOperandRegexEnum } from '../enums/SegmentOperandRegexEnum';
import { isBoolean } from '../../../utils/DataTypeUtil';
import { dynamic } from '../../../types/Common';
import { getFromGatewayService } from '../../../utils/GatewayServiceUtil';
import { UrlEnum } from '../../../enums/UrlEnum';
import { LogManager } from '../../logger';
import { ContextModel } from '../../../models/user/ContextModel';
/**
* SegmentOperandEvaluator class provides methods to evaluate different types of DSL (Domain Specific Language)
* expressions based on the segment conditions defined for custom variables, user IDs, and user agents.
*/
export class SegmentOperandEvaluator {
/**
* Evaluates a custom variable DSL expression.
* @param {Record<string, dynamic>} dslOperandValue - The DSL expression for the custom variable.
* @param {Record<string, dynamic>} properties - The properties object containing the actual values to be matched against.
* @returns {Promise<boolean>} - A promise that resolves to a boolean indicating if the DSL condition is met.
*/
async evaluateCustomVariableDSL(
dslOperandValue: Record<string, dynamic>,
properties: Record<string, dynamic>,
): Promise<boolean> {
// Extract key and value from the DSL operand
const { key, value } = getKeyValue(dslOperandValue);
const operandKey = key;
const operand = value;
// Check if the property exists
if (!Object.prototype.hasOwnProperty.call(properties, operandKey)) {
return false;
}
// Handle 'inlist' operand
if (operand.includes('inlist')) {
const listIdRegex = /inlist\(([^)]+)\)/;
const match = operand.match(listIdRegex);
if (!match || match.length < 2) {
LogManager.Instance.error("Invalid 'inList' operand format");
return false;
}
// Process the tag value and prepare query parameters
const tagValue = properties[operandKey];
const attributeValue = this.preProcessTagValue(tagValue);
const listId = match[1];
const queryParamsObj = {
attribute: attributeValue,
listId: listId,
};
// Make a web service call to check the attribute against the list
try {
const res = await getFromGatewayService(queryParamsObj, UrlEnum.ATTRIBUTE_CHECK);
if (!res || res === undefined || res === 'false' || res.status === 0) {
return false;
}
return res;
} catch (error) {
LogManager.Instance.error('Error while fetching data: ' + error);
return false;
}
} else {
// Process other types of operands
let tagValue = properties[operandKey];
tagValue = this.preProcessTagValue(tagValue);
const { operandType, operandValue } = this.preProcessOperandValue(operand);
const processedValues = this.processValues(operandValue, tagValue);
tagValue = processedValues.tagValue;
return this.extractResult(operandType, processedValues.operandValue, tagValue);
}
}
/**
* Evaluates a user DSL expression to check if a user ID is in a specified list.
* @param {Record<string, any>} dslOperandValue - The DSL expression containing user IDs.
* @param {Record<string, dynamic>} properties - The properties object containing the actual user ID to check.
* @returns {boolean} - True if the user ID is in the list, otherwise false.
*/
evaluateUserDSL(dslOperandValue: Record<string, any>, properties: Record<string, dynamic>): boolean {
const users = dslOperandValue.split(',');
for (let i = 0; i < users.length; i++) {
if (users[i].trim() == properties._vwoUserId) {
return true;
}
}
return false;
}
/**
* Evaluates a user agent DSL expression.
* @param {Record<string, any>} dslOperandValue - The DSL expression for the user agent.
* @param {any} context - The context object containing the user agent string.
* @returns {boolean} - True if the user agent matches the DSL condition, otherwise false.
*/
evaluateUserAgentDSL(dslOperandValue: Record<string, any>, context: ContextModel): boolean {
const operand = dslOperandValue;
if (!context.getUserAgent() || context.getUserAgent() === undefined) {
LogManager.Instance.info('To Evaluate UserAgent segmentation, please provide userAgent in context');
return false;
}
let tagValue = decodeURIComponent(context.getUserAgent());
const { operandType, operandValue } = this.preProcessOperandValue(operand);
const processedValues = this.processValues(operandValue, tagValue);
tagValue = processedValues.tagValue as string; // Fix: Type assertion to ensure tagValue is of type string
return this.extractResult(operandType, processedValues.operandValue, tagValue);
}
/**
* Pre-processes the tag value to ensure it is in the correct format for evaluation.
* @param {any} tagValue - The value to be processed.
* @returns {string | boolean} - The processed tag value, either as a string or a boolean.
*/
preProcessTagValue(tagValue: any): string | boolean {
// Default to empty string if undefined
if (tagValue === undefined) {
tagValue = '';
}
// Convert boolean values to boolean type
if (isBoolean(tagValue)) {
tagValue = tagValue ? true : false;
}
// Convert all non-null values to string
if (tagValue !== null) {
tagValue = tagValue.toString();
}
return tagValue;
}
/**
* Pre-processes the operand value to determine its type and extract the value based on regex matches.
* @param {any} operand - The operand to be processed.
* @returns {Record<string, any>} - An object containing the operand type and value.
*/
preProcessOperandValue(operand: any): Record<string, any> {
let operandType: SegmentOperandValueEnum;
let operandValue: dynamic;
// Determine the type of operand and extract value based on regex patterns
if (matchWithRegex(operand, SegmentOperandRegexEnum.LOWER_MATCH)) {
operandType = SegmentOperandValueEnum.LOWER_VALUE;
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.LOWER_MATCH);
} else if (matchWithRegex(operand, SegmentOperandRegexEnum.WILDCARD_MATCH)) {
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.WILDCARD_MATCH);
const startingStar = matchWithRegex(operandValue, SegmentOperandRegexEnum.STARTING_STAR);
const endingStar = matchWithRegex(operandValue, SegmentOperandRegexEnum.ENDING_STAR);
// Determine specific wildcard type
if (startingStar && endingStar) {
operandType = SegmentOperandValueEnum.STARTING_ENDING_STAR_VALUE;
} else if (startingStar) {
operandType = SegmentOperandValueEnum.STARTING_STAR_VALUE;
} else if (endingStar) {
operandType = SegmentOperandValueEnum.ENDING_STAR_VALUE;
}
// Remove wildcard characters from the operand value
operandValue = operandValue
.replace(new RegExp(SegmentOperandRegexEnum.STARTING_STAR), '')
.replace(new RegExp(SegmentOperandRegexEnum.ENDING_STAR), '');
} else if (matchWithRegex(operand, SegmentOperandRegexEnum.REGEX_MATCH)) {
operandType = SegmentOperandValueEnum.REGEX_VALUE;
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.REGEX_MATCH);
} else if (matchWithRegex(operand, SegmentOperandRegexEnum.GREATER_THAN_MATCH)) {
operandType = SegmentOperandValueEnum.GREATER_THAN_VALUE;
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.GREATER_THAN_MATCH);
} else if (matchWithRegex(operand, SegmentOperandRegexEnum.GREATER_THAN_EQUAL_TO_MATCH)) {
operandType = SegmentOperandValueEnum.GREATER_THAN_EQUAL_TO_VALUE;
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.GREATER_THAN_EQUAL_TO_MATCH);
} else if (matchWithRegex(operand, SegmentOperandRegexEnum.LESS_THAN_MATCH)) {
operandType = SegmentOperandValueEnum.LESS_THAN_VALUE;
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.LESS_THAN_MATCH);
} else if (matchWithRegex(operand, SegmentOperandRegexEnum.LESS_THAN_EQUAL_TO_MATCH)) {
operandType = SegmentOperandValueEnum.LESS_THAN_EQUAL_TO_VALUE;
operandValue = this.extractOperandValue(operand, SegmentOperandRegexEnum.LESS_THAN_EQUAL_TO_MATCH);
} else {
operandType = SegmentOperandValueEnum.EQUAL_VALUE;
operandValue = operand;
}
return {
operandType,
operandValue,
};
}
/**
* Extracts the operand value from a string based on a specified regex pattern.
* @param {any} operand - The operand string to extract from.
* @param {string} regex - The regex pattern to use for extraction.
* @returns {string} - The extracted value.
*/
extractOperandValue(operand: any, regex: string): string {
// Match operand with regex and return the first capturing group
return matchWithRegex(operand, regex) && matchWithRegex(operand, regex)[1];
}
/**
* Processes numeric values from operand and tag values, converting them to strings.
* @param {any} operandValue - The operand value to process.
* @param {any} tagValue - The tag value to process.
* @returns {Record<string, dynamic>} - An object containing the processed operand and tag values as strings.
*/
processValues(operandValue: any, tagValue: any): Record<string, dynamic> {
// Convert operand and tag values to floats
const processedOperandValue = parseFloat(operandValue);
const processedTagValue = parseFloat(tagValue);
// Return original values if conversion fails
if (!processedOperandValue || !processedTagValue) {
return {
operandValue: operandValue,
tagValue: tagValue,
};
}
// now we have surity that both are numbers
// now we can convert them independently to int type if they
// are int rather than floats
// if (processedOperandValue === Math.floor(processedOperandValue)) {
// processedOperandValue = parseInt(processedOperandValue, 10);
// }
// if (processedTagValue === Math.floor(processedTagValue)) {
// processedTagValue = parseInt(processedTagValue, 10);
// }
// Convert numeric values back to strings
return {
operandValue: processedOperandValue.toString(),
tagValue: processedTagValue.toString(),
};
}
/**
* Extracts the result of the evaluation based on the operand type and values.
* @param {SegmentOperandValueEnum} operandType - The type of the operand.
* @param {any} operandValue - The value of the operand.
* @param {any} tagValue - The value of the tag to compare against.
* @returns {boolean} - The result of the evaluation.
*/
extractResult(operandType: SegmentOperandValueEnum, operandValue: any, tagValue: any): boolean {
let result: boolean;
switch (operandType) {
case SegmentOperandValueEnum.LOWER_VALUE:
// Check if both values are equal, ignoring case
if (tagValue !== null) {
result = operandValue.toLowerCase() === tagValue.toLowerCase();
}
break;
case SegmentOperandValueEnum.STARTING_ENDING_STAR_VALUE:
// Check if the tagValue contains the operandValue
if (tagValue !== null) {
result = tagValue.indexOf(operandValue) > -1;
}
break;
case SegmentOperandValueEnum.STARTING_STAR_VALUE:
// Check if the tagValue ends with the operandValue
if (tagValue !== null) {
result = tagValue.endsWith(operandValue);
}
break;
case SegmentOperandValueEnum.ENDING_STAR_VALUE:
// Check if the tagValue starts with the operandValue
if (tagValue !== null) {
result = tagValue.startsWith(operandValue);
}
break;
case SegmentOperandValueEnum.REGEX_VALUE:
// Evaluate the tagValue against the regex pattern of operandValue
try {
const pattern = new RegExp(operandValue, 'g');
result = !!pattern.test(tagValue);
} catch (err) {
result = false;
}
break;
case SegmentOperandValueEnum.GREATER_THAN_VALUE:
if (tagValue !== null) {
try {
result = parseFloat(operandValue) < parseFloat(tagValue);
} catch (err) {
result = false;
}
}
break;
case SegmentOperandValueEnum.GREATER_THAN_EQUAL_TO_VALUE:
if (tagValue !== null) {
try {
result = parseFloat(operandValue) <= parseFloat(tagValue);
} catch (err) {
result = false;
}
}
break;
case SegmentOperandValueEnum.LESS_THAN_VALUE:
if (tagValue !== null) {
try {
result = parseFloat(operandValue) > parseFloat(tagValue);
} catch (err) {
result = false;
}
}
break;
case SegmentOperandValueEnum.LESS_THAN_EQUAL_TO_VALUE:
if (tagValue !== null) {
try {
result = parseFloat(operandValue) >= parseFloat(tagValue);
} catch (err) {
result = false;
}
}
break;
default:
// Check if the tagValue is exactly equal to the operandValue
result = tagValue === operandValue;
}
return result;
}
}