From f69cdc6ab2758a21b4b06dbdbc37b201d6ce5461 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Mon, 25 Mar 2024 16:59:33 -0700
Subject: [PATCH 01/39] wip

---
 src/assignment-cache.ts    |   7 +-
 src/assignment-logger.ts   |  19 +-
 src/client/eppo-client.ts  | 438 +++++++++++------------------------
 src/configuration-store.ts |   1 +
 src/dto/rule-dto.ts        |   1 -
 src/eppo_value.ts          |  75 +++---
 src/eval.spec.ts           | 458 +++++++++++++++++++++++++++++++++++++
 src/eval.ts                |  92 ++++++++
 src/interfaces.ts          |  48 ++++
 src/rule_evaluator.spec.ts | 306 ++-----------------------
 src/rule_evaluator.ts      |  17 +-
 src/shard.ts               |  15 --
 src/sharders.ts            |  32 +++
 src/types.ts               |   4 +
 14 files changed, 840 insertions(+), 673 deletions(-)
 create mode 100644 src/eval.spec.ts
 create mode 100644 src/eval.ts
 create mode 100644 src/interfaces.ts
 delete mode 100644 src/shard.ts
 create mode 100644 src/sharders.ts
 create mode 100644 src/types.ts

diff --git a/src/assignment-cache.ts b/src/assignment-cache.ts
index f63eb870..aa402e41 100644
--- a/src/assignment-cache.ts
+++ b/src/assignment-cache.ts
@@ -1,12 +1,13 @@
 import { LRUCache } from 'lru-cache';
 
 import { EppoValue } from './eppo_value';
+import { getMD5Hash } from './obfuscation';
 
 export interface AssignmentCacheKey {
   subjectKey: string;
   flagKey: string;
   allocationKey: string;
-  variationValue: EppoValue;
+  variationKey: string;
 }
 
 export interface Cacheable {
@@ -32,7 +33,7 @@ export abstract class AssignmentCache<T extends Cacheable> {
     // the subject has been assigned to a different variation
     // than was previously logged.
     // in this case we need to log the assignment again.
-    if (this.cache.get(this.getCacheKey(key)) !== key.variationValue.toHashedString()) {
+    if (this.cache.get(this.getCacheKey(key)) !== getMD5Hash(key.variationKey)) {
       return false;
     }
 
@@ -40,7 +41,7 @@ export abstract class AssignmentCache<T extends Cacheable> {
   }
 
   setLastLoggedAssignment(key: AssignmentCacheKey): void {
-    this.cache.set(this.getCacheKey(key), key.variationValue.toHashedString());
+    this.cache.set(this.getCacheKey(key), getMD5Hash(key.variationKey));
   }
 
   protected getCacheKey({ subjectKey, flagKey, allocationKey }: AssignmentCacheKey): string {
diff --git a/src/assignment-logger.ts b/src/assignment-logger.ts
index 6f88d7f2..d1f3a73f 100644
--- a/src/assignment-logger.ts
+++ b/src/assignment-logger.ts
@@ -13,12 +13,12 @@ export interface IAssignmentEvent {
   /**
    * An Eppo allocation key
    */
-  allocation: string;
+  allocation: string | null;
 
   /**
    * An Eppo experiment key
    */
-  experiment: string;
+  experiment: string | null;
 
   /**
    * An Eppo feature flag key
@@ -28,7 +28,7 @@ export interface IAssignmentEvent {
   /**
    * The assigned variation
    */
-  variation: string;
+  variation: string | null;
 
   /**
    * The entity or user that was assigned to a variation
@@ -40,18 +40,13 @@ export interface IAssignmentEvent {
    */
   timestamp: string;
 
-  /**
-   * An Eppo holdout key
-   */
-  holdout: string | null;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  subjectAttributes: Record<string, any>;
 
   /**
-   * The Eppo holdout variation for the assigned variation
+   * For additional fields to log
    */
-  holdoutVariation: NullableHoldoutVariationType;
-
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  subjectAttributes: Record<string, any>;
+  [propName: string]: string;
 }
 
 /**
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 99405bc7..f3cb4dd8 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -27,12 +27,12 @@ import { IAllocation } from '../dto/allocation-dto';
 import { IExperimentConfiguration } from '../dto/experiment-configuration-dto';
 import { IVariation } from '../dto/variation-dto';
 import { EppoValue, ValueType } from '../eppo_value';
+import { Evaluator, FlagEvaluation, noneResult } from '../eval';
 import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
 import HttpClient from '../http-client';
+import { Flag, VariationType } from '../interfaces';
 import { getMD5Hash } from '../obfuscation';
 import initPoller, { IPoller } from '../poller';
-import { findMatchingRule } from '../rule_evaluator';
-import { getShard, isShardInRange } from '../shard';
 import { validateNotBlank } from '../validation';
 
 /**
@@ -40,25 +40,6 @@ import { validateNotBlank } from '../validation';
  * @public
  */
 export interface IEppoClient {
-  /**
-   * Maps a subject to a variation for a given experiment.
-   *
-   * @param subjectKey an identifier of the experiment subject, for example a user ID.
-   * @param flagKey feature flag identifier
-   * @param subjectAttributes optional attributes associated with the subject, for example name and email.
-   * The subject attributes are used for evaluating any targeting rules tied to the experiment.
-   * @param assignmentHooks optional interface for pre and post assignment hooks
-   * @returns a variation value if the subject is part of the experiment sample, otherwise null
-   * @public
-   */
-  getAssignment(
-    subjectKey: string,
-    flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes?: Record<string, any>,
-    assignmentHooks?: IAssignmentHooks,
-  ): string | null;
-
   /**
    * Maps a subject to a variation for a given experiment.
    *
@@ -74,6 +55,7 @@ export interface IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes?: Record<string, any>,
+    defaultValue?: string | null,
     assignmentHooks?: IAssignmentHooks,
   ): string | null;
 
@@ -82,6 +64,7 @@ export interface IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes?: Record<string, any>,
+    defaultValue?: boolean | null,
     assignmentHooks?: IAssignmentHooks,
   ): boolean | null;
 
@@ -90,6 +73,7 @@ export interface IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes?: Record<string, any>,
+    defaultValue?: number | null,
     assignmentHooks?: IAssignmentHooks,
   ): number | null;
 
@@ -98,6 +82,7 @@ export interface IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes?: Record<string, any>,
+    defaultValue?: string | null,
     assignmentHooks?: IAssignmentHooks,
   ): string | null;
 
@@ -106,6 +91,7 @@ export interface IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes?: Record<string, any>,
+    defaultValue?: object | null,
     assignmentHooks?: IAssignmentHooks,
   ): object | null;
 
@@ -147,11 +133,14 @@ export default class EppoClient implements IEppoClient {
   private configurationStore: IConfigurationStore;
   private configurationRequestParameters: ExperimentConfigurationRequestParameters | undefined;
   private requestPoller: IPoller | undefined;
+  private evaluator: Evaluator;
 
   constructor(
+    evaluator: Evaluator,
     configurationStore: IConfigurationStore,
     configurationRequestParameters?: ExperimentConfigurationRequestParameters,
   ) {
+    this.evaluator = evaluator;
     this.configurationStore = configurationStore;
     this.configurationRequestParameters = configurationRequestParameters;
   }
@@ -216,52 +205,26 @@ export default class EppoClient implements IEppoClient {
     }
   }
 
-  // @deprecated getAssignment is deprecated in favor of the typed get<Type>Assignment methods
-  public getAssignment(
-    subjectKey: string,
-    flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> = {},
-    assignmentHooks?: IAssignmentHooks | undefined,
-    obfuscated = false,
-  ): string | null {
-    try {
-      return (
-        this.getAssignmentVariation(
-          subjectKey,
-          flagKey,
-          subjectAttributes,
-          assignmentHooks,
-          obfuscated,
-        ).stringValue ?? null
-      );
-    } catch (error) {
-      return this.rethrowIfNotGraceful(error);
-    }
-  }
-
   public getStringAssignment(
     subjectKey: string,
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes: Record<string, any> = {},
+    defaultValue?: string | null,
     assignmentHooks?: IAssignmentHooks | undefined,
     obfuscated = false,
   ): string | null {
-    try {
-      return (
-        this.getAssignmentVariation(
-          subjectKey,
-          flagKey,
-          subjectAttributes,
-          assignmentHooks,
-          obfuscated,
-          ValueType.StringType,
-        ).stringValue ?? null
-      );
-    } catch (error) {
-      return this.rethrowIfNotGraceful(error);
-    }
+    return (
+      this.getAssignmentVariation(
+        subjectKey,
+        flagKey,
+        subjectAttributes,
+        defaultValue ? EppoValue.String(defaultValue) : EppoValue.Null(),
+        assignmentHooks,
+        obfuscated,
+        VariationType.STRING,
+      ).stringValue ?? null
+    );
   }
 
   getBoolAssignment(
@@ -269,70 +232,42 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes: Record<string, any> = {},
+    defaultValue: boolean | null = null,
     assignmentHooks?: IAssignmentHooks | undefined,
     obfuscated = false,
   ): boolean | null {
-    try {
-      return (
-        this.getAssignmentVariation(
-          subjectKey,
-          flagKey,
-          subjectAttributes,
-          assignmentHooks,
-          obfuscated,
-          ValueType.BoolType,
-        ).boolValue ?? null
-      );
-    } catch (error) {
-      return this.rethrowIfNotGraceful(error);
-    }
+    return (
+      this.getAssignmentVariation(
+        subjectKey,
+        flagKey,
+        subjectAttributes,
+        defaultValue ? EppoValue.Bool(defaultValue) : EppoValue.Null(),
+        assignmentHooks,
+        obfuscated,
+        VariationType.BOOLEAN,
+      ).boolValue ?? null
+    );
   }
 
   getNumericAssignment(
     subjectKey: string,
     flagKey: string,
     subjectAttributes?: Record<string, EppoValue>,
+    defaultValue?: number | null,
     assignmentHooks?: IAssignmentHooks | undefined,
     obfuscated = false,
   ): number | null {
-    try {
-      return (
-        this.getAssignmentVariation(
-          subjectKey,
-          flagKey,
-          subjectAttributes,
-          assignmentHooks,
-          obfuscated,
-          ValueType.NumericType,
-        ).numericValue ?? null
-      );
-    } catch (error) {
-      return this.rethrowIfNotGraceful(error);
-    }
-  }
-
-  public getJSONStringAssignment(
-    subjectKey: string,
-    flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> = {},
-    assignmentHooks?: IAssignmentHooks | undefined,
-    obfuscated = false,
-  ): string | null {
-    try {
-      return (
-        this.getAssignmentVariation(
-          subjectKey,
-          flagKey,
-          subjectAttributes,
-          assignmentHooks,
-          obfuscated,
-          ValueType.JSONType,
-        ).stringValue ?? null
-      );
-    } catch (error) {
-      return this.rethrowIfNotGraceful(error);
-    }
+    return (
+      this.getAssignmentVariation(
+        subjectKey,
+        flagKey,
+        subjectAttributes,
+        defaultValue ? EppoValue.Numeric(defaultValue) : EppoValue.Null(),
+        assignmentHooks,
+        obfuscated,
+        VariationType.FLOAT,
+      ).numericValue ?? null
+    );
   }
 
   public getParsedJSONAssignment(
@@ -340,29 +275,27 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes: Record<string, any> = {},
+    defaultValue?: object | null,
     assignmentHooks?: IAssignmentHooks | undefined,
     obfuscated = false,
   ): object | null {
-    try {
-      return (
-        this.getAssignmentVariation(
-          subjectKey,
-          flagKey,
-          subjectAttributes,
-          assignmentHooks,
-          obfuscated,
-          ValueType.JSONType,
-        ).objectValue ?? null
-      );
-    } catch (error) {
-      return this.rethrowIfNotGraceful(error);
-    }
+    return (
+      this.getAssignmentVariation(
+        subjectKey,
+        flagKey,
+        subjectAttributes,
+        defaultValue ? EppoValue.JSON(defaultValue) : EppoValue.Null(),
+        assignmentHooks,
+        obfuscated,
+        VariationType.JSON,
+      ).objectValue ?? null
+    );
   }
 
-  private rethrowIfNotGraceful(err: Error): null {
+  private rethrowIfNotGraceful(err: Error, defaultValue?: EppoValue): EppoValue {
     if (this.isGracefulFailureMode) {
       console.error(`[Eppo SDK] Error getting assignment: ${err.message}`);
-      return null;
+      return defaultValue ?? EppoValue.Null();
     }
     throw err;
   }
@@ -372,147 +305,82 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes: Record<string, any> = {},
+    defaultValue: EppoValue,
     assignmentHooks: IAssignmentHooks | undefined,
     obfuscated: boolean,
-    valueType?: ValueType,
+    expectedVariationType: VariationType,
   ): EppoValue {
-    const { allocationKey, assignment, holdoutKey, holdoutVariation } = this.getAssignmentInternal(
-      subjectKey,
-      flagKey,
-      subjectAttributes,
-      assignmentHooks,
-      obfuscated,
-      valueType,
-    );
-    assignmentHooks?.onPostAssignment(flagKey, subjectKey, assignment, allocationKey);
-
-    if (!assignment.isNullType() && allocationKey !== null)
-      this.logAssignment(
-        flagKey,
-        allocationKey,
-        assignment,
+    try {
+      const result = this.getAssignmentDetail(
         subjectKey,
-        holdoutKey,
-        holdoutVariation,
+        flagKey,
         subjectAttributes,
+        expectedVariationType,
       );
 
-    return assignment;
+      // TODO: figure out whether to translate VariationType to EppoValueType or not
+      return EppoValue.generateEppoValue(result.variation?.value, expectedVariationType);
+    } catch (error) {
+      return this.rethrowIfNotGraceful(error);
+    }
   }
 
-  private getAssignmentInternal(
+  public getAssignmentDetail(
     subjectKey: string,
     flagKey: string,
-    subjectAttributes = {},
-    assignmentHooks: IAssignmentHooks | undefined,
-    obfuscated: boolean,
-    expectedValueType?: ValueType,
-  ): {
-    allocationKey: string | null;
-    assignment: EppoValue;
-    holdoutKey: string | null;
-    holdoutVariation: NullableHoldoutVariationType;
-  } {
+    subjectAttributes: Record<string, any> = {},
+    expectedVariationType?: VariationType,
+    obfuscated = false,
+  ): FlagEvaluation {
     validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
     validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
 
-    const nullAssignment = {
-      allocationKey: null,
-      assignment: EppoValue.Null(),
-      holdoutKey: null,
-      holdoutVariation: null,
-    };
+    const flag: Flag = this.configurationStore.get(flagKey);
 
-    const experimentConfig = this.configurationStore.get<IExperimentConfiguration>(
-      obfuscated ? getMD5Hash(flagKey) : flagKey,
-    );
-    const allowListOverride = this.getSubjectVariationOverride(
-      subjectKey,
-      experimentConfig,
-      expectedValueType,
-    );
-
-    if (!allowListOverride.isNullType()) {
-      if (!allowListOverride.isExpectedType()) {
-        return nullAssignment;
-      }
-      return { ...nullAssignment, assignment: allowListOverride };
+    if (flag === null) {
+      console.warn(`[Eppo SDK] No assigned variation. Flag not found: ${flagKey}`);
+      // note: this is different from the Python SDK, which returns None instead
+      return noneResult(flagKey, subjectKey, subjectAttributes);
     }
 
-    // Check for disabled flag.
-    if (!experimentConfig?.enabled) return nullAssignment;
+    if (!this.checkTypeMatch(expectedVariationType, flag.variationType)) {
+      throw new TypeError(
+        `Variation value does not have the correct type. Found: ${flag.variationType} != ${expectedVariationType}`,
+      );
+    }
 
-    // check for overridden assignment via hook
-    const overriddenAssignment = assignmentHooks?.onPreAssignment(flagKey, subjectKey);
-    if (overriddenAssignment !== null && overriddenAssignment !== undefined) {
-      if (!overriddenAssignment.isExpectedType()) return nullAssignment;
-      return { ...nullAssignment, assignment: overriddenAssignment };
+    if (!flag.enabled) {
+      console.info(`[Eppo SDK] No assigned variation. Flag is disabled: ${flagKey}`);
+      // note: this is different from the Python SDK, which returns None instead
+      return noneResult(flagKey, subjectKey, subjectAttributes);
     }
 
-    // Attempt to match a rule from the list.
-    const matchedRule = findMatchingRule(
-      subjectAttributes || {},
-      experimentConfig.rules,
-      obfuscated,
-    );
-    if (!matchedRule) return nullAssignment;
-
-    // Check if subject is in allocation sample.
-    const allocation = experimentConfig.allocations[matchedRule.allocationKey];
-    if (!this.isInExperimentSample(subjectKey, flagKey, experimentConfig, allocation))
-      return nullAssignment;
-
-    // Compute variation for subject.
-    const { subjectShards } = experimentConfig;
-    const { variations, holdouts, statusQuoVariationKey, shippedVariationKey } = allocation;
-
-    let assignedVariation: IVariation | undefined;
-    let holdoutVariation = null;
-
-    const holdoutShard = getShard(`holdout-${subjectKey}`, subjectShards);
-    const matchingHoldout = holdouts?.find((holdout) => {
-      const { statusQuoShardRange, shippedShardRange } = holdout;
-      if (isShardInRange(holdoutShard, statusQuoShardRange)) {
-        assignedVariation = variations.find(
-          (variation) => variation.variationKey === statusQuoVariationKey,
-        );
-        // Only log the holdout variation if this is a rollout allocation
-        // Only rollout allocations have shippedShardRange specified
-        if (shippedShardRange) {
-          holdoutVariation = HoldoutVariationEnum.STATUS_QUO;
-        }
-      } else if (shippedShardRange && isShardInRange(holdoutShard, shippedShardRange)) {
-        assignedVariation = variations.find(
-          (variation) => variation.variationKey === shippedVariationKey,
-        );
-        holdoutVariation = HoldoutVariationEnum.ALL_SHIPPED;
+    const result = this.evaluator.evaluateFlag(flag, subjectKey, subjectAttributes, obfuscated);
+
+    try {
+      if (result && result.doLog) {
+        // TODO: check assignment cache
+        this.logAssignment(assignmentEvent);
       }
-      return assignedVariation;
-    });
-    const holdoutKey = matchingHoldout?.holdoutKey ?? null;
-    if (!matchingHoldout) {
-      const assignmentShard = getShard(`assignment-${subjectKey}-${flagKey}`, subjectShards);
-      assignedVariation = variations.find((variation) =>
-        isShardInRange(assignmentShard, variation.shardRange),
-      );
+    } catch (error) {
+      console.error(`[Eppo SDK] Error logging assignment event: ${error}`);
     }
 
-    const internalAssignment: {
-      allocationKey: string;
-      assignment: EppoValue;
-      holdoutKey: string | null;
-      holdoutVariation: NullableHoldoutVariationType;
-    } = {
-      allocationKey: matchedRule.allocationKey,
-      assignment: EppoValue.generateEppoValue(
-        expectedValueType,
-        assignedVariation?.value,
-        assignedVariation?.typedValue,
-      ),
-      holdoutKey,
-      holdoutVariation: holdoutVariation as NullableHoldoutVariationType,
-    };
-    return internalAssignment.assignment.isExpectedType() ? internalAssignment : nullAssignment;
+    return result;
+  }
+
+  private checkTypeMatch(expectedType?: VariationType, actualType?: VariationType): boolean {
+    return expectedType === undefined || actualType === expectedType;
+  }
+
+  public get_flag_keys() {
+    /**
+     * Returns a list of all flag keys that have been initialized.
+     * This can be useful to debug the initialization process.
+     *
+     * Note that it is generally not a good idea to pre-load all flag configurations.
+     */
+    return this.configurationStore.getKeys();
   }
 
   public setLogger(logger: IAssignmentLogger) {
@@ -555,38 +423,31 @@ export default class EppoClient implements IEppoClient {
     }
   }
 
-  private logAssignment(
-    flagKey: string,
-    allocationKey: string,
-    variation: EppoValue,
-    subjectKey: string,
-    holdout: string | null,
-    holdoutVariation: NullableHoldoutVariationType,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> | undefined = {},
-  ) {
+  private logAssignment(result: FlagEvaluation) {
+    const event: IAssignmentEvent = {
+      ...(result.extraLogging ?? {}),
+      allocation: result.allocationKey ?? null,
+      experiment: result.allocationKey ? `${result.flagKey}-${result.allocationKey}` : null,
+      featureFlag: result.flagKey,
+      variation: result.variation?.key ?? null,
+      subject: result.subjectKey,
+      timestamp: new Date().toISOString(),
+      subjectAttributes: result.subjectAttributes,
+    };
+
     if (
+      result.variation &&
+      result.allocationKey &&
       this.assignmentCache?.hasLoggedAssignment({
-        flagKey,
-        subjectKey,
-        allocationKey,
-        variationValue: variation,
+        flagKey: result.flagKey,
+        subjectKey: result.subjectKey,
+        allocationKey: result.allocationKey,
+        variationKey: result.variation.key,
       })
     ) {
       return;
     }
 
-    const event: IAssignmentEvent = {
-      allocation: allocationKey,
-      experiment: `${flagKey}-${allocationKey}`,
-      featureFlag: flagKey,
-      variation: variation.toString(), // return the string representation to the logging callback
-      timestamp: new Date().toISOString(),
-      subject: subjectKey,
-      holdout,
-      holdoutVariation,
-      subjectAttributes,
-    };
     // assignment logger may be null while waiting for initialization
     if (this.assignmentLogger == null) {
       this.queuedEvents.length < MAX_EVENT_QUEUE_SIZE && this.queuedEvents.push(event);
@@ -595,42 +456,13 @@ export default class EppoClient implements IEppoClient {
     try {
       this.assignmentLogger.logAssignment(event);
       this.assignmentCache?.setLastLoggedAssignment({
-        flagKey,
-        subjectKey,
-        allocationKey,
-        variationValue: variation,
+        flagKey: result.flagKey,
+        subjectKey: result.subjectKey,
+        allocationKey: result.allocationKey ?? null,
+        variationKey: result.variation?.key ?? null,
       });
     } catch (error) {
       console.error(`[Eppo SDK] Error logging assignment event: ${error.message}`);
     }
   }
-
-  private getSubjectVariationOverride(
-    subjectKey: string,
-    experimentConfig: IExperimentConfiguration,
-    expectedValueType?: ValueType,
-  ): EppoValue {
-    const subjectHash = md5(subjectKey);
-    const override = experimentConfig?.overrides && experimentConfig.overrides[subjectHash];
-    const typedOverride =
-      experimentConfig?.typedOverrides && experimentConfig.typedOverrides[subjectHash];
-    return EppoValue.generateEppoValue(expectedValueType, override, typedOverride);
-  }
-
-  /**
-   * This checks whether the subject is included in the experiment sample.
-   * It is used to determine whether the subject should be assigned to a variant.
-   * Given a hash function output (bucket), check whether the bucket is between 0 and exposure_percent * total_buckets.
-   */
-  private isInExperimentSample(
-    subjectKey: string,
-    flagKey: string,
-    experimentConfig: IExperimentConfiguration,
-    allocation: IAllocation,
-  ): boolean {
-    const { subjectShards } = experimentConfig;
-    const { percentExposure } = allocation;
-    const shard = getShard(`exposure-${subjectKey}-${flagKey}`, subjectShards);
-    return shard <= percentExposure * subjectShards;
-  }
 }
diff --git a/src/configuration-store.ts b/src/configuration-store.ts
index 90e14804..e2116dc1 100644
--- a/src/configuration-store.ts
+++ b/src/configuration-store.ts
@@ -1,4 +1,5 @@
 export interface IConfigurationStore {
   get<T>(key: string): T;
+  getKeys(): string[];
   setEntries<T>(entries: Record<string, T>): void;
 }
diff --git a/src/dto/rule-dto.ts b/src/dto/rule-dto.ts
index aba56c2e..c72f0cef 100644
--- a/src/dto/rule-dto.ts
+++ b/src/dto/rule-dto.ts
@@ -23,6 +23,5 @@ export interface Condition {
 }
 
 export interface IRule {
-  allocationKey: string;
   conditions: Condition[];
 }
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index 4f2d42bf..203c2ac3 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -1,6 +1,6 @@
 import { getMD5Hash } from './obfuscation';
 
-export enum ValueType {
+export enum EppoValueType {
   NullType,
   BoolType,
   NumericType,
@@ -11,14 +11,14 @@ export enum ValueType {
 export type IValue = boolean | number | string | undefined;
 
 export class EppoValue {
-  public valueType: ValueType;
+  public valueType: EppoValueType;
   public boolValue: boolean | undefined;
   public numericValue: number | undefined;
   public stringValue: string | undefined;
   public objectValue: object | undefined;
 
   private constructor(
-    valueType: ValueType,
+    valueType: EppoValueType,
     boolValue: boolean | undefined,
     numericValue: number | undefined,
     stringValue: string | undefined,
@@ -32,20 +32,19 @@ export class EppoValue {
   }
 
   static generateEppoValue(
-    expectedValueType?: ValueType,
-    value?: string,
-    typedValue?: boolean | number | string | object,
+    value: boolean | number | string | object | null | undefined,
+    valueType: EppoValueType,
   ): EppoValue {
-    if (value != null && typedValue != null) {
-      switch (expectedValueType) {
-        case ValueType.BoolType:
-          return EppoValue.Bool(typedValue as boolean);
-        case ValueType.NumericType:
-          return EppoValue.Numeric(typedValue as number);
-        case ValueType.StringType:
-          return EppoValue.String(typedValue as string);
-        case ValueType.JSONType:
-          return EppoValue.JSON(value, typedValue as object);
+    if (value != null && value != undefined) {
+      switch (valueType) {
+        case EppoValueType.BoolType:
+          return EppoValue.Bool(value as boolean);
+        case EppoValueType.NumericType:
+          return EppoValue.Numeric(value as number);
+        case EppoValueType.StringType:
+          return EppoValue.String(value as string);
+        case EppoValueType.JSONType:
+          return EppoValue.JSON(value as object);
         default:
           return EppoValue.String(value as string);
       }
@@ -55,15 +54,15 @@ export class EppoValue {
 
   toString(): string {
     switch (this.valueType) {
-      case ValueType.NullType:
+      case EppoValueType.NullType:
         return 'null';
-      case ValueType.BoolType:
+      case EppoValueType.BoolType:
         return this.boolValue ? 'true' : 'false';
-      case ValueType.NumericType:
+      case EppoValueType.NumericType:
         return this.numericValue ? this.numericValue.toString() : '0';
-      case ValueType.StringType:
+      case EppoValueType.StringType:
         return this.stringValue ?? '';
-      case ValueType.JSONType:
+      case EppoValueType.JSONType:
         try {
           return JSON.stringify(this.objectValue) ?? '';
         } catch {
@@ -85,48 +84,40 @@ export class EppoValue {
 
   isExpectedType(): boolean {
     switch (this.valueType) {
-      case ValueType.BoolType:
+      case EppoValueType.BoolType:
         return typeof this.boolValue === 'boolean';
-      case ValueType.NumericType:
+      case EppoValueType.NumericType:
         return typeof this.numericValue === 'number';
-      case ValueType.StringType:
+      case EppoValueType.StringType:
         return typeof this.stringValue === 'string';
-      case ValueType.JSONType:
-        try {
-          return (
-            typeof this.objectValue === 'object' &&
-            typeof this.stringValue === 'string' &&
-            JSON.stringify(JSON.parse(this.stringValue)) === JSON.stringify(this.objectValue)
-          );
-        } catch {
-          return false;
-        }
-      case ValueType.NullType:
+      case EppoValueType.JSONType:
+        return typeof this.objectValue === 'object';
+      case EppoValueType.NullType:
         return false;
     }
   }
 
   isNullType(): boolean {
-    return this.valueType === ValueType.NullType;
+    return this.valueType === EppoValueType.NullType;
   }
 
   static Bool(value: boolean): EppoValue {
-    return new EppoValue(ValueType.BoolType, value, undefined, undefined, undefined);
+    return new EppoValue(EppoValueType.BoolType, value, undefined, undefined, undefined);
   }
 
   static Numeric(value: number): EppoValue {
-    return new EppoValue(ValueType.NumericType, undefined, value, undefined, undefined);
+    return new EppoValue(EppoValueType.NumericType, undefined, value, undefined, undefined);
   }
 
   static String(value: string): EppoValue {
-    return new EppoValue(ValueType.StringType, undefined, undefined, value, undefined);
+    return new EppoValue(EppoValueType.StringType, undefined, undefined, value, undefined);
   }
 
-  static JSON(value: string, typedValue: object): EppoValue {
-    return new EppoValue(ValueType.JSONType, undefined, undefined, value, typedValue);
+  static JSON(value: object): EppoValue {
+    return new EppoValue(EppoValueType.JSONType, undefined, undefined, undefined, value);
   }
 
   static Null(): EppoValue {
-    return new EppoValue(ValueType.NullType, undefined, undefined, undefined, undefined);
+    return new EppoValue(EppoValueType.NullType, undefined, undefined, undefined, undefined);
   }
 }
diff --git a/src/eval.spec.ts b/src/eval.spec.ts
new file mode 100644
index 00000000..b24402d1
--- /dev/null
+++ b/src/eval.spec.ts
@@ -0,0 +1,458 @@
+import { Evaluator, hashKey, isInShardRange } from './eval';
+import { Flag, Variation, Shard, VariationType } from './interfaces';
+import { MD5Sharder, DeterministicSharder } from './sharders';
+
+describe('Evaluator', () => {
+  const VARIATION_A: Variation = { key: 'a', value: 'A' };
+  const VARIATION_B: Variation = { key: 'b', value: 'B' };
+  const VARIATION_C: Variation = { key: 'c', value: 'C' };
+
+  it('should return none result for disabled flag', () => {
+    const flag: Flag = {
+      key: 'disabled_flag',
+      enabled: false,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A },
+      allocations: [
+        {
+          key: 'default',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [{ salt: 'a', ranges: [{ start: 0, end: 10 }] }],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
+    expect(result.flagKey).toEqual('disabled_flag');
+    expect(result.allocationKey).toBeNull();
+    expect(result.variation).toBeNull();
+    expect(result.doLog).toBeFalsy();
+  });
+
+  it('should match shard with full range', () => {
+    const shard: Shard = {
+      salt: 'a',
+      ranges: [{ start: 0, end: 100 }],
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    expect(evaluator.matchesShard(shard, 'subject_key', 100)).toBeTruthy();
+  });
+
+  it('should match shard with full range split', () => {
+    const shard: Shard = {
+      salt: 'a',
+      ranges: [
+        { start: 0, end: 50 },
+        { start: 50, end: 100 },
+      ],
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    expect(evaluator.matchesShard(shard, 'subject_key', 100)).toBeTruthy();
+
+    const deterministicEvaluator = new Evaluator(new DeterministicSharder({ subject_key: 50 }));
+    expect(deterministicEvaluator.matchesShard(shard, 'subject_key', 100)).toBeTruthy();
+  });
+
+  it('should not match shard when out of range', () => {
+    const shard: Shard = {
+      salt: 'a',
+      ranges: [{ start: 0, end: 50 }],
+    };
+
+    const evaluator = new Evaluator(new DeterministicSharder({ 'a-subject_key': 99 }));
+    expect(evaluator.matchesShard(shard, 'subject_key', 100)).toBeFalsy();
+  });
+
+  it('should evaluate empty flag to none result', () => {
+    const emptyFlag: Flag = {
+      key: 'empty',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A, b: VARIATION_B },
+      allocations: [],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(emptyFlag, 'subject_key', {}, false);
+    expect(result.flagKey).toEqual('empty');
+    expect(result.allocationKey).toBeNull();
+    expect(result.variation).toBeNull();
+    expect(result.doLog).toBeFalsy();
+  });
+
+  it('should evaluate simple flag and return control variation', () => {
+    const flag: Flag = {
+      key: 'flag-key',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { control: { key: 'control', value: 'control' } },
+      allocations: [
+        {
+          key: 'allocation',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'control',
+              shards: [{ salt: 'salt', ranges: [{ start: 0, end: 10000 }] }],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10000,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'user-1', {}, false);
+    expect(result.variation).toEqual({ key: 'control', value: 'control' });
+  });
+
+  it('should evaluate flag and target on id', () => {
+    const flag: Flag = {
+      key: 'flag-key',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { control: { key: 'control', value: 'control' } },
+      allocations: [
+        {
+          key: 'allocation',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'control',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10000,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    let result = evaluator.evaluateFlag(flag, 'user-1', {}, false);
+    expect(result.variation).toEqual({ key: 'control', value: 'control' });
+
+    result = evaluator.evaluateFlag(flag, 'user-3', {}, false);
+    expect(result.variation).toBeNull();
+  });
+
+  it('should catch all allocation and return variation A', () => {
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A, b: VARIATION_B },
+      allocations: [
+        {
+          key: 'default',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
+    expect(result.flagKey).toEqual('flag');
+    expect(result.allocationKey).toEqual('default');
+    expect(result.variation).toEqual(VARIATION_A);
+    expect(result.doLog).toBeTruthy();
+  });
+
+  it('should match first allocation rule and return variation B', () => {
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A, b: VARIATION_B },
+      allocations: [
+        {
+          key: 'first',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'b',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+        {
+          key: 'default',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(
+      flag,
+      'subject_key',
+      { email: 'eppo@example.com' },
+      false,
+    );
+    expect(result.flagKey).toEqual('flag');
+    expect(result.allocationKey).toEqual('first');
+    expect(result.variation).toEqual(VARIATION_B);
+  });
+
+  it('should not match first allocation rule and return variation A', () => {
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A, b: VARIATION_B },
+      allocations: [
+        {
+          key: 'first',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'b',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+        {
+          key: 'default',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', { email: 'eppo@test.com' }, false);
+    expect(result.flagKey).toEqual('flag');
+    expect(result.allocationKey).toEqual('default');
+    expect(result.variation).toEqual(VARIATION_A);
+  });
+  it('should evaluate sharding and return correct variations', () => {
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A, b: VARIATION_B, c: VARIATION_C },
+      allocations: [
+        {
+          key: 'first',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [
+                { salt: 'traffic', ranges: [{ start: 0, end: 5 }] },
+                { salt: 'split', ranges: [{ start: 0, end: 3 }] },
+              ],
+              extraLogging: {},
+            },
+            {
+              variationKey: 'b',
+              shards: [
+                { salt: 'traffic', ranges: [{ start: 0, end: 5 }] },
+                { salt: 'split', ranges: [{ start: 3, end: 6 }] },
+              ],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+        {
+          key: 'default',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'c',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const deterministicEvaluator = new Evaluator(
+      new DeterministicSharder({
+        'traffic-alice': 2,
+        'traffic-bob': 3,
+        'traffic-charlie': 4,
+        'traffic-dave': 7,
+        'split-alice': 1,
+        'split-bob': 4,
+        'split-charlie': 8,
+        'split-dave': 1,
+      }),
+    );
+
+    expect(deterministicEvaluator.evaluateFlag(flag, 'alice', {}, false).variation).toEqual(
+      VARIATION_A,
+    );
+    expect(deterministicEvaluator.evaluateFlag(flag, 'bob', {}, false).variation).toEqual(
+      VARIATION_B,
+    );
+    expect(deterministicEvaluator.evaluateFlag(flag, 'charlie', {}, false).variation).toEqual(
+      VARIATION_C,
+    );
+    expect(deterministicEvaluator.evaluateFlag(flag, 'dave', {}, false).variation).toEqual(
+      VARIATION_C,
+    );
+  });
+
+  it('should return none result for evaluation prior to allocation', () => {
+    const now = new Date();
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A },
+      allocations: [
+        {
+          key: 'default',
+          startAt: new Date(now.getFullYear() + 1, 0, 1),
+          endAt: new Date(now.getFullYear() + 1, 1, 1),
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
+    expect(result.flagKey).toEqual('flag');
+    expect(result.allocationKey).toBeNull();
+    expect(result.variation).toBeNull();
+  });
+
+  it('should return correct variation for evaluation during allocation', () => {
+    const now = new Date();
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A },
+      allocations: [
+        {
+          key: 'default',
+          startAt: new Date(now.getFullYear() - 1, 0, 1),
+          endAt: new Date(now.getFullYear() + 1, 0, 1),
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
+    expect(result.flagKey).toEqual('flag');
+    expect(result.allocationKey).toEqual('default');
+    expect(result.variation).toEqual(VARIATION_A);
+  });
+
+  it('should evaluate flag after allocation period', () => {
+    const now = new Date();
+    const flag: Flag = {
+      key: 'flag',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A },
+      allocations: [
+        {
+          key: 'default',
+          startAt: new Date(now.getFullYear() - 2, 0, 1),
+          endAt: new Date(now.getFullYear() - 1, 0, 1),
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
+    expect(result.flagKey).toEqual('flag');
+    expect(result.allocationKey).toBeNull();
+    expect(result.variation).toBeNull();
+  });
+
+  it('should create a hash key that appends subject to salt', () => {
+    expect(hashKey('salt', 'subject')).toEqual('salt-subject');
+  });
+
+  it('should correctly determine if a shard is within a range', () => {
+    expect(isInShardRange(5, { start: 0, end: 10 })).toBeTruthy();
+    expect(isInShardRange(10, { start: 0, end: 10 })).toBeFalsy();
+    expect(isInShardRange(0, { start: 0, end: 10 })).toBeTruthy();
+    expect(isInShardRange(0, { start: 0, end: 0 })).toBeFalsy();
+    expect(isInShardRange(0, { start: 0, end: 1 })).toBeTruthy();
+    expect(isInShardRange(1, { start: 0, end: 1 })).toBeFalsy();
+    expect(isInShardRange(1, { start: 1, end: 1 })).toBeFalsy();
+  });
+});
diff --git a/src/eval.ts b/src/eval.ts
new file mode 100644
index 00000000..5247736e
--- /dev/null
+++ b/src/eval.ts
@@ -0,0 +1,92 @@
+import { Flag, Shard, Range, Variation, Allocation, Split } from './interfaces';
+import { matchesRule } from './rule_evaluator';
+import { Sharder } from './sharders';
+
+export interface FlagEvaluation {
+  flagKey: string;
+  subjectKey: string;
+  subjectAttributes: Record<string, string | number | boolean>;
+  allocationKey: string | null;
+  variation: Variation | null;
+  extraLogging: Record<string, string>;
+  doLog: boolean;
+}
+
+export class Evaluator {
+  sharder: Sharder; // Assuming a Sharder type exists, replace 'any' with 'Sharder' when available
+
+  constructor(sharder: Sharder) {
+    this.sharder = sharder;
+  }
+
+  evaluateFlag(
+    flag: Flag,
+    subjectKey: string,
+    subjectAttributes: Record<string, string | number | boolean>,
+    obfuscated: boolean,
+  ): FlagEvaluation {
+    if (!flag.enabled) {
+      return noneResult(flag.key, subjectKey, subjectAttributes);
+    }
+
+    const now = new Date();
+    for (const allocation of flag.allocations) {
+      if (allocation.startAt && now < allocation.startAt) continue;
+      if (allocation.endAt && now > allocation.endAt) continue;
+
+      if (
+        !allocation.rules ||
+        allocation.rules.some((rule) =>
+          matchesRule(rule, { id: subjectKey, ...subjectAttributes }, obfuscated),
+        )
+      ) {
+        for (const split of allocation.splits) {
+          if (
+            split.shards.every((shard) => this.matchesShard(shard, subjectKey, flag.totalShards))
+          ) {
+            return {
+              flagKey: flag.key,
+              subjectKey,
+              subjectAttributes,
+              allocationKey: allocation.key,
+              variation: flag.variations[split.variationKey],
+              extraLogging: split.extraLogging,
+              doLog: allocation.doLog,
+            };
+          }
+        }
+      }
+    }
+
+    return noneResult(flag.key, subjectKey, subjectAttributes);
+  }
+
+  matchesShard(shard: Shard, subjectKey: string, totalShards: number): boolean {
+    const h = this.sharder.getShard(hashKey(shard.salt, subjectKey), totalShards);
+    return shard.ranges.some((range) => isInShardRange(h, range));
+  }
+}
+
+export function isInShardRange(shard: number, range: Range): boolean {
+  return range.start <= shard && shard < range.end;
+}
+
+export function hashKey(salt: string, subjectKey: string): string {
+  return `${salt}-${subjectKey}`;
+}
+
+export function noneResult(
+  flagKey: string,
+  subjectKey: string,
+  subjectAttributes: Record<string, string | number | boolean>,
+): FlagEvaluation {
+  return {
+    flagKey,
+    subjectKey,
+    subjectAttributes,
+    allocationKey: null,
+    variation: null,
+    extraLogging: {},
+    doLog: false,
+  };
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
new file mode 100644
index 00000000..1f11dde4
--- /dev/null
+++ b/src/interfaces.ts
@@ -0,0 +1,48 @@
+import { Rule } from './rules';
+
+export enum VariationType {
+  STRING = 'string',
+  INTEGER = 'integer',
+  FLOAT = 'float',
+  BOOLEAN = 'boolean',
+  JSON = 'json',
+}
+
+export interface Variation {
+  key: string;
+  value: string | number | boolean;
+}
+
+export interface Range {
+  start: number;
+  end: number;
+}
+
+export interface Shard {
+  salt: string;
+  ranges: Range[];
+}
+
+export interface Split {
+  shards: Shard[];
+  variationKey: string;
+  extraLogging: Record<string, string>;
+}
+
+export interface Allocation {
+  key: string;
+  rules: Rule[];
+  startAt?: Date;
+  endAt?: Date;
+  splits: Split[];
+  doLog: boolean;
+}
+
+export interface Flag {
+  key: string;
+  enabled: boolean;
+  variationType: VariationType;
+  variations: Record<string, Variation>;
+  allocations: Allocation[];
+  totalShards: number;
+}
diff --git a/src/rule_evaluator.spec.ts b/src/rule_evaluator.spec.ts
index d0e2f5f2..a4692936 100644
--- a/src/rule_evaluator.spec.ts
+++ b/src/rule_evaluator.spec.ts
@@ -1,13 +1,11 @@
 import { OperatorType, IRule } from './dto/rule-dto';
-import { findMatchingRule } from './rule_evaluator';
+import { matchesRule } from './rule_evaluator';
 
-describe('findMatchingRule', () => {
+describe('matchesRule', () => {
   const ruleWithEmptyConditions: IRule = {
-    allocationKey: 'test',
     conditions: [],
   };
   const numericRule: IRule = {
-    allocationKey: 'test',
     conditions: [
       {
         operator: OperatorType.GTE,
@@ -22,7 +20,6 @@ describe('findMatchingRule', () => {
     ],
   };
   const semverRule: IRule = {
-    allocationKey: 'test',
     conditions: [
       {
         operator: OperatorType.GTE,
@@ -37,7 +34,6 @@ describe('findMatchingRule', () => {
     ],
   };
   const ruleWithMatchesCondition: IRule = {
-    allocationKey: 'test',
     conditions: [
       {
         operator: OperatorType.MATCHES,
@@ -47,294 +43,40 @@ describe('findMatchingRule', () => {
     ],
   };
 
-  it('returns null if rules array is empty', () => {
-    const rules: IRule[] = [];
-    expect(findMatchingRule({ name: 'my-user' }, rules, false)).toEqual(null);
-  });
-
-  it('returns null if attributes do not match any rules', () => {
-    const rules = [numericRule];
-    expect(findMatchingRule({ totalSales: 101 }, rules, false)).toEqual(null);
-
-    // input subject attribute is a string which is not a valid semver nor numeric
-    // verify that is not parsed to a semver nor a numeric.
-    expect(
-      findMatchingRule(
-        { version: '1.2.03' },
-        [
-          {
-            allocationKey: 'test',
-            conditions: [
-              {
-                operator: OperatorType.GTE,
-                attribute: 'version',
-                value: '1.2.0',
-              },
-            ],
-          },
-        ],
-        false,
-      ),
-    ).toEqual(null);
-  });
-
-  it('returns the rule if attributes match AND conditions', () => {
-    const rules = [numericRule];
-    expect(findMatchingRule({ totalSales: 100 }, rules, false)).toEqual(numericRule);
-  });
-
-  it('returns the rule for semver conditions', () => {
-    const rules = [semverRule];
-    expect(findMatchingRule({ version: '1.1.0' }, rules, false)).toEqual(semverRule);
-    expect(findMatchingRule({ version: '2.0.0' }, rules, false)).toEqual(semverRule);
-    expect(findMatchingRule({ version: '2.1.0' }, rules, false)).toBeNull();
-  });
-
-  it('returns the rule for semver prerelease conditions', () => {
-    /*
-    https://semver.org/#spec-item-9
-
-    A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. 
-    Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. 
-    Numeric identifiers MUST NOT include leading zeroes. Pre-release versions have a lower precedence than the associated normal version. 
-    A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version. 
-    Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, 1.0.0-x.7.z.92, 1.0.0-x-y-z.--.
-
-    Pre-release versions have a lower precedence than the associated normal version.
-    For example, 1.0.0-alpha < 1.0.0.
-    */
-    const extendedSemverRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.GT,
-          attribute: 'version',
-          value: '1.2.3-alpha',
-        },
-        {
-          operator: OperatorType.LTE,
-          attribute: 'version',
-          value: '2.0.0',
-        },
-      ],
-    };
-    const rules = [extendedSemverRule];
-
-    // is greater than the associated alpha version
-    expect(findMatchingRule({ version: '1.2.3' }, rules, false)).toEqual(extendedSemverRule);
-
-    // beta is greater than alpha (lexicographically)
-    expect(findMatchingRule({ version: '1.2.3-beta' }, rules, false)).toEqual(extendedSemverRule);
-
-    // 1.2.4 is greater than 1.2.3
-    expect(findMatchingRule({ version: '1.2.4' }, rules, false)).toEqual(extendedSemverRule);
-  });
-
-  it('returns the rule for semver build numbers', () => {
-    /*
-    https://semver.org/#spec-item-10
-
-    Build metadata MAY be denoted by appending a plus sign and a series of dot separated identifiers immediately following the patch or pre-release version. 
-    Identifiers MUST comprise only ASCII alphanumerics and hyphens [0-9A-Za-z-]. Identifiers MUST NOT be empty. 
-    Build metadata MUST be ignored when determining version precedence. 
-    Thus two versions that differ only in the build metadata, have the same precedence. 
-    
-    Examples: 1.0.0-alpha+001, 1.0.0+20130313144700, 1.0.0-beta+exp.sha.5114f85, 1.0.0+21AF26D3----117B344092BD.
-    */
-
-    const extendedSemverRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.GT,
-          attribute: 'version',
-          value: '1.2.3+001',
-        },
-        {
-          operator: OperatorType.LTE,
-          attribute: 'version',
-          value: '2.0.0',
-        },
-      ],
-    };
-    const rules = [extendedSemverRule];
-
-    // build number is ignored therefore 1.2.3 is not greater than 1.2.3
-    expect(findMatchingRule({ version: '1.2.3' }, rules, false)).toEqual(null);
-
-    // build number is ignored therefore 1.2.3 is not greater than 1.2.3
-    expect(findMatchingRule({ version: '1.2.3+500' }, rules, false)).toEqual(null);
-
-    // 1.2.4 is greater than 1.2.3
-    expect(findMatchingRule({ version: '1.2.4' }, rules, false)).toEqual(extendedSemverRule);
-  });
-
-  it('returns the rule for semver mixed prerelease and build numbers', () => {
-    /*
-    When a version in Semantic Versioning (SemVer) includes both pre-release identifiers and build metadata, 
-    the version's precedence is determined first by the pre-release version, 
-    with the build metadata being ignored in terms of precedence. 
-    
-    The format for such a version would look something like this: MAJOR.MINOR.PATCH-prerelease+build
-    */
-    const extendedSemverRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.GT,
-          attribute: 'version',
-          value: '1.2.3-beta+001',
-        },
-        {
-          operator: OperatorType.LTE,
-          attribute: 'version',
-          value: '2.0.0',
-        },
-      ],
-    };
-    const rules = [extendedSemverRule];
-
-    // gamma is greater than beta (lexicographically)
-    expect(findMatchingRule({ version: '1.2.3-gamma' }, rules, false)).toEqual(extendedSemverRule);
-
-    // 1.2.3 is greater than 1.2.3-beta; this is a stable release
-    expect(findMatchingRule({ version: '1.2.3+500' }, rules, false)).toEqual(extendedSemverRule);
-
-    // 1.2.4 is greater than 1.2.3
-    expect(findMatchingRule({ version: '1.2.4' }, rules, false)).toEqual(extendedSemverRule);
-  });
-
-  it('returns null if there is no attribute for the condition', () => {
-    const rules = [numericRule];
-    expect(findMatchingRule({ unknown: 'test' }, rules, false)).toEqual(null);
-  });
-
-  it('returns the rule if it has no conditions', () => {
-    const rules = [ruleWithEmptyConditions];
-    expect(findMatchingRule({ totalSales: 101 }, rules, false)).toEqual(ruleWithEmptyConditions);
-  });
+  const subjectAttributes = {
+    totalSales: 50,
+    version: '1.5.0',
+    user_id: '12345',
+  };
 
-  it('allows for a mix of numeric and string values', () => {
-    const rules = [numericRule, ruleWithMatchesCondition];
-    expect(findMatchingRule({ totalSales: 'stringValue' }, rules, false)).toEqual(null);
-    expect(findMatchingRule({ totalSales: '20' }, rules, false)).toEqual(numericRule);
+  it('should return true for a rule with empty conditions', () => {
+    expect(matchesRule(ruleWithEmptyConditions, subjectAttributes, false)).toBe(true);
   });
 
-  it('handles rule with matches operator', () => {
-    const rules = [ruleWithMatchesCondition];
-    expect(findMatchingRule({ user_id: '14' }, rules, false)).toEqual(ruleWithMatchesCondition);
-    expect(findMatchingRule({ user_id: 14 }, rules, false)).toEqual(ruleWithMatchesCondition);
+  it('should return true for a numeric rule that matches the subject attributes', () => {
+    expect(matchesRule(numericRule, subjectAttributes, false)).toBe(true);
   });
 
-  it('handles oneOf rule type with boolean', () => {
-    const oneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.ONE_OF,
-          value: ['true'],
-          attribute: 'enabled',
-        },
-      ],
-    };
-    const notOneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.NOT_ONE_OF,
-          value: ['true'],
-          attribute: 'enabled',
-        },
-      ],
-    };
-    expect(findMatchingRule({ enabled: true }, [oneOfRule], false)).toEqual(oneOfRule);
-    expect(findMatchingRule({ enabled: false }, [oneOfRule], false)).toEqual(null);
-    expect(findMatchingRule({ enabled: true }, [notOneOfRule], false)).toEqual(null);
-    expect(findMatchingRule({ enabled: false }, [notOneOfRule], false)).toEqual(notOneOfRule);
+  it('should return false for a numeric rule that does not match the subject attributes', () => {
+    const failingAttributes = { totalSales: 101 };
+    expect(matchesRule(numericRule, failingAttributes, false)).toBe(false);
   });
 
-  it('handles oneOf rule type with string', () => {
-    const oneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.ONE_OF,
-          value: ['user1', 'user2'],
-          attribute: 'userId',
-        },
-      ],
-    };
-    const notOneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.NOT_ONE_OF,
-          value: ['user14'],
-          attribute: 'userId',
-        },
-      ],
-    };
-    expect(findMatchingRule({ userId: 'user1' }, [oneOfRule], false)).toEqual(oneOfRule);
-    expect(findMatchingRule({ userId: 'user2' }, [oneOfRule], false)).toEqual(oneOfRule);
-    expect(findMatchingRule({ userId: 'user3' }, [oneOfRule], false)).toEqual(null);
-    expect(findMatchingRule({ userId: 'user14' }, [notOneOfRule], false)).toEqual(null);
-    expect(findMatchingRule({ userId: 'user15' }, [notOneOfRule], false)).toEqual(notOneOfRule);
+  it('should return true for a semver rule that matches the subject attributes', () => {
+    expect(matchesRule(semverRule, subjectAttributes, false)).toBe(true);
   });
 
-  it('does case insensitive matching with oneOf operator', () => {
-    const oneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.ONE_OF,
-          value: ['CA', 'US'],
-          attribute: 'country',
-        },
-      ],
-    };
-    expect(findMatchingRule({ country: 'us' }, [oneOfRule], false)).toEqual(oneOfRule);
-    expect(findMatchingRule({ country: 'cA' }, [oneOfRule], false)).toEqual(oneOfRule);
+  it('should return false for a semver rule that does not match the subject attributes', () => {
+    const failingAttributes = { version: '2.1.0' };
+    expect(matchesRule(semverRule, failingAttributes, false)).toBe(false);
   });
 
-  it('does case insensitive matching with notOneOf operator', () => {
-    const notOneOf: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.NOT_ONE_OF,
-          value: ['1.0.BB', '1Ab'],
-          attribute: 'deviceType',
-        },
-      ],
-    };
-    expect(findMatchingRule({ deviceType: '1ab' }, [notOneOf], false)).toEqual(null);
+  it('should return true for a rule with matches condition that matches the subject attributes', () => {
+    expect(matchesRule(ruleWithMatchesCondition, subjectAttributes, false)).toBe(true);
   });
 
-  it('handles oneOf rule with number', () => {
-    const oneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.ONE_OF,
-          value: ['1', '2'],
-          attribute: 'userId',
-        },
-      ],
-    };
-    const notOneOfRule: IRule = {
-      allocationKey: 'test',
-      conditions: [
-        {
-          operator: OperatorType.NOT_ONE_OF,
-          value: ['14'],
-          attribute: 'userId',
-        },
-      ],
-    };
-    expect(findMatchingRule({ userId: 1 }, [oneOfRule], false)).toEqual(oneOfRule);
-    expect(findMatchingRule({ userId: '2' }, [oneOfRule], false)).toEqual(oneOfRule);
-    expect(findMatchingRule({ userId: 3 }, [oneOfRule], false)).toEqual(null);
-    expect(findMatchingRule({ userId: 14 }, [notOneOfRule], false)).toEqual(null);
-    expect(findMatchingRule({ userId: '15' }, [notOneOfRule], false)).toEqual(notOneOfRule);
+  it('should return false for a rule with matches condition that does not match the subject attributes', () => {
+    const failingAttributes = { user_id: 'abcde' };
+    expect(matchesRule(ruleWithMatchesCondition, failingAttributes, false)).toBe(false);
   });
 });
diff --git a/src/rule_evaluator.ts b/src/rule_evaluator.ts
index a56b8768..f6418933 100644
--- a/src/rule_evaluator.ts
+++ b/src/rule_evaluator.ts
@@ -10,22 +10,9 @@ import {
 import { Condition, OperatorType, IRule, OperatorValueType } from './dto/rule-dto';
 import { decodeBase64, getMD5Hash } from './obfuscation';
 
-export function findMatchingRule(
-  subjectAttributes: Record<string, any>,
-  rules: IRule[],
-  obfuscated: boolean,
-): IRule | null {
-  for (const rule of rules) {
-    if (matchesRule(subjectAttributes, rule, obfuscated)) {
-      return rule;
-    }
-  }
-  return null;
-}
-
-function matchesRule(
-  subjectAttributes: Record<string, any>,
+export function matchesRule(
   rule: IRule,
+  subjectAttributes: Record<string, any>,
   obfuscated: boolean,
 ): boolean {
   const conditionEvaluations = evaluateRuleConditions(
diff --git a/src/shard.ts b/src/shard.ts
deleted file mode 100644
index 4ea68fbc..00000000
--- a/src/shard.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as md5 from 'md5';
-
-import { IShardRange } from './dto/variation-dto';
-
-export function getShard(input: string, subjectShards: number): number {
-  const hashOutput = md5(input);
-  // get the first 4 bytes of the md5 hex string and parse it using base 16
-  // (8 hex characters represent 4 bytes, e.g. 0xffffffff represents the max 4-byte integer)
-  const intFromHash = parseInt(hashOutput.slice(0, 8), 16);
-  return intFromHash % subjectShards;
-}
-
-export function isShardInRange(shard: number, range: IShardRange) {
-  return shard >= range.start && shard < range.end;
-}
diff --git a/src/sharders.ts b/src/sharders.ts
new file mode 100644
index 00000000..a702ea1d
--- /dev/null
+++ b/src/sharders.ts
@@ -0,0 +1,32 @@
+import { getMD5Hash } from './obfuscation';
+
+export abstract class Sharder {
+  abstract getShard(input: string, totalShards: number): number;
+}
+
+export class MD5Sharder extends Sharder {
+  getShard(input: string, totalShards: number): number {
+    const hashOutput = getMD5Hash(input);
+    // get the first 4 bytes of the md5 hex string and parse it using base 16
+    // (8 hex characters represent 4 bytes, e.g. 0xffffffff represents the max 4-byte integer)
+    const intFromHash = parseInt(hashOutput.slice(0, 8), 16);
+    return intFromHash % totalShards;
+  }
+}
+
+export class DeterministicSharder extends Sharder {
+  /*
+  Deterministic sharding based on a look-up table
+  to simplify writing tests
+  */
+  private lookup: Record<string, number>;
+
+  constructor(lookup: Record<string, number>) {
+    super();
+    this.lookup = lookup;
+  }
+
+  getShard(input: string, totalShards: number): number {
+    return this.lookup[input] ?? 0;
+  }
+}
diff --git a/src/types.ts b/src/types.ts
new file mode 100644
index 00000000..db075ec1
--- /dev/null
+++ b/src/types.ts
@@ -0,0 +1,4 @@
+export type ValueType = string | number | boolean | JSON;
+export type AttributeType = string | number | boolean;
+export type ConditionValueType = AttributeType | AttributeType[];
+export type SubjectAttributes = { [key: string]: AttributeType };

From 07b9cde524134a4b7e68230e9df75da2b77ed304 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Tue, 26 Mar 2024 17:09:11 -0700
Subject: [PATCH 02/39] making progress

---
 Makefile                                  |    5 +-
 src/assignment-logger.ts                  |    7 +-
 src/client/eppo-client.spec.ts            | 1728 ++++++++-------------
 src/client/eppo-client.ts                 |   42 +-
 src/dto/allocation-dto.ts                 |   10 -
 src/dto/experiment-configuration-dto.ts   |   12 -
 src/dto/holdout-dto.ts                    |    7 -
 src/dto/variation-dto.ts                  |   14 -
 src/eppo_value.spec.ts                    |    2 +-
 src/eppo_value.ts                         |   13 +-
 src/eval.spec.ts                          |   39 +-
 src/eval.ts                               |    2 +-
 src/experiment-configuration-requestor.ts |   14 +-
 src/index.ts                              |    7 +-
 src/interfaces.ts                         |   68 +-
 src/rule_evaluator.ts                     |  105 +-
 test/testHelpers.ts                       |   34 +-
 17 files changed, 842 insertions(+), 1267 deletions(-)
 delete mode 100644 src/dto/allocation-dto.ts
 delete mode 100644 src/dto/experiment-configuration-dto.ts
 delete mode 100644 src/dto/holdout-dto.ts
 delete mode 100644 src/dto/variation-dto.ts

diff --git a/Makefile b/Makefile
index 714834bc..e73df080 100644
--- a/Makefile
+++ b/Makefile
@@ -35,10 +35,7 @@ test-data:
 	rm -rf $(testDataDir)
 	mkdir -p $(tempDir)
 	git clone -b ${branchName} --depth 1 --single-branch ${githubRepoLink} ${gitDataDir}
-	cp ${gitDataDir}rac-experiments-v3.json ${testDataDir}
-	cp ${gitDataDir}rac-experiments-v3-obfuscated.json ${testDataDir}
-	cp -r ${gitDataDir}assignment-v2 ${testDataDir}
-	cp -r ${gitDataDir}assignment-v2-holdouts/. ${testDataDir}assignment-v2
+	cp -r ${gitDataDir}ufc ${testDataDir}
 	rm -rf ${tempDir}
 
 ## prepare
diff --git a/src/assignment-logger.ts b/src/assignment-logger.ts
index d1f3a73f..4c2cefcf 100644
--- a/src/assignment-logger.ts
+++ b/src/assignment-logger.ts
@@ -9,6 +9,7 @@ export type NullableHoldoutVariationType = HoldoutVariationEnum | null;
  * Holds data about the variation a subject was assigned to.
  * @public
  */
+
 export interface IAssignmentEvent {
   /**
    * An Eppo allocation key
@@ -42,11 +43,7 @@ export interface IAssignmentEvent {
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   subjectAttributes: Record<string, any>;
-
-  /**
-   * For additional fields to log
-   */
-  [propName: string]: string;
+  [propName: string]: unknown;
 }
 
 /**
diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index de565925..e2e47988 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -1,37 +1,33 @@
 /**
  * @jest-environment jsdom
  */
-import axios from "axios";
-import * as td from "testdouble";
-import mock, { MockResponse } from "xhr-mock";
+import axios from 'axios';
+import * as td from 'testdouble';
+import mock, { MockResponse } from 'xhr-mock';
 
 import {
-  IAssignmentTestCase,
-  MOCK_RAC_RESPONSE_FILE,
-  OBFUSCATED_MOCK_RAC_RESPONSE_FILE,
+  MOCK_UFC_RESPONSE_FILE,
+  OBFUSCATED_MOCK_UFC_RESPONSE_FILE,
   ValueTestType,
   readAssignmentTestData,
-  readMockRacResponse,
-} from "../../test/testHelpers";
-import { IAssignmentHooks } from "../assignment-hooks";
-import { IAssignmentLogger } from "../assignment-logger";
-import { IConfigurationStore } from "../configuration-store";
-import {
-  MAX_EVENT_QUEUE_SIZE,
-  POLL_INTERVAL_MS,
-  POLL_JITTER_PCT,
-} from "../constants";
-import { OperatorType } from "../dto/rule-dto";
-import { EppoValue } from "../eppo_value";
-import ExperimentConfigurationRequestor from "../experiment-configuration-requestor";
-import HttpClient from "../http-client";
-
-import EppoClient, {
-  ExperimentConfigurationRequestParameters,
-} from "./eppo-client";
+  readMockUFCResponse,
+} from '../../test/testHelpers';
+import { IAssignmentHooks } from '../assignment-hooks';
+import { IAssignmentLogger } from '../assignment-logger';
+import { IConfigurationStore } from '../configuration-store';
+import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
+import { OperatorType } from '../dto/rule-dto';
+import { EppoValue } from '../eppo_value';
+import { Evaluator } from '../eval';
+import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
+import HttpClient from '../http-client';
+import { VariationType } from '../interfaces';
+import { MD5Sharder } from '../sharders';
+
+import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
-const packageJson = require("../../package.json");
+const packageJson = require('../../package.json');
 
 class TestConfigurationStore implements IConfigurationStore {
   private store: Record<string, string> = {};
@@ -46,38 +42,40 @@ class TestConfigurationStore implements IConfigurationStore {
       this.store[key] = JSON.stringify(val);
     });
   }
+
+  public getKeys(): string[] {
+    return Object.keys(this.store);
+  }
 }
 
 export async function init(configurationStore: IConfigurationStore) {
   const axiosInstance = axios.create({
-    baseURL: "http://127.0.0.1:4000",
+    baseURL: 'http://127.0.0.1:4000',
     timeout: 1000,
   });
 
   const httpClient = new HttpClient(axiosInstance, {
-    apiKey: "dummy",
-    sdkName: "js-client-sdk-common",
+    apiKey: 'dummy',
+    sdkName: 'js-client-sdk-common',
     sdkVersion: packageJson.version,
   });
 
   const configurationRequestor = new ExperimentConfigurationRequestor(
     configurationStore,
-    httpClient
+    httpClient,
   );
   await configurationRequestor.fetchAndStoreConfigurations();
 }
 
-describe("EppoClient E2E test", () => {
-  const sessionOverrideSubject = "subject-14";
-  const sessionOverrideExperiment = "exp-100";
-
+describe('EppoClient E2E test', () => {
+  // const evaluator = new Evaluator(new MD5Sharder());
   const storage = new TestConfigurationStore();
-  const globalClient = new EppoClient(storage);
+  // const globalClient = new EppoClient(evaluator, storage);
 
   beforeAll(async () => {
     mock.setup();
-    mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
-      const rac = readMockRacResponse(MOCK_RAC_RESPONSE_FILE);
+    mock.get(/flag_config\/v1\/config*/, (_req, res) => {
+      const rac = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
       return res.status(200).body(JSON.stringify(rac));
     });
 
@@ -88,69 +86,60 @@ describe("EppoClient E2E test", () => {
     mock.teardown();
   });
 
-  const flagKey = "mock-experiment";
+  const flagKey = 'mock-flag';
+
+  const variationA = {
+    key: 'a',
+    value: 'variation-a',
+  };
+
+  const variationB = {
+    name: 'b',
+    value: 'variation-b',
+  };
 
   const mockExperimentConfig = {
     name: flagKey,
     enabled: true,
-    subjectShards: 10000,
-    overrides: {},
-    typedOverrides: {},
-    rules: [
+    variationType: VariationType.STRING,
+    variations: { a: variationA, b: variationB },
+    allocations: [
       {
-        allocationKey: "allocation1",
-        conditions: [],
-      },
-    ],
-    allocations: {
-      allocation1: {
-        percentExposure: 1,
-        statusQuoVariationKey: null,
-        shippedVariationKey: null,
-        holdouts: [],
-        variations: [
-          {
-            name: "control",
-            value: "control",
-            typedValue: "control",
-            shardRange: {
-              start: 0,
-              end: 3333,
-            },
-          },
+        key: 'allocation-a',
+        rules: [],
+        splits: [
           {
-            name: "variant-1",
-            value: "variant-1",
-            typedValue: "variant-1",
-            shardRange: {
-              start: 3333,
-              end: 6667,
-            },
+            shards: [
+              { salt: 'traffic', ranges: [{ start: 0, end: 10000 }] },
+              { salt: 'assignment', ranges: [{ start: 0, end: 5000 }] },
+            ],
+            variationKey: 'a',
           },
           {
-            name: "variant-2",
-            value: "variant-2",
-            typedValue: "variant-2",
-            shardRange: {
-              start: 6667,
-              end: 10000,
-            },
+            shards: [
+              { salt: 'traffic', ranges: [{ start: 0, end: 10000 }] },
+              { salt: 'assignment', ranges: [{ start: 5000, end: 10000 }] },
+            ],
+            variationKey: 'b',
           },
         ],
+        doLog: true,
       },
-    },
+    ],
+    totalShards: 10000,
   };
 
-  describe("error encountered", () => {
+  describe('error encountered', () => {
     let client: EppoClient;
     const mockHooks = td.object<IAssignmentHooks>();
 
     beforeAll(() => {
       storage.setEntries({ [flagKey]: mockExperimentConfig });
-      client = new EppoClient(storage);
+      const evaluator = new Evaluator(new MD5Sharder());
+      client = new EppoClient(evaluator, storage);
 
-      td.replace(EppoClient.prototype, "getAssignmentVariation", function () {
-        throw new Error("So Graceful Error");
+      td.replace(EppoClient.prototype, 'getAssignmentVariation', function () {
+        throw new Error('So Graceful Error');
       });
     });
 
@@ -158,107 +147,62 @@ describe("EppoClient E2E test", () => {
       td.reset();
     });
 
-    it("returns null when graceful failure if error encountered", async () => {
+    it('returns null when graceful failure if error encountered', async () => {
       client.setIsGracefulFailureMode(true);
 
-      expect(
-        client.getAssignment("subject-identifer", flagKey, {}, mockHooks)
-      ).toBeNull();
-      expect(
-        client.getBoolAssignment("subject-identifer", flagKey, {}, mockHooks)
-      ).toBeNull();
-      expect(
-        client.getJSONStringAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          mockHooks
-        )
-      ).toBeNull();
-      expect(
-        client.getNumericAssignment("subject-identifer", flagKey, {}, mockHooks)
-      ).toBeNull();
-      expect(
-        client.getParsedJSONAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          mockHooks
-        )
-      ).toBeNull();
-      expect(
-        client.getStringAssignment("subject-identifer", flagKey, {}, mockHooks)
-      ).toBeNull();
+      expect(client.getBoolAssignment('subject-identifer', flagKey, {})).toBeNull();
+      expect(client.getNumericAssignment('subject-identifer', flagKey, {})).toBeNull();
+      expect(client.getParsedJSONAssignment('subject-identifer', flagKey, {})).toBeNull();
+      expect(client.getStringAssignment('subject-identifer', flagKey, {})).toBeNull();
     });
 
-    it("throws error when graceful failure is false", async () => {
+    it('throws error when graceful failure is false', async () => {
       client.setIsGracefulFailureMode(false);
 
       expect(() => {
-        client.getAssignment("subject-identifer", flagKey, {}, mockHooks);
-      }).toThrow();
-
-      expect(() => {
-        client.getBoolAssignment("subject-identifer", flagKey, {}, mockHooks);
-      }).toThrow();
-
-      expect(() => {
-        client.getJSONStringAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          mockHooks
-        );
+        client.getBoolAssignment('subject-identifer', flagKey, {});
       }).toThrow();
 
       expect(() => {
-        client.getParsedJSONAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          mockHooks
-        );
+        client.getParsedJSONAssignment('subject-identifer', flagKey, {});
       }).toThrow();
 
       expect(() => {
-        client.getNumericAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          mockHooks
-        );
+        client.getNumericAssignment('subject-identifer', flagKey, {});
       }).toThrow();
 
       expect(() => {
-        client.getStringAssignment("subject-identifer", flagKey, {}, mockHooks);
+        client.getStringAssignment('subject-identifer', flagKey, {});
       }).toThrow();
     });
   });
 
-  describe("setLogger", () => {
+  describe('setLogger', () => {
     beforeAll(() => {
       storage.setEntries({ [flagKey]: mockExperimentConfig });
     });
 
-    it("Invokes logger for queued events", () => {
+    it('Invokes logger for queued events', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const client = new EppoClient(storage);
-      client.getAssignment("subject-to-be-logged", flagKey);
+      const evaluator = new Evaluator(new MD5Sharder());
+      const client = new EppoClient(evaluator, storage);
+      client.getStringAssignment('subject-to-be-logged', flagKey);
       client.setLogger(mockLogger);
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
-      expect(
-        td.explain(mockLogger.logAssignment).calls[0].args[0].subject
-      ).toEqual("subject-to-be-logged");
+      expect(td.explain(mockLogger.logAssignment).calls[0].args[0].subject).toEqual(
+        'subject-to-be-logged',
+      );
     });
 
-    it("Does not log same queued event twice", () => {
+    it('Does not log same queued event twice', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const client = new EppoClient(storage);
+      const evaluator = new Evaluator(new MD5Sharder());
+      const client = new EppoClient(evaluator, storage);
 
-      client.getAssignment("subject-to-be-logged", flagKey);
+      client.getStringAssignment('subject-to-be-logged', flagKey);
       client.setLogger(mockLogger);
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
 
@@ -266,179 +210,111 @@ describe("EppoClient E2E test", () => {
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
     });
 
-    it("Does not invoke logger for events that exceed queue size", () => {
+    it('Does not invoke logger for events that exceed queue size', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const client = new EppoClient(storage);
+      const evaluator = new Evaluator(new MD5Sharder());
+      const client = new EppoClient(evaluator, storage);
+
       for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
-        client.getAssignment(`subject-to-be-logged-${i}`, flagKey);
+        client.getStringAssignment(`subject-to-be-logged-${i}`, flagKey);
       }
       client.setLogger(mockLogger);
-      expect(td.explain(mockLogger.logAssignment).callCount).toEqual(
-        MAX_EVENT_QUEUE_SIZE
-      );
+      expect(td.explain(mockLogger.logAssignment).callCount).toEqual(MAX_EVENT_QUEUE_SIZE);
     });
   });
 
-  describe("getAssignment", () => {
-    it.each(readAssignmentTestData())(
-      "test variation assignment splits",
-      async ({
-        experiment,
-        valueType = ValueTestType.StringType,
-        subjects,
-        subjectsWithAttributes,
-        expectedAssignments,
-      }: IAssignmentTestCase) => {
-        `---- Test Case for ${experiment} Experiment ----`;
-
-        const assignments = getAssignmentsWithSubjectAttributes(
-          subjectsWithAttributes
-            ? subjectsWithAttributes
-            : subjects.map((subject) => ({ subjectKey: subject })),
-          experiment,
-          valueType
-        );
-
-        switch (valueType) {
-          case ValueTestType.BoolType: {
-            const boolAssignments = assignments.map(
-              (a) => a?.boolValue ?? null
-            );
-            expect(boolAssignments).toEqual(expectedAssignments);
-            break;
-          }
-          case ValueTestType.NumericType: {
-            const numericAssignments = assignments.map(
-              (a) => a?.numericValue ?? null
-            );
-            expect(numericAssignments).toEqual(expectedAssignments);
-            break;
-          }
-          case ValueTestType.StringType: {
-            const stringAssignments = assignments.map(
-              (a) => a?.stringValue ?? null
-            );
-            expect(stringAssignments).toEqual(expectedAssignments);
-            break;
-          }
-          case ValueTestType.JSONType: {
-            const jsonStringAssignments = assignments.map(
-              (a) => a?.stringValue ?? null
-            );
-            expect(jsonStringAssignments).toEqual(expectedAssignments);
-            break;
-          }
-        }
-      }
-    );
-  });
-
-  it("returns null if getAssignment was called for the subject before any RAC was loaded", () => {
-    expect(
-      globalClient.getAssignment(
-        sessionOverrideSubject,
-        sessionOverrideExperiment
-      )
-    ).toEqual(null);
-  });
-
-  it("returns subject from overrides when enabled is true", () => {
-    const entry = {
-      ...mockExperimentConfig,
-      enabled: false,
-      overrides: {
-        "1b50f33aef8f681a13f623963da967ed": "override",
-      },
-      typedOverrides: {
-        "1b50f33aef8f681a13f623963da967ed": "override",
-      },
-    };
-
-    storage.setEntries({ [flagKey]: entry });
-
-    const client = new EppoClient(storage);
-    const mockLogger = td.object<IAssignmentLogger>();
-    client.setLogger(mockLogger);
-
-    const assignment = client.getAssignment("subject-10", flagKey);
-    expect(assignment).toEqual("override");
-    expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
-  });
-
-  it("returns subject from overrides when enabled is false", () => {
-    const entry = {
-      ...mockExperimentConfig,
-      enabled: false,
-      overrides: {
-        "1b50f33aef8f681a13f623963da967ed": "override",
-      },
-      typedOverrides: {
-        "1b50f33aef8f681a13f623963da967ed": "override",
-      },
-    };
-
-    storage.setEntries({ [flagKey]: entry });
-
-    const client = new EppoClient(storage);
-    const mockLogger = td.object<IAssignmentLogger>();
-    client.setLogger(mockLogger);
-    const assignment = client.getAssignment("subject-10", flagKey);
-    expect(assignment).toEqual("override");
-    expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
-  });
-
-  it("logs variation assignment and experiment key", () => {
+  // describe('getStringAssignment', () => {
+  //   it.each(readAssignmentTestData())(
+  //     'test variation assignment splits',
+  //     async ({
+  //       experiment,
+  //       valueType = ValueTestType.StringType,
+  //       subjects,
+  //       subjectsWithAttributes,
+  //       expectedAssignments,
+  //     }: IAssignmentTestCase) => {
+  //       `---- Test Case for ${experiment} Experiment ----`;
+
+  //       const assignments = getAssignmentsWithSubjectAttributes(
+  //         subjectsWithAttributes
+  //           ? subjectsWithAttributes
+  //           : subjects.map((subject) => ({ subjectKey: subject })),
+  //         experiment,
+  //         valueType,
+  //       );
+
+  //       switch (valueType) {
+  //         case ValueTestType.BoolType: {
+  //           const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
+  //           expect(boolAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //         case ValueTestType.NumericType: {
+  //           const numericAssignments = assignments.map((a) => a?.numericValue ?? null);
+  //           expect(numericAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //         case ValueTestType.StringType: {
+  //           const stringAssignments = assignments.map((a) => a?.stringValue ?? null);
+  //           expect(stringAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //         case ValueTestType.JSONType: {
+  //           const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
+  //           expect(jsonStringAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //       }
+  //     },
+  //   );
+  // });
+
+  // it('returns null if getStringAssignment was called for the subject before any RAC was loaded', () => {
+  //   expect(
+  //     globalClient.getStringAssignment(sessionOverrideSubject, sessionOverrideExperiment),
+  //   ).toEqual(null);
+  // });
+
+  it('logs variation assignment and experiment key', () => {
     const mockLogger = td.object<IAssignmentLogger>();
 
     storage.setEntries({ [flagKey]: mockExperimentConfig });
-    const client = new EppoClient(storage);
+    const evaluator = new Evaluator(new MD5Sharder());
+    const client = new EppoClient(evaluator, storage);
     client.setLogger(mockLogger);
 
     const subjectAttributes = { foo: 3 };
-    const assignment = client.getAssignment(
-      "subject-10",
-      flagKey,
-      subjectAttributes
-    );
+    const assignment = client.getStringAssignment('subject-10', flagKey, subjectAttributes);
 
-    expect(assignment).toEqual("control");
+    expect(assignment).toEqual('control');
     expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].subject
-    ).toEqual("subject-10");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].featureFlag
-    ).toEqual(flagKey);
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].experiment
-    ).toEqual(`${flagKey}-${mockExperimentConfig.rules[0].allocationKey}`);
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].allocation
-    ).toEqual(`${mockExperimentConfig.rules[0].allocationKey}`);
+    expect(td.explain(mockLogger.logAssignment).calls[0].args[0].subject).toEqual('subject-10');
+    expect(td.explain(mockLogger.logAssignment).calls[0].args[0].featureFlag).toEqual(flagKey);
+    // expect(td.explain(mockLogger.logAssignment).calls[0].args[0].experiment).toEqual(
+    //   `${flagKey}-${mockExperimentConfig.rules[0].allocationKey}`,
+    // );
+    // expect(td.explain(mockLogger.logAssignment).calls[0].args[0].allocation).toEqual(
+    //   `${mockExperimentConfig.rules[0].allocationKey}`,
+    // );
   });
 
-  it("handles logging exception", () => {
+  it('handles logging exception', () => {
     const mockLogger = td.object<IAssignmentLogger>();
-    td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(
-      new Error("logging error")
-    );
+    td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(new Error('logging error'));
 
     storage.setEntries({ [flagKey]: mockExperimentConfig });
-    const client = new EppoClient(storage);
+    const evaluator = new Evaluator(new MD5Sharder());
+    const client = new EppoClient(evaluator, storage);
     client.setLogger(mockLogger);
 
     const subjectAttributes = { foo: 3 };
-    const assignment = client.getAssignment(
-      "subject-10",
-      flagKey,
-      subjectAttributes
-    );
+    const assignment = client.getStringAssignment('subject-10', flagKey, subjectAttributes);
 
-    expect(assignment).toEqual("control");
+    expect(assignment).toEqual('control');
   });
 
-  describe("assignment logging deduplication", () => {
+  describe('assignment logging deduplication', () => {
     let client: EppoClient;
     let mockLogger: IAssignmentLogger;
 
@@ -446,91 +322,93 @@ describe("EppoClient E2E test", () => {
       mockLogger = td.object<IAssignmentLogger>();
 
       storage.setEntries({ [flagKey]: mockExperimentConfig });
-      client = new EppoClient(storage);
+      const evaluator = new Evaluator(new MD5Sharder());
+      const client = new EppoClient(evaluator, storage);
       client.setLogger(mockLogger);
     });
 
-    it("logs duplicate assignments without an assignment cache", () => {
+    it('logs duplicate assignments without an assignment cache', () => {
       client.disableAssignmentCache();
 
-      client.getAssignment("subject-10", flagKey);
-      client.getAssignment("subject-10", flagKey);
+      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey);
 
       // call count should be 2 because there is no cache.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
     });
 
-    it("does not log duplicate assignments", () => {
+    it('does not log duplicate assignments', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
-      client.getAssignment("subject-10", flagKey);
-      client.getAssignment("subject-10", flagKey);
+      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey);
 
       // call count should be 1 because the second call is a cache hit and not logged.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
     });
 
-    it("logs assignment again after the lru cache is full", () => {
+    it('logs assignment again after the lru cache is full', () => {
       client.useLRUInMemoryAssignmentCache(2);
 
-      client.getAssignment("subject-10", flagKey); // logged
-      client.getAssignment("subject-10", flagKey); // cached
+      client.getStringAssignment('subject-10', flagKey); // logged
+      client.getStringAssignment('subject-10', flagKey); // cached
 
-      client.getAssignment("subject-11", flagKey); // logged
-      client.getAssignment("subject-11", flagKey); // cached
+      client.getStringAssignment('subject-11', flagKey); // logged
+      client.getStringAssignment('subject-11', flagKey); // cached
 
-      client.getAssignment("subject-12", flagKey); // cache evicted subject-10, logged
-      client.getAssignment("subject-10", flagKey); // previously evicted, logged
-      client.getAssignment("subject-12", flagKey); // cached
+      client.getStringAssignment('subject-12', flagKey); // cache evicted subject-10, logged
+      client.getStringAssignment('subject-10', flagKey); // previously evicted, logged
+      client.getStringAssignment('subject-12', flagKey); // cached
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(4);
     });
 
-    it("does not cache assignments if the logger had an exception", () => {
+    it('does not cache assignments if the logger had an exception', () => {
       td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(
-        new Error("logging error")
+        new Error('logging error'),
       );
 
-      const client = new EppoClient(storage);
+      const evaluator = new Evaluator(new MD5Sharder());
+      const client = new EppoClient(evaluator, storage);
       client.setLogger(mockLogger);
 
-      client.getAssignment("subject-10", flagKey);
-      client.getAssignment("subject-10", flagKey);
+      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey);
 
       // call count should be 2 because the first call had an exception
       // therefore we are not sure the logger was successful and try again.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
     });
 
-    it("logs for each unique flag", () => {
+    it('logs for each unique flag', () => {
       storage.setEntries({
         [flagKey]: mockExperimentConfig,
-        "flag-2": {
+        'flag-2': {
           ...mockExperimentConfig,
-          name: "flag-2",
+          name: 'flag-2',
         },
-        "flag-3": {
+        'flag-3': {
           ...mockExperimentConfig,
-          name: "flag-3",
+          name: 'flag-3',
         },
       });
 
       client.useNonExpiringInMemoryAssignmentCache();
 
-      client.getAssignment("subject-10", flagKey);
-      client.getAssignment("subject-10", flagKey);
-      client.getAssignment("subject-10", "flag-2");
-      client.getAssignment("subject-10", "flag-2");
-      client.getAssignment("subject-10", "flag-3");
-      client.getAssignment("subject-10", "flag-3");
-      client.getAssignment("subject-10", flagKey);
-      client.getAssignment("subject-10", "flag-2");
-      client.getAssignment("subject-10", "flag-3");
+      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', 'flag-2');
+      client.getStringAssignment('subject-10', 'flag-2');
+      client.getStringAssignment('subject-10', 'flag-3');
+      client.getStringAssignment('subject-10', 'flag-3');
+      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', 'flag-2');
+      client.getStringAssignment('subject-10', 'flag-3');
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(3);
     });
 
-    it("logs twice for the same flag when rollout increases/flag changes", () => {
+    it('logs twice for the same flag when rollout increases/flag changes', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
       storage.setEntries({
@@ -544,18 +422,18 @@ describe("EppoClient E2E test", () => {
               holdouts: [],
               variations: [
                 {
-                  name: "control",
-                  value: "control",
-                  typedValue: "control",
+                  name: 'control',
+                  value: 'control',
+                  typedValue: 'control',
                   shardRange: {
                     start: 0,
                     end: 10000,
                   },
                 },
                 {
-                  name: "treatment",
-                  value: "treatment",
-                  typedValue: "treatment",
+                  name: 'treatment',
+                  value: 'treatment',
+                  typedValue: 'treatment',
                   shardRange: {
                     start: 0,
                     end: 0,
@@ -566,7 +444,7 @@ describe("EppoClient E2E test", () => {
           },
         },
       });
-      client.getAssignment("subject-10", flagKey);
+      client.getStringAssignment('subject-10', flagKey);
 
       storage.setEntries({
         [flagKey]: {
@@ -579,18 +457,18 @@ describe("EppoClient E2E test", () => {
               holdouts: [],
               variations: [
                 {
-                  name: "control",
-                  value: "control",
-                  typedValue: "control",
+                  name: 'control',
+                  value: 'control',
+                  typedValue: 'control',
                   shardRange: {
                     start: 0,
                     end: 0,
                   },
                 },
                 {
-                  name: "treatment",
-                  value: "treatment",
-                  typedValue: "treatment",
+                  name: 'treatment',
+                  value: 'treatment',
+                  typedValue: 'treatment',
                   shardRange: {
                     start: 0,
                     end: 10000,
@@ -601,18 +479,18 @@ describe("EppoClient E2E test", () => {
           },
         },
       });
-      client.getAssignment("subject-10", flagKey);
+      client.getStringAssignment('subject-10', flagKey);
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
     });
 
-    it("logs the same subject/flag/variation after two changes", () => {
+    it('logs the same subject/flag/variation after two changes', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
       // original configuration version
       storage.setEntries({ [flagKey]: mockExperimentConfig });
 
-      client.getAssignment("subject-10", flagKey); // log this assignment
-      client.getAssignment("subject-10", flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey); // log this assignment
+      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
       // change the flag
       storage.setEntries({
@@ -626,9 +504,9 @@ describe("EppoClient E2E test", () => {
               holdouts: [],
               variations: [
                 {
-                  name: "some-new-treatment",
-                  value: "some-new-treatment",
-                  typedValue: "some-new-treatment",
+                  name: 'some-new-treatment',
+                  value: 'some-new-treatment',
+                  typedValue: 'some-new-treatment',
                   shardRange: {
                     start: 0,
                     end: 10000,
@@ -640,29 +518,29 @@ describe("EppoClient E2E test", () => {
         },
       });
 
-      client.getAssignment("subject-10", flagKey); // log this assignment
-      client.getAssignment("subject-10", flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey); // log this assignment
+      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
       // change the flag again, back to the original
       storage.setEntries({ [flagKey]: mockExperimentConfig });
 
-      client.getAssignment("subject-10", flagKey); // important: log this assignment
-      client.getAssignment("subject-10", flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey); // important: log this assignment
+      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(3);
     });
   });
 
-  it("only returns variation if subject matches rules", () => {
+  it('only returns variation if subject matches rules', () => {
     const entry = {
       ...mockExperimentConfig,
       rules: [
         {
-          allocationKey: "allocation1",
+          allocationKey: 'allocation1',
           conditions: [
             {
               operator: OperatorType.GT,
-              attribute: "appVersion",
+              attribute: 'appVersion',
               value: 10,
             },
           ],
@@ -672,742 +550,422 @@ describe("EppoClient E2E test", () => {
 
     storage.setEntries({ [flagKey]: entry });
 
-    const client = new EppoClient(storage);
-    let assignment = client.getAssignment("subject-10", flagKey, {
+    const evaluator = new Evaluator(new MD5Sharder());
+    const client = new EppoClient(evaluator, storage);
+    let assignment = client.getStringAssignment('subject-10', flagKey, {
       appVersion: 9,
     });
     expect(assignment).toBeNull();
-    assignment = client.getAssignment("subject-10", flagKey);
+    assignment = client.getStringAssignment('subject-10', flagKey);
     expect(assignment).toBeNull();
-    assignment = client.getAssignment("subject-10", flagKey, {
+    assignment = client.getStringAssignment('subject-10', flagKey, {
       appVersion: 11,
     });
-    expect(assignment).toEqual("control");
-  });
-
-  it("returns control variation and logs holdout key if subject is in holdout in an experiment allocation", () => {
-    const entry = {
-      ...mockExperimentConfig,
-      allocations: {
-        allocation1: {
-          percentExposure: 1,
-          statusQuoVariationKey: "variation-7",
-          shippedVariationKey: null,
-          holdouts: [
-            {
-              holdoutKey: "holdout-2",
-              statusQuoShardRange: {
-                start: 0,
-                end: 200,
-              },
-              shippedShardRange: null, // this is an experiment allocation because shippedShardRange is null
-            },
-            {
-              holdoutKey: "holdout-3",
-              statusQuoShardRange: {
-                start: 200,
-                end: 400,
-              },
-              shippedShardRange: null,
-            },
-          ],
-          variations: [
-            {
-              name: "control",
-              value: "control",
-              typedValue: "control",
-              shardRange: {
-                start: 0,
-                end: 3333,
-              },
-              variationKey: "variation-7",
-            },
-            {
-              name: "variant-1",
-              value: "variant-1",
-              typedValue: "variant-1",
-              shardRange: {
-                start: 3333,
-                end: 6667,
-              },
-              variationKey: "variation-8",
-            },
-            {
-              name: "variant-2",
-              value: "variant-2",
-              typedValue: "variant-2",
-              shardRange: {
-                start: 6667,
-                end: 10000,
-              },
-              variationKey: "variation-9",
-            },
-          ],
-        },
-      },
-    };
-
-    storage.setEntries({ [flagKey]: entry });
-
-    const mockLogger = td.object<IAssignmentLogger>();
-    const client = new EppoClient(storage);
-    client.setLogger(mockLogger);
-    td.reset();
-
-    // subject-79 --> holdout shard is 186
-    let assignment = client.getAssignment("subject-79", flagKey);
-    expect(assignment).toEqual("control");
-    // Only log holdout key (not variation) if this is an experiment allocation
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].holdoutVariation
-    ).toBeNull();
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].holdout
-    ).toEqual("holdout-2");
-
-    // subject-8 --> holdout shard is 201
-    assignment = client.getAssignment("subject-8", flagKey);
-    expect(assignment).toEqual("control");
-    // Only log holdout key (not variation) if this is an experiment allocation
-    expect(
-      td.explain(mockLogger.logAssignment).calls[1].args[0].holdoutVariation
-    ).toBeNull();
-    expect(
-      td.explain(mockLogger.logAssignment).calls[1].args[0].holdout
-    ).toEqual("holdout-3");
-
-    // subject-11 --> holdout shard is 9137 (outside holdout), non-holdout assignment shard is 8414
-    assignment = client.getAssignment("subject-11", flagKey);
-    expect(assignment).toEqual("variant-2");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[2].args[0].holdoutVariation
-    ).toBeNull();
-    expect(
-      td.explain(mockLogger.logAssignment).calls[2].args[0].holdout
-    ).toBeNull();
-  });
-
-  it("returns the shipped variation and logs holdout key and variation if subject is in holdout in a rollout allocation", () => {
-    const entry = {
-      ...mockExperimentConfig,
-      allocations: {
-        allocation1: {
-          percentExposure: 1,
-          statusQuoVariationKey: "variation-7",
-          shippedVariationKey: "variation-8",
-          holdouts: [
-            {
-              holdoutKey: "holdout-2",
-              statusQuoShardRange: {
-                start: 0,
-                end: 100,
-              },
-              shippedShardRange: {
-                start: 100,
-                end: 200,
-              },
-            },
-            {
-              holdoutKey: "holdout-3",
-              statusQuoShardRange: {
-                start: 200,
-                end: 300,
-              },
-              shippedShardRange: {
-                start: 300,
-                end: 400,
-              },
-            },
-          ],
-          variations: [
-            {
-              name: "control",
-              value: "control",
-              typedValue: "control",
-              shardRange: {
-                start: 0,
-                end: 0,
-              },
-              variationKey: "variation-7",
-            },
-            {
-              name: "variant-1",
-              value: "variant-1",
-              typedValue: "variant-1",
-              shardRange: {
-                start: 0,
-                end: 0,
-              },
-              variationKey: "variation-8",
-            },
-            {
-              name: "variant-2",
-              value: "variant-2",
-              typedValue: "variant-2",
-              shardRange: {
-                start: 0,
-                end: 10000,
-              },
-              variationKey: "variation-9",
-            },
-          ],
-        },
-      },
-    };
-
-    storage.setEntries({ [flagKey]: entry });
-
-    const mockLogger = td.object<IAssignmentLogger>();
-    const client = new EppoClient(storage);
-    client.setLogger(mockLogger);
-    td.reset();
-
-    // subject-227 --> holdout shard is 57
-    let assignment = client.getAssignment("subject-227", flagKey);
-    expect(assignment).toEqual("control");
-    // Log both holdout key and variation if this is a rollout allocation
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].holdoutVariation
-    ).toEqual("status_quo");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[0].args[0].holdout
-    ).toEqual("holdout-2");
-
-    // subject-79 --> holdout shard is 186
-    assignment = client.getAssignment("subject-79", flagKey);
-    expect(assignment).toEqual("variant-1");
-    // Log both holdout key and variation if this is a rollout allocation
-    expect(
-      td.explain(mockLogger.logAssignment).calls[1].args[0].holdoutVariation
-    ).toEqual("all_shipped_variants");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[1].args[0].holdout
-    ).toEqual("holdout-2");
-
-    // subject-8 --> holdout shard is 201
-    assignment = client.getAssignment("subject-8", flagKey);
-    expect(assignment).toEqual("control");
-    // Log both holdout key and variation if this is a rollout allocation
-    expect(
-      td.explain(mockLogger.logAssignment).calls[2].args[0].holdoutVariation
-    ).toEqual("status_quo");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[2].args[0].holdout
-    ).toEqual("holdout-3");
-
-    // subject-50 --> holdout shard is 347
-    assignment = client.getAssignment("subject-50", flagKey);
-    expect(assignment).toEqual("variant-1");
-    // Log both holdout key and variation if this is a rollout allocation
-    expect(
-      td.explain(mockLogger.logAssignment).calls[3].args[0].holdoutVariation
-    ).toEqual("all_shipped_variants");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[3].args[0].holdout
-    ).toEqual("holdout-3");
-
-    // subject-7 --> holdout shard is 9483 (outside holdout), non-holdout assignment shard is 8673
-    assignment = client.getAssignment("subject-7", flagKey);
-    expect(assignment).toEqual("variant-2");
-    expect(
-      td.explain(mockLogger.logAssignment).calls[4].args[0].holdoutVariation
-    ).toBeNull();
-    expect(
-      td.explain(mockLogger.logAssignment).calls[4].args[0].holdout
-    ).toBeNull();
-  });
-
-  function getAssignmentsWithSubjectAttributes(
-    subjectsWithAttributes: {
-      subjectKey: string;
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      subjectAttributes?: Record<string, any>;
-    }[],
-    experiment: string,
-    valueTestType: ValueTestType = ValueTestType.StringType,
-    obfuscated = false
-  ): (EppoValue | null)[] {
-    return subjectsWithAttributes.map((subject) => {
-      switch (valueTestType) {
-        case ValueTestType.BoolType: {
-          const ba = globalClient.getBoolAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes,
-            undefined,
-            obfuscated
-          );
-          if (ba === null) return null;
-          return EppoValue.Bool(ba);
-        }
-        case ValueTestType.NumericType: {
-          const na = globalClient.getNumericAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes
-          );
-          if (na === null) return null;
-          return EppoValue.Numeric(na);
-        }
-        case ValueTestType.StringType: {
-          const sa = globalClient.getStringAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes
-          );
-          if (sa === null) return null;
-          return EppoValue.String(sa);
-        }
-        case ValueTestType.JSONType: {
-          const sa = globalClient.getJSONStringAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes
-          );
-          const oa = globalClient.getParsedJSONAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes
-          );
-          if (oa == null || sa === null) return null;
-          return EppoValue.JSON(sa, oa);
-        }
-      }
-    });
-  }
-
-  describe("getAssignment with hooks", () => {
-    let client: EppoClient;
-
-    beforeAll(() => {
-      storage.setEntries({ [flagKey]: mockExperimentConfig });
-      client = new EppoClient(storage);
-    });
-
-    describe("onPreAssignment", () => {
-      it("called with experiment key and subject id", () => {
-        const mockHooks = td.object<IAssignmentHooks>();
-        client.getAssignment("subject-identifer", flagKey, {}, mockHooks);
-        expect(td.explain(mockHooks.onPreAssignment).callCount).toEqual(1);
-        expect(td.explain(mockHooks.onPreAssignment).calls[0].args[0]).toEqual(
-          flagKey
-        );
-        expect(td.explain(mockHooks.onPreAssignment).calls[0].args[1]).toEqual(
-          "subject-identifer"
-        );
-      });
-
-      it("overrides returned assignment", async () => {
-        const mockLogger = td.object<IAssignmentLogger>();
-        client.setLogger(mockLogger);
-        td.reset();
-        const variation = await client.getAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          {
-            // eslint-disable-next-line @typescript-eslint/no-unused-vars
-            onPreAssignment(
-              experimentKey: string,
-              subject: string
-            ): EppoValue | null {
-              return EppoValue.String("my-overridden-variation");
-            },
-
-            // eslint-disable-next-line @typescript-eslint/no-unused-vars
-            onPostAssignment(
-              experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-              subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-              variation: EppoValue | null // eslint-disable-line @typescript-eslint/no-unused-vars
-            ): void {
-              // no-op
-            },
-          }
-        );
-
-        expect(variation).toEqual("my-overridden-variation");
-        expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
-      });
-
-      it("uses regular assignment logic if onPreAssignment returns null", async () => {
-        const mockLogger = td.object<IAssignmentLogger>();
-        client.setLogger(mockLogger);
-        td.reset();
-        const variation = await client.getAssignment(
-          "subject-identifer",
-          flagKey,
-          {},
-          {
-            // eslint-disable-next-line @typescript-eslint/no-unused-vars
-            onPreAssignment(
-              experimentKey: string,
-              subject: string
-            ): EppoValue | null {
-              return null;
-            },
-
-            onPostAssignment(
-              experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-              subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-              variation: EppoValue | null // eslint-disable-line @typescript-eslint/no-unused-vars
-            ): void {
-              // no-op
-            },
-          }
-        );
-
-        expect(variation).not.toBeNull();
-        expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
-      });
-    });
-
-    describe("onPostAssignment", () => {
-      it("called with assigned variation after assignment", async () => {
-        const mockHooks = td.object<IAssignmentHooks>();
-        const subject = "subject-identifier";
-        const variation = client.getAssignment(subject, flagKey, {}, mockHooks);
-        expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
-        expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
-        expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(
-          flagKey
-        );
-        expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(
-          subject
-        );
-        expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(
-          EppoValue.String(variation ?? "")
-        );
-      });
-    });
-  });
-});
-
-describe(" EppoClient getAssignment From Obfuscated RAC", () => {
-  const storage = new TestConfigurationStore();
-  const globalClient = new EppoClient(storage);
-
-  beforeAll(async () => {
-    mock.setup();
-    mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
-      const rac = readMockRacResponse(OBFUSCATED_MOCK_RAC_RESPONSE_FILE);
-      return res.status(200).body(JSON.stringify(rac));
-    });
-    await init(storage);
-  });
-
-  afterAll(() => {
-    mock.teardown();
-  });
-
-  it.each(readAssignmentTestData())(
-    "test variation assignment splits",
-    async ({
-      experiment,
-      valueType = ValueTestType.StringType,
-      subjects,
-      subjectsWithAttributes,
-      expectedAssignments,
-    }: IAssignmentTestCase) => {
-      `---- Test Case for ${experiment} Experiment ----`;
-
-      const assignments = getAssignmentsWithSubjectAttributes(
-        subjectsWithAttributes
-          ? subjectsWithAttributes
-          : subjects.map((subject) => ({ subjectKey: subject })),
-        experiment,
-        valueType
-      );
-
-      switch (valueType) {
-        case ValueTestType.BoolType: {
-          const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
-          expect(boolAssignments).toEqual(expectedAssignments);
-          break;
-        }
-        case ValueTestType.NumericType: {
-          const numericAssignments = assignments.map(
-            (a) => a?.numericValue ?? null
-          );
-          expect(numericAssignments).toEqual(expectedAssignments);
-          break;
-        }
-        case ValueTestType.StringType: {
-          const stringAssignments = assignments.map(
-            (a) => a?.stringValue ?? null
-          );
-          expect(stringAssignments).toEqual(expectedAssignments);
-          break;
-        }
-        case ValueTestType.JSONType: {
-          const jsonStringAssignments = assignments.map(
-            (a) => a?.stringValue ?? null
-          );
-          expect(jsonStringAssignments).toEqual(expectedAssignments);
-          break;
-        }
-      }
-    }
-  );
-
-  function getAssignmentsWithSubjectAttributes(
-    subjectsWithAttributes: {
-      subjectKey: string;
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      subjectAttributes?: Record<string, any>;
-    }[],
-    experiment: string,
-    valueTestType: ValueTestType = ValueTestType.StringType
-  ): (EppoValue | null)[] {
-    return subjectsWithAttributes.map((subject) => {
-      switch (valueTestType) {
-        case ValueTestType.BoolType: {
-          const ba = globalClient.getBoolAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes,
-            undefined,
-            true
-          );
-          if (ba === null) return null;
-          return EppoValue.Bool(ba);
-        }
-        case ValueTestType.NumericType: {
-          const na = globalClient.getNumericAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes,
-            undefined,
-            true
-          );
-          if (na === null) return null;
-          return EppoValue.Numeric(na);
-        }
-        case ValueTestType.StringType: {
-          const sa = globalClient.getStringAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes,
-            undefined,
-            true
-          );
-          if (sa === null) return null;
-          return EppoValue.String(sa);
-        }
-        case ValueTestType.JSONType: {
-          const sa = globalClient.getJSONStringAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes,
-            undefined,
-            true
-          );
-          const oa = globalClient.getParsedJSONAssignment(
-            subject.subjectKey,
-            experiment,
-            subject.subjectAttributes,
-            undefined,
-            true
-          );
-          if (oa == null || sa === null) return null;
-          return EppoValue.JSON(sa, oa);
-        }
-      }
-    });
-  }
-});
-
-describe("Eppo Client constructed with configuration request parameters", () => {
-  let client: EppoClient;
-  let storage: IConfigurationStore;
-  let requestConfiguration: ExperimentConfigurationRequestParameters;
-  let mockServerResponseFunc: (res: MockResponse) => MockResponse;
-
-  const racBody = JSON.stringify(readMockRacResponse(MOCK_RAC_RESPONSE_FILE));
-  const flagKey = "randomization_algo";
-  const subjectForGreenVariation = "subject-identiferA";
-
-  const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
-
-  beforeAll(() => {
-    mock.setup();
-    mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
-      return mockServerResponseFunc(res);
-    });
-  });
-
-  beforeEach(() => {
-    storage = new TestConfigurationStore();
-    requestConfiguration = {
-      apiKey: "dummy key",
-      sdkName: "js-client-sdk-common",
-      sdkVersion: packageJson.version,
-    };
-    mockServerResponseFunc = (res) => res.status(200).body(racBody);
-
-    // We only want to fake setTimeout() and clearTimeout()
-    jest.useFakeTimers({
-      advanceTimers: true,
-      doNotFake: [
-        "Date",
-        "hrtime",
-        "nextTick",
-        "performance",
-        "queueMicrotask",
-        "requestAnimationFrame",
-        "cancelAnimationFrame",
-        "requestIdleCallback",
-        "cancelIdleCallback",
-        "setImmediate",
-        "clearImmediate",
-        "setInterval",
-        "clearInterval",
-      ],
-    });
-  });
-
-  afterEach(() => {
-    jest.clearAllTimers();
-    jest.useRealTimers();
-  });
-
-  afterAll(() => {
-    mock.teardown();
-  });
-
-  it("Fetches initial configuration with parameters in constructor", async () => {
-    client = new EppoClient(storage, requestConfiguration);
-    client.setIsGracefulFailureMode(false);
-    // no configuration loaded
-    let variation = client.getAssignment(subjectForGreenVariation, flagKey);
-    expect(variation).toBeNull();
-    // have client fetch configurations
-    await client.fetchFlagConfigurations();
-    variation = client.getAssignment(subjectForGreenVariation, flagKey);
-    expect(variation).toBe("green");
-  });
-
-  it("Fetches initial configuration with parameters provided later", async () => {
-    client = new EppoClient(storage);
-    client.setIsGracefulFailureMode(false);
-    client.setConfigurationRequestParameters(requestConfiguration);
-    // no configuration loaded
-    let variation = client.getAssignment(subjectForGreenVariation, flagKey);
-    expect(variation).toBeNull();
-    // have client fetch configurations
-    await client.fetchFlagConfigurations();
-    variation = client.getAssignment(subjectForGreenVariation, flagKey);
-    expect(variation).toBe("green");
+    expect(assignment).toEqual('control');
   });
 
-  it.each([
-    { pollAfterSuccessfulInitialization: false },
-    { pollAfterSuccessfulInitialization: true },
-  ])(
-    "retries initial configuration request with config %p",
-    async (configModification) => {
-      let callCount = 0;
-      mockServerResponseFunc = (res) => {
-        if (++callCount === 1) {
-          // Throw an error for the first call
-          return res.status(500);
-        } else {
-          // Return a mock object for subsequent calls
-          return res.status(200).body(racBody);
-        }
-      };
-
-      const { pollAfterSuccessfulInitialization } = configModification;
-      requestConfiguration = {
-        ...requestConfiguration,
-        pollAfterSuccessfulInitialization,
-      };
-      client = new EppoClient(storage, requestConfiguration);
-      client.setIsGracefulFailureMode(false);
-      // no configuration loaded
-      let variation = client.getAssignment(subjectForGreenVariation, flagKey);
-      expect(variation).toBeNull();
-
-      // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
-      const fetchPromise = client.fetchFlagConfigurations();
-
-      // Advance timers mid-init to allow retrying
-      await jest.advanceTimersByTimeAsync(maxRetryDelay);
-
-      // Await so it can finish its initialization before this test proceeds
-      await fetchPromise;
-
-      variation = client.getAssignment(subjectForGreenVariation, flagKey);
-      expect(variation).toBe("green");
-      expect(callCount).toBe(2);
-
-      await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
-      // By default, no more polling
-      expect(callCount).toBe(pollAfterSuccessfulInitialization ? 3 : 2);
-    }
-  );
-
-  it.each([
-    {
-      pollAfterFailedInitialization: false,
-      throwOnFailedInitialization: false,
-    },
-    { pollAfterFailedInitialization: false, throwOnFailedInitialization: true },
-    { pollAfterFailedInitialization: true, throwOnFailedInitialization: false },
-    { pollAfterFailedInitialization: true, throwOnFailedInitialization: true },
-  ])(
-    "initial configuration request fails with config %p",
-    async (configModification) => {
-      let callCount = 0;
-      mockServerResponseFunc = (res) => {
-        if (++callCount === 1) {
-          // Throw an error for initialization call
-          return res.status(500);
-        } else {
-          // Return a mock object for subsequent calls
-          return res.status(200).body(racBody);
-        }
-      };
-
-      const { pollAfterFailedInitialization, throwOnFailedInitialization } =
-        configModification;
-
-      // Note: fake time does not play well with errors bubbled up after setTimeout (event loop,
-      // timeout queue, message queue stuff) so we don't allow retries when rethrowing.
-      const numInitialRequestRetries = 0;
-
-      requestConfiguration = {
-        ...requestConfiguration,
-        numInitialRequestRetries,
-        throwOnFailedInitialization,
-        pollAfterFailedInitialization,
-      };
-      client = new EppoClient(storage, requestConfiguration);
-      client.setIsGracefulFailureMode(false);
-      // no configuration loaded
-      expect(
-        client.getAssignment(subjectForGreenVariation, flagKey)
-      ).toBeNull();
-
-      // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
-      if (throwOnFailedInitialization) {
-        await expect(client.fetchFlagConfigurations()).rejects.toThrow();
-      } else {
-        await expect(client.fetchFlagConfigurations()).resolves.toBeUndefined();
-      }
-      expect(callCount).toBe(1);
-      // still no configuration loaded
-      expect(
-        client.getAssignment(subjectForGreenVariation, flagKey)
-      ).toBeNull();
-
-      // Advance timers so a post-init poll can take place
-      await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
-
-      // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
-      expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);
-      expect(client.getAssignment(subjectForGreenVariation, flagKey)).toBe(
-        pollAfterFailedInitialization ? "green" : null
-      );
-    }
-  );
+  //   describe('getStringAssignment with hooks', () => {
+  //     let client: EppoClient;
+
+  //     beforeAll(() => {
+  //       storage.setEntries({ [flagKey]: mockExperimentConfig });
+  //       client = new EppoClient(storage);
+  //     });
+
+  //     describe('onPreAssignment', () => {
+  //       it('called with experiment key and subject id', () => {
+  //         const mockHooks = td.object<IAssignmentHooks>();
+  //         client.getStringAssignment('subject-identifer', flagKey, {}, mockHooks);
+  //         expect(td.explain(mockHooks.onPreAssignment).callCount).toEqual(1);
+  //         expect(td.explain(mockHooks.onPreAssignment).calls[0].args[0]).toEqual(flagKey);
+  //         expect(td.explain(mockHooks.onPreAssignment).calls[0].args[1]).toEqual('subject-identifer');
+  //       });
+
+  //       it('overrides returned assignment', async () => {
+  //         const mockLogger = td.object<IAssignmentLogger>();
+  //         client.setLogger(mockLogger);
+  //         td.reset();
+  //         const variation = await client.getStringAssignment(
+  //           'subject-identifer',
+  //           flagKey,
+  //           {},
+  //           {
+  //             // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  //             onPreAssignment(experimentKey: string, subject: string): EppoValue | null {
+  //               return EppoValue.String('my-overridden-variation');
+  //             },
+
+  //             // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  //             onPostAssignment(
+  //               experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
+  //               subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
+  //               variation: EppoValue | null, // eslint-disable-line @typescript-eslint/no-unused-vars
+  //             ): void {
+  //               // no-op
+  //             },
+  //           },
+  //         );
+
+  //         expect(variation).toEqual('my-overridden-variation');
+  //         expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
+  //       });
+
+  //       it('uses regular assignment logic if onPreAssignment returns null', async () => {
+  //         const mockLogger = td.object<IAssignmentLogger>();
+  //         client.setLogger(mockLogger);
+  //         td.reset();
+  //         const variation = await client.getStringAssignment(
+  //           'subject-identifer',
+  //           flagKey,
+  //           {},
+  //           {
+  //             // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  //             onPreAssignment(experimentKey: string, subject: string): EppoValue | null {
+  //               return null;
+  //             },
+
+  //             onPostAssignment(
+  //               experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
+  //               subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
+  //               variation: EppoValue | null, // eslint-disable-line @typescript-eslint/no-unused-vars
+  //             ): void {
+  //               // no-op
+  //             },
+  //           },
+  //         );
+
+  //         expect(variation).not.toBeNull();
+  //         expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
+  //       });
+  //     });
+
+  //     describe('onPostAssignment', () => {
+  //       it('called with assigned variation after assignment', async () => {
+  //         const mockHooks = td.object<IAssignmentHooks>();
+  //         const subject = 'subject-identifier';
+  //         const variation = client.getStringAssignment(subject, flagKey, {}, mockHooks);
+  //         expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
+  //         expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
+  //         expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(flagKey);
+  //         expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(subject);
+  //         expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(
+  //           EppoValue.String(variation ?? ''),
+  //         );
+  //       });
+  //     });
+  //   });
+  // });
+
+  // describe(' EppoClient getStringAssignment From Obfuscated RAC', () => {
+  //   const storage = new TestConfigurationStore();
+  //   const evaluator = new Evaluator(new MD5Sharder());
+  //   const globalClient = new EppoClient(evaluator, storage);
+
+  //   beforeAll(async () => {
+  //     mock.setup();
+  //     mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
+  //       const rac = readMockUFCResponse(OBFUSCATED_MOCK_RAC_RESPONSE_FILE);
+  //       return res.status(200).body(JSON.stringify(rac));
+  //     });
+  //     await init(storage);
+  //   });
+
+  //   afterAll(() => {
+  //     mock.teardown();
+  //   });
+
+  //   it.each(readAssignmentTestData())(
+  //     'test variation assignment splits',
+  //     async ({
+  //       experiment,
+  //       valueType = ValueTestType.StringType,
+  //       subjects,
+  //       subjectsWithAttributes,
+  //       expectedAssignments,
+  //     }: IAssignmentTestCase) => {
+  //       `---- Test Case for ${experiment} Experiment ----`;
+
+  //       const assignments = getAssignmentsWithSubjectAttributes(
+  //         subjectsWithAttributes
+  //           ? subjectsWithAttributes
+  //           : subjects.map((subject) => ({ subjectKey: subject })),
+  //         experiment,
+  //         valueType,
+  //       );
+
+  //       switch (valueType) {
+  //         case ValueTestType.BoolType: {
+  //           const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
+  //           expect(boolAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //         case ValueTestType.NumericType: {
+  //           const numericAssignments = assignments.map((a) => a?.numericValue ?? null);
+  //           expect(numericAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //         case ValueTestType.StringType: {
+  //           const stringAssignments = assignments.map((a) => a?.stringValue ?? null);
+  //           expect(stringAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //         case ValueTestType.JSONType: {
+  //           const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
+  //           expect(jsonStringAssignments).toEqual(expectedAssignments);
+  //           break;
+  //         }
+  //       }
+  //     },
+  //   );
+
+  //   function getAssignmentsWithSubjectAttributes(
+  //     subjectsWithAttributes: {
+  //       subjectKey: string;
+  //       // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  //       subjectAttributes?: Record<string, any>;
+  //     }[],
+  //     experiment: string,
+  //     valueTestType: ValueTestType = ValueTestType.StringType,
+  //   ): (EppoValue | null)[] {
+  //     return subjectsWithAttributes.map((subject) => {
+  //       switch (valueTestType) {
+  //         case ValueTestType.BoolType: {
+  //           const ba = globalClient.getBoolAssignment(
+  //             subject.subjectKey,
+  //             experiment,
+  //             subject.subjectAttributes,
+  //             undefined,
+  //             true,
+  //           );
+  //           if (ba === null) return null;
+  //           return EppoValue.Bool(ba);
+  //         }
+  //         case ValueTestType.NumericType: {
+  //           const na = globalClient.getNumericAssignment(
+  //             subject.subjectKey,
+  //             experiment,
+  //             subject.subjectAttributes,
+  //             undefined,
+  //             true,
+  //           );
+  //           if (na === null) return null;
+  //           return EppoValue.Numeric(na);
+  //         }
+  //         case ValueTestType.StringType: {
+  //           const sa = globalClient.getStringAssignment(
+  //             subject.subjectKey,
+  //             experiment,
+  //             subject.subjectAttributes,
+  //             undefined,
+  //             true,
+  //           );
+  //           if (sa === null) return null;
+  //           return EppoValue.String(sa);
+  //         }
+  //         case ValueTestType.JSONType: {
+  //           const sa = globalClient.getJSONStringAssignment(
+  //             subject.subjectKey,
+  //             experiment,
+  //             subject.subjectAttributes,
+  //             undefined,
+  //             true,
+  //           );
+  //           const oa = globalClient.getParsedJSONAssignment(
+  //             subject.subjectKey,
+  //             experiment,
+  //             subject.subjectAttributes,
+  //             undefined,
+  //             true,
+  //           );
+  //           if (oa == null || sa === null) return null;
+  //           return EppoValue.JSON(sa, oa);
+  //         }
+  //       }
+  //     });
+  //   }
+  // });
+
+  // describe('Eppo Client constructed with configuration request parameters', () => {
+  //   let client: EppoClient;
+  //   let storage: IConfigurationStore;
+  //   let requestConfiguration: ExperimentConfigurationRequestParameters;
+  //   let mockServerResponseFunc: (res: MockResponse) => MockResponse;
+
+  //   const racBody = JSON.stringify(readMockRacResponse(MOCK_RAC_RESPONSE_FILE));
+  //   const flagKey = 'randomization_algo';
+  //   const subjectForGreenVariation = 'subject-identiferA';
+
+  //   const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
+
+  //   beforeAll(() => {
+  //     mock.setup();
+  //     mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
+  //       return mockServerResponseFunc(res);
+  //     });
+  //   });
+
+  //   beforeEach(() => {
+  //     storage = new TestConfigurationStore();
+  //     requestConfiguration = {
+  //       apiKey: 'dummy key',
+  //       sdkName: 'js-client-sdk-common',
+  //       sdkVersion: packageJson.version,
+  //     };
+  //     mockServerResponseFunc = (res) => res.status(200).body(racBody);
+
+  //     // We only want to fake setTimeout() and clearTimeout()
+  //     jest.useFakeTimers({
+  //       advanceTimers: true,
+  //       doNotFake: [
+  //         'Date',
+  //         'hrtime',
+  //         'nextTick',
+  //         'performance',
+  //         'queueMicrotask',
+  //         'requestAnimationFrame',
+  //         'cancelAnimationFrame',
+  //         'requestIdleCallback',
+  //         'cancelIdleCallback',
+  //         'setImmediate',
+  //         'clearImmediate',
+  //         'setInterval',
+  //         'clearInterval',
+  //       ],
+  //     });
+  //   });
+
+  //   afterEach(() => {
+  //     jest.clearAllTimers();
+  //     jest.useRealTimers();
+  //   });
+
+  //   afterAll(() => {
+  //     mock.teardown();
+  //   });
+
+  //   it('Fetches initial configuration with parameters in constructor', async () => {
+  //     client = new EppoClient(storage, requestConfiguration);
+  //     client.setIsGracefulFailureMode(false);
+  //     // no configuration loaded
+  //     let variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
+  //     expect(variation).toBeNull();
+  //     // have client fetch configurations
+  //     await client.fetchFlagConfigurations();
+  //     variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
+  //     expect(variation).toBe('green');
+  //   });
+
+  //   it('Fetches initial configuration with parameters provided later', async () => {
+  //     client = new EppoClient(storage);
+  //     client.setIsGracefulFailureMode(false);
+  //     client.setConfigurationRequestParameters(requestConfiguration);
+  //     // no configuration loaded
+  //     let variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
+  //     expect(variation).toBeNull();
+  //     // have client fetch configurations
+  //     await client.fetchFlagConfigurations();
+  //     variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
+  //     expect(variation).toBe('green');
+  //   });
+
+  //   it.each([
+  //     { pollAfterSuccessfulInitialization: false },
+  //     { pollAfterSuccessfulInitialization: true },
+  //   ])('retries initial configuration request with config %p', async (configModification) => {
+  //     let callCount = 0;
+  //     mockServerResponseFunc = (res) => {
+  //       if (++callCount === 1) {
+  //         // Throw an error for the first call
+  //         return res.status(500);
+  //       } else {
+  //         // Return a mock object for subsequent calls
+  //         return res.status(200).body(racBody);
+  //       }
+  //     };
+
+  //     const { pollAfterSuccessfulInitialization } = configModification;
+  //     requestConfiguration = {
+  //       ...requestConfiguration,
+  //       pollAfterSuccessfulInitialization,
+  //     };
+  //     client = new EppoClient(storage, requestConfiguration);
+  //     client.setIsGracefulFailureMode(false);
+  //     // no configuration loaded
+  //     let variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
+  //     expect(variation).toBeNull();
+
+  //     // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
+  //     const fetchPromise = client.fetchFlagConfigurations();
+
+  //     // Advance timers mid-init to allow retrying
+  //     await jest.advanceTimersByTimeAsync(maxRetryDelay);
+
+  //     // Await so it can finish its initialization before this test proceeds
+  //     await fetchPromise;
+
+  //     variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
+  //     expect(variation).toBe('green');
+  //     expect(callCount).toBe(2);
+
+  //     await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
+  //     // By default, no more polling
+  //     expect(callCount).toBe(pollAfterSuccessfulInitialization ? 3 : 2);
+  //   });
+
+  //   it.each([
+  //     {
+  //       pollAfterFailedInitialization: false,
+  //       throwOnFailedInitialization: false,
+  //     },
+  //     { pollAfterFailedInitialization: false, throwOnFailedInitialization: true },
+  //     { pollAfterFailedInitialization: true, throwOnFailedInitialization: false },
+  //     { pollAfterFailedInitialization: true, throwOnFailedInitialization: true },
+  //   ])('initial configuration request fails with config %p', async (configModification) => {
+  //     let callCount = 0;
+  //     mockServerResponseFunc = (res) => {
+  //       if (++callCount === 1) {
+  //         // Throw an error for initialization call
+  //         return res.status(500);
+  //       } else {
+  //         // Return a mock object for subsequent calls
+  //         return res.status(200).body(racBody);
+  //       }
+  //     };
+
+  //     const { pollAfterFailedInitialization, throwOnFailedInitialization } = configModification;
+
+  //     // Note: fake time does not play well with errors bubbled up after setTimeout (event loop,
+  //     // timeout queue, message queue stuff) so we don't allow retries when rethrowing.
+  //     const numInitialRequestRetries = 0;
+
+  //     requestConfiguration = {
+  //       ...requestConfiguration,
+  //       numInitialRequestRetries,
+  //       throwOnFailedInitialization,
+  //       pollAfterFailedInitialization,
+  //     };
+  //     client = new EppoClient(storage, requestConfiguration);
+  //     client.setIsGracefulFailureMode(false);
+  //     // no configuration loaded
+  //     expect(client.getStringAssignment(subjectForGreenVariation, flagKey)).toBeNull();
+
+  //     // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
+  //     if (throwOnFailedInitialization) {
+  //       await expect(client.fetchFlagConfigurations()).rejects.toThrow();
+  //     } else {
+  //       await expect(client.fetchFlagConfigurations()).resolves.toBeUndefined();
+  //     }
+  //     expect(callCount).toBe(1);
+  //     // still no configuration loaded
+  //     expect(client.getStringAssignment(subjectForGreenVariation, flagKey)).toBeNull();
+
+  //     // Advance timers so a post-init poll can take place
+  //     await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
+
+  //     // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
+  //     expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);
+  //     expect(client.getStringAssignment(subjectForGreenVariation, flagKey)).toBe(
+  //       pollAfterFailedInitialization ? 'green' : null,
+  //     );
+  //   });
 });
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index f3cb4dd8..1589f79c 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -1,5 +1,4 @@
 import axios from 'axios';
-import * as md5 from 'md5';
 
 import {
   AssignmentCache,
@@ -8,12 +7,7 @@ import {
   NonExpiringInMemoryAssignmentCache,
 } from '../assignment-cache';
 import { IAssignmentHooks } from '../assignment-hooks';
-import {
-  IAssignmentEvent,
-  IAssignmentLogger,
-  HoldoutVariationEnum,
-  NullableHoldoutVariationType,
-} from '../assignment-logger';
+import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
 import {
   BASE_URL as DEFAULT_BASE_URL,
@@ -23,16 +17,13 @@ import {
   MAX_EVENT_QUEUE_SIZE,
   POLL_INTERVAL_MS,
 } from '../constants';
-import { IAllocation } from '../dto/allocation-dto';
-import { IExperimentConfiguration } from '../dto/experiment-configuration-dto';
-import { IVariation } from '../dto/variation-dto';
-import { EppoValue, ValueType } from '../eppo_value';
+import { EppoValue } from '../eppo_value';
 import { Evaluator, FlagEvaluation, noneResult } from '../eval';
 import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
-import { getMD5Hash } from '../obfuscation';
 import initPoller, { IPoller } from '../poller';
+import { AttributeType } from '../types';
 import { validateNotBlank } from '../validation';
 
 /**
@@ -77,15 +68,6 @@ export interface IEppoClient {
     assignmentHooks?: IAssignmentHooks,
   ): number | null;
 
-  getJSONStringAssignment(
-    subjectKey: string,
-    flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes?: Record<string, any>,
-    defaultValue?: string | null,
-    assignmentHooks?: IAssignmentHooks,
-  ): string | null;
-
   getParsedJSONAssignment(
     subjectKey: string,
     flagKey: string,
@@ -102,7 +84,7 @@ export interface IEppoClient {
   useCustomAssignmentCache(cache: AssignmentCache<Cacheable>): void;
 
   setConfigurationRequestParameters(
-    configurationRequestParameters: ExperimentConfigurationRequestParameters,
+    configurationRequestParameters: FlagConfigurationRequestParameters,
   ): void;
 
   fetchFlagConfigurations(): void;
@@ -112,7 +94,7 @@ export interface IEppoClient {
   setIsGracefulFailureMode(gracefulFailureMode: boolean): void;
 }
 
-export type ExperimentConfigurationRequestParameters = {
+export type FlagConfigurationRequestParameters = {
   apiKey: string;
   sdkVersion: string;
   sdkName: string;
@@ -131,14 +113,14 @@ export default class EppoClient implements IEppoClient {
   private isGracefulFailureMode = true;
   private assignmentCache: AssignmentCache<Cacheable> | undefined;
   private configurationStore: IConfigurationStore;
-  private configurationRequestParameters: ExperimentConfigurationRequestParameters | undefined;
+  private configurationRequestParameters: FlagConfigurationRequestParameters | undefined;
   private requestPoller: IPoller | undefined;
   private evaluator: Evaluator;
 
   constructor(
     evaluator: Evaluator,
     configurationStore: IConfigurationStore,
-    configurationRequestParameters?: ExperimentConfigurationRequestParameters,
+    configurationRequestParameters?: FlagConfigurationRequestParameters,
   ) {
     this.evaluator = evaluator;
     this.configurationStore = configurationStore;
@@ -146,7 +128,7 @@ export default class EppoClient implements IEppoClient {
   }
 
   public setConfigurationRequestParameters(
-    configurationRequestParameters: ExperimentConfigurationRequestParameters,
+    configurationRequestParameters: FlagConfigurationRequestParameters,
   ) {
     this.configurationRequestParameters = configurationRequestParameters;
   }
@@ -328,7 +310,7 @@ export default class EppoClient implements IEppoClient {
   public getAssignmentDetail(
     subjectKey: string,
     flagKey: string,
-    subjectAttributes: Record<string, any> = {},
+    subjectAttributes: Record<string, AttributeType> = {},
     expectedVariationType?: VariationType,
     obfuscated = false,
   ): FlagEvaluation {
@@ -360,7 +342,7 @@ export default class EppoClient implements IEppoClient {
     try {
       if (result && result.doLog) {
         // TODO: check assignment cache
-        this.logAssignment(assignmentEvent);
+        this.logAssignment(result);
       }
     } catch (error) {
       console.error(`[Eppo SDK] Error logging assignment event: ${error}`);
@@ -458,8 +440,8 @@ export default class EppoClient implements IEppoClient {
       this.assignmentCache?.setLastLoggedAssignment({
         flagKey: result.flagKey,
         subjectKey: result.subjectKey,
-        allocationKey: result.allocationKey ?? null,
-        variationKey: result.variation?.key ?? null,
+        allocationKey: result.allocationKey ?? '__eppo_no_allocation',
+        variationKey: result.variation?.key ?? '__eppo_no_variation',
       });
     } catch (error) {
       console.error(`[Eppo SDK] Error logging assignment event: ${error.message}`);
diff --git a/src/dto/allocation-dto.ts b/src/dto/allocation-dto.ts
deleted file mode 100644
index 76ffb52b..00000000
--- a/src/dto/allocation-dto.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { IHoldout } from './holdout-dto';
-import { IVariation } from './variation-dto';
-
-export interface IAllocation {
-  percentExposure: number;
-  variations: IVariation[];
-  statusQuoVariationKey: string | null;
-  shippedVariationKey: string | null;
-  holdouts: IHoldout[];
-}
diff --git a/src/dto/experiment-configuration-dto.ts b/src/dto/experiment-configuration-dto.ts
deleted file mode 100644
index 5f806b6e..00000000
--- a/src/dto/experiment-configuration-dto.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { IAllocation } from './allocation-dto';
-import { IRule } from './rule-dto';
-
-export interface IExperimentConfiguration {
-  name: string;
-  enabled: boolean;
-  subjectShards: number;
-  overrides: Record<string, string>;
-  typedOverrides: Record<string, number | boolean | string | object>;
-  allocations: Record<string, IAllocation>;
-  rules: IRule[];
-}
diff --git a/src/dto/holdout-dto.ts b/src/dto/holdout-dto.ts
deleted file mode 100644
index ce0c8210..00000000
--- a/src/dto/holdout-dto.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { IShardRange } from './variation-dto';
-
-export interface IHoldout {
-  statusQuoShardRange: IShardRange;
-  shippedShardRange: IShardRange | null;
-  holdoutKey: string;
-}
diff --git a/src/dto/variation-dto.ts b/src/dto/variation-dto.ts
deleted file mode 100644
index 78ee124e..00000000
--- a/src/dto/variation-dto.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { IValue } from '../eppo_value';
-
-export interface IShardRange {
-  start: number;
-  end: number;
-}
-
-export interface IVariation {
-  variationKey: string;
-  name: string;
-  value: string;
-  typedValue: IValue;
-  shardRange: IShardRange;
-}
diff --git a/src/eppo_value.spec.ts b/src/eppo_value.spec.ts
index 382ea047..22ecdbdb 100644
--- a/src/eppo_value.spec.ts
+++ b/src/eppo_value.spec.ts
@@ -7,7 +7,7 @@ describe('EppoValue toHashedString function', () => {
   });
 
   it('is JsonType', () => {
-    const myInstance = EppoValue.JSON('{"hello":"world"}', { hello: 'world' });
+    const myInstance = EppoValue.JSON({ hello: 'world' });
     expect(myInstance.toHashedString()).toBe('fbc24bcc7a1794758fc1327fcfebdaf6');
   });
 });
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index 203c2ac3..a63e852d 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -1,3 +1,4 @@
+import { VariationType } from './interfaces';
 import { getMD5Hash } from './obfuscation';
 
 export enum EppoValueType {
@@ -33,17 +34,19 @@ export class EppoValue {
 
   static generateEppoValue(
     value: boolean | number | string | object | null | undefined,
-    valueType: EppoValueType,
+    valueType: VariationType,
   ): EppoValue {
     if (value != null && value != undefined) {
       switch (valueType) {
-        case EppoValueType.BoolType:
+        case VariationType.BOOLEAN:
           return EppoValue.Bool(value as boolean);
-        case EppoValueType.NumericType:
+        case VariationType.FLOAT:
+          return EppoValue.Numeric(value as number);
+        case VariationType.INTEGER:
           return EppoValue.Numeric(value as number);
-        case EppoValueType.StringType:
+        case VariationType.STRING:
           return EppoValue.String(value as string);
-        case EppoValueType.JSONType:
+        case VariationType.JSON:
           return EppoValue.JSON(value as object);
         default:
           return EppoValue.String(value as string);
diff --git a/src/eval.spec.ts b/src/eval.spec.ts
index b24402d1..bcf091fd 100644
--- a/src/eval.spec.ts
+++ b/src/eval.spec.ts
@@ -1,5 +1,5 @@
 import { Evaluator, hashKey, isInShardRange } from './eval';
-import { Flag, Variation, Shard, VariationType } from './interfaces';
+import { Flag, Variation, Shard, VariationType, OperatorType } from './interfaces';
 import { MD5Sharder, DeterministicSharder } from './sharders';
 
 describe('Evaluator', () => {
@@ -97,7 +97,7 @@ describe('Evaluator', () => {
       key: 'flag-key',
       enabled: true,
       variationType: VariationType.STRING,
-      variations: { control: { key: 'control', value: 'control' } },
+      variations: { control: { key: 'control', value: 'control-value' } },
       allocations: [
         {
           key: 'allocation',
@@ -117,10 +117,10 @@ describe('Evaluator', () => {
 
     const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'user-1', {}, false);
-    expect(result.variation).toEqual({ key: 'control', value: 'control' });
+    expect(result.variation).toEqual({ key: 'control', value: 'control-value' });
   });
 
-  it('should evaluate flag and target on id', () => {
+  it('should evaluate flag based on a targeting condition based on id', () => {
     const flag: Flag = {
       key: 'flag-key',
       enabled: true,
@@ -129,7 +129,13 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'allocation',
-          rules: [],
+          rules: [
+            {
+              conditions: [
+                { operator: OperatorType.ONE_OF, attribute: 'id', value: ['alice', 'bob'] },
+              ],
+            },
+          ],
           splits: [
             {
               variationKey: 'control',
@@ -144,10 +150,13 @@ describe('Evaluator', () => {
     };
 
     const evaluator = new Evaluator(new MD5Sharder());
-    let result = evaluator.evaluateFlag(flag, 'user-1', {}, false);
+    let result = evaluator.evaluateFlag(flag, 'alice', {}, false);
     expect(result.variation).toEqual({ key: 'control', value: 'control' });
 
-    result = evaluator.evaluateFlag(flag, 'user-3', {}, false);
+    result = evaluator.evaluateFlag(flag, 'bob', {}, false);
+    expect(result.variation).toEqual({ key: 'control', value: 'control' });
+
+    result = evaluator.evaluateFlag(flag, 'charlie', {}, false);
     expect(result.variation).toBeNull();
   });
 
@@ -191,7 +200,13 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'first',
-          rules: [],
+          rules: [
+            {
+              conditions: [
+                { operator: OperatorType.MATCHES, attribute: 'email', value: '.*@example.com' },
+              ],
+            },
+          ],
           splits: [
             {
               variationKey: 'b',
@@ -238,7 +253,13 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'first',
-          rules: [],
+          rules: [
+            {
+              conditions: [
+                { operator: OperatorType.MATCHES, attribute: 'email', value: '.*@example.com' },
+              ],
+            },
+          ],
           splits: [
             {
               variationKey: 'b',
diff --git a/src/eval.ts b/src/eval.ts
index 5247736e..0a2d48fe 100644
--- a/src/eval.ts
+++ b/src/eval.ts
@@ -35,7 +35,7 @@ export class Evaluator {
       if (allocation.endAt && now > allocation.endAt) continue;
 
       if (
-        !allocation.rules ||
+        !allocation.rules.length ||
         allocation.rules.some((rule) =>
           matchesRule(rule, { id: subjectKey, ...subjectAttributes }, obfuscated),
         )
diff --git a/src/experiment-configuration-requestor.ts b/src/experiment-configuration-requestor.ts
index 4a1445f0..4773c14b 100644
--- a/src/experiment-configuration-requestor.ts
+++ b/src/experiment-configuration-requestor.ts
@@ -1,22 +1,22 @@
 import { IConfigurationStore } from './configuration-store';
-import { IExperimentConfiguration } from './dto/experiment-configuration-dto';
 import HttpClient from './http-client';
+import { Flag } from './interfaces';
 
-const RAC_ENDPOINT = '/randomized_assignment/v3/config';
+const UFC_ENDPOINT = '/flag_config/v1/config';
 
-interface IRandomizedAssignmentConfig {
-  flags: Record<string, IExperimentConfiguration>;
+interface IUniversalFlagConfig {
+  flags: Record<string, Flag>;
 }
 
 export default class ExperimentConfigurationRequestor {
   constructor(private configurationStore: IConfigurationStore, private httpClient: HttpClient) {}
 
-  async fetchAndStoreConfigurations(): Promise<Record<string, IExperimentConfiguration>> {
-    const responseData = await this.httpClient.get<IRandomizedAssignmentConfig>(RAC_ENDPOINT);
+  async fetchAndStoreConfigurations(): Promise<Record<string, Flag>> {
+    const responseData = await this.httpClient.get<IUniversalFlagConfig>(UFC_ENDPOINT);
     if (!responseData) {
       return {};
     }
-    this.configurationStore.setEntries<IExperimentConfiguration>(responseData.flags);
+    this.configurationStore.setEntries<Flag>(responseData.flags);
     return responseData.flags;
   }
 }
diff --git a/src/index.ts b/src/index.ts
index a8c67a39..407d795f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,10 +1,7 @@
 import { AssignmentCache } from './assignment-cache';
 import { IAssignmentHooks } from './assignment-hooks';
 import { IAssignmentLogger, IAssignmentEvent } from './assignment-logger';
-import EppoClient, {
-  ExperimentConfigurationRequestParameters,
-  IEppoClient,
-} from './client/eppo-client';
+import EppoClient, { FlagConfigurationRequestParameters, IEppoClient } from './client/eppo-client';
 import { IConfigurationStore } from './configuration-store';
 import * as constants from './constants';
 import ExperimentConfigurationRequestor from './experiment-configuration-requestor';
@@ -23,5 +20,5 @@ export {
   validation,
   IConfigurationStore,
   AssignmentCache,
-  ExperimentConfigurationRequestParameters,
+  FlagConfigurationRequestParameters as ExperimentConfigurationRequestParameters,
 };
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 1f11dde4..dbdb536a 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -1,4 +1,4 @@
-import { Rule } from './rules';
+import { ConditionValueType } from './types';
 
 export enum VariationType {
   STRING = 'string',
@@ -46,3 +46,69 @@ export interface Flag {
   allocations: Allocation[];
   totalShards: number;
 }
+
+export enum OperatorType {
+  MATCHES = 'MATCHES',
+  NOT_MATCHES = 'NOT_MATCHES',
+  GTE = 'GTE',
+  GT = 'GT',
+  LTE = 'LTE',
+  LT = 'LT',
+  ONE_OF = 'ONE_OF',
+  NOT_ONE_OF = 'NOT_ONE_OF',
+}
+
+export enum OperatorValueType {
+  PLAIN_STRING = 'PLAIN_STRING',
+  STRING_ARRAY = 'STRING_ARRAY',
+  SEM_VER = 'SEM_VER',
+  NUMERIC = 'NUMERIC',
+}
+
+interface MatchesCondition {
+  operator: OperatorType.MATCHES;
+  attribute: string;
+  value: string;
+}
+
+interface NotMatchesCondition {
+  operator: OperatorType.NOT_MATCHES;
+  attribute: string;
+  value: string;
+}
+
+interface OneOfCondition {
+  operator: OperatorType.ONE_OF;
+  attribute: string;
+  value: string[];
+}
+
+interface NotOneOfCondition {
+  operator: OperatorType.NOT_ONE_OF;
+  attribute: string;
+  value: string[];
+}
+
+interface SemVerCondition {
+  operator: OperatorType.GTE | OperatorType.GT | OperatorType.LTE | OperatorType.LT;
+  attribute: string;
+  value: string;
+}
+
+interface NumericCondition {
+  operator: OperatorType.GTE | OperatorType.GT | OperatorType.LTE | OperatorType.LT;
+  attribute: string;
+  value: number;
+}
+
+export type Condition =
+  | MatchesCondition
+  | NotMatchesCondition
+  | OneOfCondition
+  | NotOneOfCondition
+  | SemVerCondition
+  | NumericCondition;
+
+export interface Rule {
+  conditions: Condition[];
+}
diff --git a/src/rule_evaluator.ts b/src/rule_evaluator.ts
index f6418933..40d54fdf 100644
--- a/src/rule_evaluator.ts
+++ b/src/rule_evaluator.ts
@@ -7,11 +7,12 @@ import {
   lte as semverLte,
 } from 'semver';
 
-import { Condition, OperatorType, IRule, OperatorValueType } from './dto/rule-dto';
+import { Condition, OperatorType, Rule, OperatorValueType } from './interfaces';
 import { decodeBase64, getMD5Hash } from './obfuscation';
+import { ConditionValueType } from './types';
 
 export function matchesRule(
-  rule: IRule,
+  rule: Rule,
   subjectAttributes: Record<string, any>,
   obfuscated: boolean,
 ): boolean {
@@ -28,11 +29,8 @@ function evaluateRuleConditions(
   conditions: Condition[],
   obfuscated: boolean,
 ): boolean[] {
-  return conditions.map((condition) =>
-    obfuscated
-      ? evaluateObfuscatedCondition(subjectAttributes, condition)
-      : evaluateCondition(subjectAttributes, condition),
-  );
+  // TODO: obfuscated version
+  return conditions.map((condition) => evaluateCondition(subjectAttributes, condition));
 }
 
 function evaluateCondition(subjectAttributes: Record<string, any>, condition: Condition): boolean {
@@ -79,49 +77,50 @@ function evaluateCondition(subjectAttributes: Record<string, any>, condition: Co
   return false;
 }
 
-function evaluateObfuscatedCondition(
-  subjectAttributes: Record<string, any>,
-  condition: Condition,
-): boolean {
-  const hashedSubjectAttributes: Record<string, any> = Object.entries(subjectAttributes).reduce(
-    (accum, [key, val]) => ({ [getMD5Hash(key)]: val, ...accum }),
-    {},
-  );
-  const value = hashedSubjectAttributes[condition.attribute];
-  const conditionValueType = targetingRuleConditionValuesTypesFromValues(value);
-
-  if (value != null) {
-    switch (condition.operator) {
-      case getMD5Hash(OperatorType.GTE):
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, decodeBase64(condition.value), semverGte);
-        }
-        return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a >= b);
-      case getMD5Hash(OperatorType.GT):
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, decodeBase64(condition.value), semverGt);
-        }
-        return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a > b);
-      case getMD5Hash(OperatorType.LTE):
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, decodeBase64(condition.value), semverLte);
-        }
-        return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a <= b);
-      case getMD5Hash(OperatorType.LT):
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, decodeBase64(condition.value), semverLt);
-        }
-        return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a < b);
-      case getMD5Hash(OperatorType.MATCHES):
-        return new RegExp(decodeBase64(condition.value)).test(value as string);
-      case getMD5Hash(OperatorType.ONE_OF):
-        return isOneOf(getMD5Hash(value.toString().toLowerCase()), condition.value);
-      case getMD5Hash(OperatorType.NOT_ONE_OF):
-        return isNotOneOf(getMD5Hash(value.toString().toLowerCase()), condition.value);
-    }
-  }
-  return false;
-}
+// TODO: implement the obfuscated version of this function
+// function evaluateObfuscatedCondition(
+//   subjectAttributes: Record<string, any>,
+//   condition: Condition,
+// ): boolean {
+//   const hashedSubjectAttributes: Record<string, any> = Object.entries(subjectAttributes).reduce(
+//     (accum, [key, val]) => ({ [getMD5Hash(key)]: val, ...accum }),
+//     {},
+//   );
+//   const value = hashedSubjectAttributes[condition.attribute];
+//   const conditionValueType = targetingRuleConditionValuesTypesFromValues(value);
+
+//   if (value != null) {
+//     switch (condition.operator) {
+//       case getMD5Hash(OperatorType.GTE):
+//         if (conditionValueType === OperatorValueType.SEM_VER) {
+//           return compareSemVer(value, decodeBase64(condition.value), semverGte);
+//         }
+//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a >= b);
+//       case getMD5Hash(OperatorType.GT):
+//         if (conditionValueType === OperatorValueType.SEM_VER) {
+//           return compareSemVer(value, decodeBase64(condition.value), semverGt);
+//         }
+//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a > b);
+//       case getMD5Hash(OperatorType.LTE):
+//         if (conditionValueType === OperatorValueType.SEM_VER) {
+//           return compareSemVer(value, decodeBase64(condition.value), semverLte);
+//         }
+//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a <= b);
+//       case getMD5Hash(OperatorType.LT):
+//         if (conditionValueType === OperatorValueType.SEM_VER) {
+//           return compareSemVer(value, decodeBase64(condition.value), semverLt);
+//         }
+//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a < b);
+//       case getMD5Hash(OperatorType.MATCHES):
+//         return new RegExp(decodeBase64(condition.value)).test(value as string);
+//       case getMD5Hash(OperatorType.ONE_OF):
+//         return isOneOf(getMD5Hash(value.toString().toLowerCase()), condition.value);
+//       case getMD5Hash(OperatorType.NOT_ONE_OF):
+//         return isNotOneOf(getMD5Hash(value.toString().toLowerCase()), condition.value);
+//     }
+//   }
+//   return false;
+// }
 
 function isOneOf(attributeValue: string, conditionValue: string[]) {
   return getMatchingStringValues(attributeValue, conditionValue).length > 0;
@@ -155,9 +154,7 @@ function compareSemVer(
   );
 }
 
-function targetingRuleConditionValuesTypesFromValues(
-  value: number | string | string[],
-): OperatorValueType {
+function targetingRuleConditionValuesTypesFromValues(value: ConditionValueType): OperatorValueType {
   // Check if input is a number
   if (typeof value === 'number') {
     return OperatorValueType.NUMERIC;
@@ -168,7 +165,7 @@ function targetingRuleConditionValuesTypesFromValues(
   }
 
   // Check if input is a string that represents a SemVer
-  if (validSemver(value)) {
+  if (typeof value === 'string' && validSemver(value)) {
     return OperatorValueType.SEM_VER;
   }
 
diff --git a/test/testHelpers.ts b/test/testHelpers.ts
index 75732b23..20724884 100644
--- a/test/testHelpers.ts
+++ b/test/testHelpers.ts
@@ -1,14 +1,12 @@
 import * as fs from 'fs';
 
-import { IExperimentConfiguration } from '../src/dto/experiment-configuration-dto';
-import { IVariation } from '../src/dto/variation-dto';
-import { IValue } from '../src/eppo_value';
+import { Flag, VariationType } from '../src/interfaces';
 
-export const TEST_DATA_DIR = './test/data/';
-export const ASSIGNMENT_TEST_DATA_DIR = TEST_DATA_DIR + 'assignment-v2/';
-const MOCK_RAC_FILENAME = 'rac-experiments-v3';
-export const MOCK_RAC_RESPONSE_FILE = `${MOCK_RAC_FILENAME}.json`;
-export const OBFUSCATED_MOCK_RAC_RESPONSE_FILE = `${MOCK_RAC_FILENAME}-obfuscated.json`;
+export const TEST_DATA_DIR = './test/data/ufc/';
+export const ASSIGNMENT_TEST_DATA_DIR = TEST_DATA_DIR + 'tests/';
+const MOCK_UFC_FILENAME = 'flags-v1';
+export const MOCK_UFC_RESPONSE_FILE = `${MOCK_UFC_FILENAME}.json`;
+export const OBFUSCATED_MOCK_UFC_RESPONSE_FILE = `${MOCK_UFC_FILENAME}-obfuscated.json`;
 
 export enum ValueTestType {
   BoolType = 'boolean',
@@ -17,19 +15,21 @@ export enum ValueTestType {
   JSONType = 'json',
 }
 
+interface SubjectTestCase {
+  subjectKey: string;
+  subjectAttributes: Record<string, unknown>;
+  assignment: string | null;
+}
+
 export interface IAssignmentTestCase {
-  experiment: string;
-  valueType: ValueTestType;
+  flag: string;
+  variationType: VariationType;
   percentExposure: number;
-  variations: IVariation[];
-  subjects: string[];
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  subjectsWithAttributes: { subjectKey: string; subjectAttributes: Record<string, any> }[];
-  expectedAssignments: IValue[];
+  subjects: SubjectTestCase[];
 }
 
-export function readMockRacResponse(filename: string): {
-  flags: Record<string, IExperimentConfiguration>;
+export function readMockUFCResponse(filename: string): {
+  flags: Record<string, Flag>;
 } {
   return JSON.parse(fs.readFileSync(TEST_DATA_DIR + filename, 'utf-8'));
 }

From fd04b4bede65d015c12ceefc03fc7ae1eca8e458 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Tue, 26 Mar 2024 17:40:56 -0700
Subject: [PATCH 03/39] make more tests pass

---
 src/client/eppo-client.spec.ts | 69 ++++++++++++++--------------------
 src/interfaces.ts              |  4 +-
 2 files changed, 31 insertions(+), 42 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index e2e47988..cf438a7f 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -21,7 +21,7 @@ import { EppoValue } from '../eppo_value';
 import { Evaluator } from '../eval';
 import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
 import HttpClient from '../http-client';
-import { VariationType } from '../interfaces';
+import { Flag, VariationType } from '../interfaces';
 import { MD5Sharder } from '../sharders';
 
 import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
@@ -94,12 +94,12 @@ describe('EppoClient E2E test', () => {
   };
 
   const variationB = {
-    name: 'b',
+    key: 'b',
     value: 'variation-b',
   };
 
-  const mockExperimentConfig = {
-    name: flagKey,
+  const mockFlag: Flag = {
+    key: flagKey,
     enabled: true,
     variationType: VariationType.STRING,
     variations: { a: variationA, b: variationB },
@@ -109,19 +109,9 @@ describe('EppoClient E2E test', () => {
         rules: [],
         splits: [
           {
-            shards: [
-              { salt: 'traffic', ranges: [{ start: 0, end: 10000 }] },
-              { salt: 'assignment', ranges: [{ start: 0, end: 5000 }] },
-            ],
+            shards: [],
             variationKey: 'a',
           },
-          {
-            shards: [
-              { salt: 'traffic', ranges: [{ start: 0, end: 10000 }] },
-              { salt: 'assignment', ranges: [{ start: 5000, end: 10000 }] },
-            ],
-            variationKey: 'b',
-          },
         ],
         doLog: true,
       },
@@ -134,12 +124,12 @@ describe('EppoClient E2E test', () => {
     const mockHooks = td.object<IAssignmentHooks>();
 
     beforeAll(() => {
-      storage.setEntries({ [flagKey]: mockExperimentConfig });
+      storage.setEntries({ [flagKey]: mockFlag });
       const evaluator = new Evaluator(new MD5Sharder());
       client = new EppoClient(evaluator, storage);
 
-      td.replace(EppoClient.prototype, 'getAssignmentVariation', function () {
-        throw new Error('So Graceful Error');
+      td.replace(EppoClient.prototype, 'getAssignmentDetail', function () {
+        throw new Error('Mock test error');
       });
     });
 
@@ -179,7 +169,7 @@ describe('EppoClient E2E test', () => {
 
   describe('setLogger', () => {
     beforeAll(() => {
-      storage.setEntries({ [flagKey]: mockExperimentConfig });
+      storage.setEntries({ [flagKey]: mockFlag });
     });
 
     it('Invokes logger for queued events', () => {
@@ -279,7 +269,7 @@ describe('EppoClient E2E test', () => {
   it('logs variation assignment and experiment key', () => {
     const mockLogger = td.object<IAssignmentLogger>();
 
-    storage.setEntries({ [flagKey]: mockExperimentConfig });
+    storage.setEntries({ [flagKey]: mockFlag });
     const evaluator = new Evaluator(new MD5Sharder());
     const client = new EppoClient(evaluator, storage);
     client.setLogger(mockLogger);
@@ -287,23 +277,22 @@ describe('EppoClient E2E test', () => {
     const subjectAttributes = { foo: 3 };
     const assignment = client.getStringAssignment('subject-10', flagKey, subjectAttributes);
 
-    expect(assignment).toEqual('control');
+    expect(assignment).toEqual(variationA.value);
     expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
-    expect(td.explain(mockLogger.logAssignment).calls[0].args[0].subject).toEqual('subject-10');
-    expect(td.explain(mockLogger.logAssignment).calls[0].args[0].featureFlag).toEqual(flagKey);
-    // expect(td.explain(mockLogger.logAssignment).calls[0].args[0].experiment).toEqual(
-    //   `${flagKey}-${mockExperimentConfig.rules[0].allocationKey}`,
-    // );
-    // expect(td.explain(mockLogger.logAssignment).calls[0].args[0].allocation).toEqual(
-    //   `${mockExperimentConfig.rules[0].allocationKey}`,
-    // );
+
+    const loggedAssignmentEvent = td.explain(mockLogger.logAssignment).calls[0].args[0];
+    console.log(loggedAssignmentEvent);
+    expect(loggedAssignmentEvent.subject).toEqual('subject-10');
+    expect(loggedAssignmentEvent.featureFlag).toEqual(flagKey);
+    expect(loggedAssignmentEvent.experiment).toEqual(`${flagKey}-${mockFlag.allocations[0].key}`);
+    expect(loggedAssignmentEvent.allocation).toEqual(mockFlag.allocations[0].key);
   });
 
   it('handles logging exception', () => {
     const mockLogger = td.object<IAssignmentLogger>();
     td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(new Error('logging error'));
 
-    storage.setEntries({ [flagKey]: mockExperimentConfig });
+    storage.setEntries({ [flagKey]: mockFlag });
     const evaluator = new Evaluator(new MD5Sharder());
     const client = new EppoClient(evaluator, storage);
     client.setLogger(mockLogger);
@@ -321,7 +310,7 @@ describe('EppoClient E2E test', () => {
     beforeEach(() => {
       mockLogger = td.object<IAssignmentLogger>();
 
-      storage.setEntries({ [flagKey]: mockExperimentConfig });
+      storage.setEntries({ [flagKey]: mockFlag });
       const evaluator = new Evaluator(new MD5Sharder());
       const client = new EppoClient(evaluator, storage);
       client.setLogger(mockLogger);
@@ -382,13 +371,13 @@ describe('EppoClient E2E test', () => {
 
     it('logs for each unique flag', () => {
       storage.setEntries({
-        [flagKey]: mockExperimentConfig,
+        [flagKey]: mockFlag,
         'flag-2': {
-          ...mockExperimentConfig,
+          ...mockFlag,
           name: 'flag-2',
         },
         'flag-3': {
-          ...mockExperimentConfig,
+          ...mockFlag,
           name: 'flag-3',
         },
       });
@@ -413,7 +402,7 @@ describe('EppoClient E2E test', () => {
 
       storage.setEntries({
         [flagKey]: {
-          ...mockExperimentConfig,
+          ...mockFlag,
           allocations: {
             allocation1: {
               percentExposure: 1,
@@ -448,7 +437,7 @@ describe('EppoClient E2E test', () => {
 
       storage.setEntries({
         [flagKey]: {
-          ...mockExperimentConfig,
+          ...mockFlag,
           allocations: {
             allocation1: {
               percentExposure: 1,
@@ -487,7 +476,7 @@ describe('EppoClient E2E test', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
       // original configuration version
-      storage.setEntries({ [flagKey]: mockExperimentConfig });
+      storage.setEntries({ [flagKey]: mockFlag });
 
       client.getStringAssignment('subject-10', flagKey); // log this assignment
       client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
@@ -495,7 +484,7 @@ describe('EppoClient E2E test', () => {
       // change the flag
       storage.setEntries({
         [flagKey]: {
-          ...mockExperimentConfig,
+          ...mockFlag,
           allocations: {
             allocation1: {
               percentExposure: 1,
@@ -522,7 +511,7 @@ describe('EppoClient E2E test', () => {
       client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
       // change the flag again, back to the original
-      storage.setEntries({ [flagKey]: mockExperimentConfig });
+      storage.setEntries({ [flagKey]: mockFlag });
 
       client.getStringAssignment('subject-10', flagKey); // important: log this assignment
       client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
@@ -533,7 +522,7 @@ describe('EppoClient E2E test', () => {
 
   it('only returns variation if subject matches rules', () => {
     const entry = {
-      ...mockExperimentConfig,
+      ...mockFlag,
       rules: [
         {
           allocationKey: 'allocation1',
diff --git a/src/interfaces.ts b/src/interfaces.ts
index dbdb536a..fe8257df 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -1,4 +1,4 @@
-import { ConditionValueType } from './types';
+import { getMD5Hash } from './obfuscation';
 
 export enum VariationType {
   STRING = 'string',
@@ -26,7 +26,7 @@ export interface Shard {
 export interface Split {
   shards: Shard[];
   variationKey: string;
-  extraLogging: Record<string, string>;
+  extraLogging?: Record<string, string>;
 }
 
 export interface Allocation {

From ca7ea07aab96f0996d0e6200598bcd7684432115 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 27 Mar 2024 18:24:17 -0700
Subject: [PATCH 04/39] wip

---
 src/client/eppo-client.spec.ts | 213 +++++++++++++++++++--------------
 src/client/eppo-client.ts      |  37 +++++-
 src/eppo_value.ts              |   2 +-
 src/eval.ts                    |   2 +-
 src/interfaces.ts              |   2 +-
 test/testHelpers.ts            |   6 +-
 6 files changed, 160 insertions(+), 102 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index cf438a7f..decc2402 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -6,8 +6,10 @@ import * as td from 'testdouble';
 import mock, { MockResponse } from 'xhr-mock';
 
 import {
+  IAssignmentTestCase,
   MOCK_UFC_RESPONSE_FILE,
   OBFUSCATED_MOCK_UFC_RESPONSE_FILE,
+  SubjectTestCase,
   ValueTestType,
   readAssignmentTestData,
   readMockUFCResponse,
@@ -23,6 +25,7 @@ import ExperimentConfigurationRequestor from '../experiment-configuration-reques
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
 import { MD5Sharder } from '../sharders';
+import { AttributeType, SubjectAttributes } from '../types';
 
 import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
 
@@ -48,6 +51,29 @@ class TestConfigurationStore implements IConfigurationStore {
   }
 }
 
+function getTestAssignments(
+  testCase: IAssignmentTestCase,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  assignmentFn: any,
+): { subject: SubjectTestCase; assignment: string | boolean | number | null | object }[] {
+  const assignments: {
+    subject: SubjectTestCase;
+    assignment: string | boolean | number | null | object;
+  }[] = [];
+  for (const subject of testCase.subjects) {
+    const assignment = assignmentFn(
+      subject.subjectKey,
+      testCase.flag,
+      subject.subjectAttributes,
+      null,
+      undefined,
+      false,
+    );
+    assignments.push({ subject: subject, assignment: assignment });
+  }
+  return assignments;
+}
+
 export async function init(configurationStore: IConfigurationStore) {
   const axiosInstance = axios.create({
     baseURL: 'http://127.0.0.1:4000',
@@ -68,9 +94,9 @@ export async function init(configurationStore: IConfigurationStore) {
 }
 
 describe('EppoClient E2E test', () => {
-  // const evaluator = new Evaluator(new MD5Sharder());
+  const evaluator = new Evaluator(new MD5Sharder());
   const storage = new TestConfigurationStore();
-  // const globalClient = new EppoClient(evaluator, storage);
+  const globalClient = new EppoClient(evaluator, storage);
 
   beforeAll(async () => {
     mock.setup();
@@ -142,10 +168,21 @@ describe('EppoClient E2E test', () => {
 
       expect(client.getBoolAssignment('subject-identifer', flagKey, {})).toBeNull();
       expect(client.getNumericAssignment('subject-identifer', flagKey, {})).toBeNull();
-      expect(client.getParsedJSONAssignment('subject-identifer', flagKey, {})).toBeNull();
+      expect(client.getJSONAssignment('subject-identifer', flagKey, {})).toBeNull();
       expect(client.getStringAssignment('subject-identifer', flagKey, {})).toBeNull();
     });
 
+    it('returns default value when graceful failure if error encounterd', async () => {
+      client.setIsGracefulFailureMode(true);
+
+      expect(client.getBoolAssignment('subject-identifer', flagKey, {}, true)).toBe(true);
+      expect(client.getNumericAssignment('subject-identifer', flagKey, {}, 1)).toBe(1);
+      expect(client.getJSONAssignment('subject-identifer', flagKey, {}, {})).toEqual({});
+      expect(client.getStringAssignment('subject-identifer', flagKey, {}, 'default')).toBe(
+        'default',
+      );
+    });
+
     it('throws error when graceful failure is false', async () => {
       client.setIsGracefulFailureMode(false);
 
@@ -154,7 +191,7 @@ describe('EppoClient E2E test', () => {
       }).toThrow();
 
       expect(() => {
-        client.getParsedJSONAssignment('subject-identifer', flagKey, {});
+        client.getJSONAssignment('subject-identifer', flagKey, {});
       }).toThrow();
 
       expect(() => {
@@ -214,51 +251,66 @@ describe('EppoClient E2E test', () => {
     });
   });
 
-  // describe('getStringAssignment', () => {
-  //   it.each(readAssignmentTestData())(
-  //     'test variation assignment splits',
-  //     async ({
-  //       experiment,
-  //       valueType = ValueTestType.StringType,
-  //       subjects,
-  //       subjectsWithAttributes,
-  //       expectedAssignments,
-  //     }: IAssignmentTestCase) => {
-  //       `---- Test Case for ${experiment} Experiment ----`;
-
-  //       const assignments = getAssignmentsWithSubjectAttributes(
-  //         subjectsWithAttributes
-  //           ? subjectsWithAttributes
-  //           : subjects.map((subject) => ({ subjectKey: subject })),
-  //         experiment,
-  //         valueType,
-  //       );
-
-  //       switch (valueType) {
-  //         case ValueTestType.BoolType: {
-  //           const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
-  //           expect(boolAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //         case ValueTestType.NumericType: {
-  //           const numericAssignments = assignments.map((a) => a?.numericValue ?? null);
-  //           expect(numericAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //         case ValueTestType.StringType: {
-  //           const stringAssignments = assignments.map((a) => a?.stringValue ?? null);
-  //           expect(stringAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //         case ValueTestType.JSONType: {
-  //           const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
-  //           expect(jsonStringAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //       }
-  //     },
-  //   );
-  // });
+  describe('UFC General Test Cases', () => {
+    it.each(readAssignmentTestData())(
+      'test variation assignment splits',
+      async ({ flag, variationType, subjects }: IAssignmentTestCase) => {
+        `---- Test Case for ${flag} Experiment ----`;
+
+        const evaluator = new Evaluator(new MD5Sharder());
+        const client = new EppoClient(evaluator, storage);
+
+        let assignments: {
+          subject: SubjectTestCase;
+          assignment: string | boolean | number | null | object;
+        }[] = [];
+        switch (variationType) {
+          case VariationType.BOOLEAN: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getBoolAssignment,
+            );
+            break;
+          }
+          case VariationType.NUMERIC: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getNumericAssignment,
+            );
+            break;
+          }
+          case VariationType.INTEGER: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getIntegerAssignment,
+            );
+            break;
+          }
+          case VariationType.STRING: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getStringAssignment,
+            );
+            break;
+          }
+          case VariationType.JSON: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getStringAssignment,
+            );
+            break;
+          }
+          default: {
+            throw new Error(`Unknown variation type: ${variationType}`);
+          }
+        }
+        console.log(assignments);
+        for (const { subject, assignment } of assignments) {
+          expect(assignment).toEqual(subject.assignment);
+        }
+      },
+    );
+  });
 
   // it('returns null if getStringAssignment was called for the subject before any RAC was loaded', () => {
   //   expect(
@@ -266,6 +318,20 @@ describe('EppoClient E2E test', () => {
   //   ).toEqual(null);
   // });
 
+  it('returns default value when key does not exist', async () => {
+    const evaluator = new Evaluator(new MD5Sharder());
+    const client = new EppoClient(evaluator, storage);
+
+    const nonExistantFlag = 'non-existent-flag';
+
+    expect(client.getBoolAssignment('subject-identifer', nonExistantFlag, {}, true)).toBe(true);
+    expect(client.getNumericAssignment('subject-identifer', nonExistantFlag, {}, 1)).toBe(1);
+    expect(client.getJSONAssignment('subject-identifer', nonExistantFlag, {}, {})).toEqual({});
+    expect(client.getStringAssignment('subject-identifer', nonExistantFlag, {}, 'default')).toBe(
+      'default',
+    );
+  });
+
   it('logs variation assignment and experiment key', () => {
     const mockLogger = td.object<IAssignmentLogger>();
 
@@ -281,7 +347,6 @@ describe('EppoClient E2E test', () => {
     expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
 
     const loggedAssignmentEvent = td.explain(mockLogger.logAssignment).calls[0].args[0];
-    console.log(loggedAssignmentEvent);
     expect(loggedAssignmentEvent.subject).toEqual('subject-10');
     expect(loggedAssignmentEvent.featureFlag).toEqual(flagKey);
     expect(loggedAssignmentEvent.experiment).toEqual(`${flagKey}-${mockFlag.allocations[0].key}`);
@@ -300,19 +365,20 @@ describe('EppoClient E2E test', () => {
     const subjectAttributes = { foo: 3 };
     const assignment = client.getStringAssignment('subject-10', flagKey, subjectAttributes);
 
-    expect(assignment).toEqual('control');
+    expect(assignment).toEqual('variation-a');
   });
 
   describe('assignment logging deduplication', () => {
     let client: EppoClient;
+    let evaluator: Evaluator;
     let mockLogger: IAssignmentLogger;
 
     beforeEach(() => {
       mockLogger = td.object<IAssignmentLogger>();
 
       storage.setEntries({ [flagKey]: mockFlag });
-      const evaluator = new Evaluator(new MD5Sharder());
-      const client = new EppoClient(evaluator, storage);
+      evaluator = new Evaluator(new MD5Sharder());
+      client = new EppoClient(evaluator, storage);
       client.setLogger(mockLogger);
     });
 
@@ -357,8 +423,6 @@ describe('EppoClient E2E test', () => {
         new Error('logging error'),
       );
 
-      const evaluator = new Evaluator(new MD5Sharder());
-      const client = new EppoClient(evaluator, storage);
       client.setLogger(mockLogger);
 
       client.getStringAssignment('subject-10', flagKey);
@@ -374,11 +438,11 @@ describe('EppoClient E2E test', () => {
         [flagKey]: mockFlag,
         'flag-2': {
           ...mockFlag,
-          name: 'flag-2',
+          key: 'flag-2',
         },
         'flag-3': {
           ...mockFlag,
-          name: 'flag-3',
+          key: 'flag-3',
         },
       });
 
@@ -398,6 +462,7 @@ describe('EppoClient E2E test', () => {
     });
 
     it('logs twice for the same flag when rollout increases/flag changes', () => {
+      // TODO: update test to UFC format
       client.useNonExpiringInMemoryAssignmentCache();
 
       storage.setEntries({
@@ -473,6 +538,7 @@ describe('EppoClient E2E test', () => {
     });
 
     it('logs the same subject/flag/variation after two changes', () => {
+      // TODO: update test to UFC format
       client.useNonExpiringInMemoryAssignmentCache();
 
       // original configuration version
@@ -520,39 +586,6 @@ describe('EppoClient E2E test', () => {
     });
   });
 
-  it('only returns variation if subject matches rules', () => {
-    const entry = {
-      ...mockFlag,
-      rules: [
-        {
-          allocationKey: 'allocation1',
-          conditions: [
-            {
-              operator: OperatorType.GT,
-              attribute: 'appVersion',
-              value: 10,
-            },
-          ],
-        },
-      ],
-    };
-
-    storage.setEntries({ [flagKey]: entry });
-
-    const evaluator = new Evaluator(new MD5Sharder());
-    const client = new EppoClient(evaluator, storage);
-    let assignment = client.getStringAssignment('subject-10', flagKey, {
-      appVersion: 9,
-    });
-    expect(assignment).toBeNull();
-    assignment = client.getStringAssignment('subject-10', flagKey);
-    expect(assignment).toBeNull();
-    assignment = client.getStringAssignment('subject-10', flagKey, {
-      appVersion: 11,
-    });
-    expect(assignment).toEqual('control');
-  });
-
   //   describe('getStringAssignment with hooks', () => {
   //     let client: EppoClient;
 
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 1589f79c..aa5112ed 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -68,7 +68,7 @@ export interface IEppoClient {
     assignmentHooks?: IAssignmentHooks,
   ): number | null;
 
-  getParsedJSONAssignment(
+  getJSONAssignment(
     subjectKey: string,
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -247,12 +247,33 @@ export default class EppoClient implements IEppoClient {
         defaultValue ? EppoValue.Numeric(defaultValue) : EppoValue.Null(),
         assignmentHooks,
         obfuscated,
-        VariationType.FLOAT,
+        VariationType.NUMERIC,
       ).numericValue ?? null
     );
   }
 
-  public getParsedJSONAssignment(
+  getIntegerAssignment(
+    subjectKey: string,
+    flagKey: string,
+    subjectAttributes?: Record<string, EppoValue>,
+    defaultValue?: number | null,
+    assignmentHooks?: IAssignmentHooks | undefined,
+    obfuscated = false,
+  ): number | null {
+    return (
+      this.getAssignmentVariation(
+        subjectKey,
+        flagKey,
+        subjectAttributes,
+        defaultValue ? EppoValue.Numeric(defaultValue) : EppoValue.Null(),
+        assignmentHooks,
+        obfuscated,
+        VariationType.INTEGER,
+      ).numericValue ?? null
+    );
+  }
+
+  public getJSONAssignment(
     subjectKey: string,
     flagKey: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -298,12 +319,16 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         subjectAttributes,
         expectedVariationType,
+        obfuscated,
       );
 
-      // TODO: figure out whether to translate VariationType to EppoValueType or not
-      return EppoValue.generateEppoValue(result.variation?.value, expectedVariationType);
+      if (!result.variation) {
+        return defaultValue;
+      }
+
+      return EppoValue.generateEppoValue(result.variation.value, expectedVariationType);
     } catch (error) {
-      return this.rethrowIfNotGraceful(error);
+      return this.rethrowIfNotGraceful(error, defaultValue);
     }
   }
 
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index a63e852d..a04af801 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -40,7 +40,7 @@ export class EppoValue {
       switch (valueType) {
         case VariationType.BOOLEAN:
           return EppoValue.Bool(value as boolean);
-        case VariationType.FLOAT:
+        case VariationType.NUMERIC:
           return EppoValue.Numeric(value as number);
         case VariationType.INTEGER:
           return EppoValue.Numeric(value as number);
diff --git a/src/eval.ts b/src/eval.ts
index 0a2d48fe..34ad548b 100644
--- a/src/eval.ts
+++ b/src/eval.ts
@@ -50,7 +50,7 @@ export class Evaluator {
               subjectAttributes,
               allocationKey: allocation.key,
               variation: flag.variations[split.variationKey],
-              extraLogging: split.extraLogging,
+              extraLogging: split.extraLogging ?? {},
               doLog: allocation.doLog,
             };
           }
diff --git a/src/interfaces.ts b/src/interfaces.ts
index fe8257df..27fae25a 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -3,7 +3,7 @@ import { getMD5Hash } from './obfuscation';
 export enum VariationType {
   STRING = 'string',
   INTEGER = 'integer',
-  FLOAT = 'float',
+  NUMERIC = 'numeric',
   BOOLEAN = 'boolean',
   JSON = 'json',
 }
diff --git a/test/testHelpers.ts b/test/testHelpers.ts
index 20724884..6d3eebcc 100644
--- a/test/testHelpers.ts
+++ b/test/testHelpers.ts
@@ -1,6 +1,7 @@
 import * as fs from 'fs';
 
 import { Flag, VariationType } from '../src/interfaces';
+import { AttributeType } from '../src/types';
 
 export const TEST_DATA_DIR = './test/data/ufc/';
 export const ASSIGNMENT_TEST_DATA_DIR = TEST_DATA_DIR + 'tests/';
@@ -15,16 +16,15 @@ export enum ValueTestType {
   JSONType = 'json',
 }
 
-interface SubjectTestCase {
+export interface SubjectTestCase {
   subjectKey: string;
-  subjectAttributes: Record<string, unknown>;
+  subjectAttributes: Record<string, AttributeType>;
   assignment: string | null;
 }
 
 export interface IAssignmentTestCase {
   flag: string;
   variationType: VariationType;
-  percentExposure: number;
   subjects: SubjectTestCase[];
 }
 

From 2a7b3ec4b6981c4985042031dd891bdbb2b27d86 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 27 Mar 2024 18:36:33 -0700
Subject: [PATCH 05/39] fix more tests

---
 src/client/eppo-client.spec.ts | 96 +++++++++++-----------------------
 src/client/eppo-client.ts      |  1 +
 2 files changed, 31 insertions(+), 66 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index decc2402..5b1b668b 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -461,41 +461,26 @@ describe('EppoClient E2E test', () => {
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(3);
     });
 
-    it('logs twice for the same flag when rollout increases/flag changes', () => {
-      // TODO: update test to UFC format
+    it('logs twice for the same flag when allocations change', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
       storage.setEntries({
         [flagKey]: {
           ...mockFlag,
-          allocations: {
-            allocation1: {
-              percentExposure: 1,
-              statusQuoVariationKey: null,
-              shippedVariationKey: null,
-              holdouts: [],
-              variations: [
-                {
-                  name: 'control',
-                  value: 'control',
-                  typedValue: 'control',
-                  shardRange: {
-                    start: 0,
-                    end: 10000,
-                  },
-                },
+
+          allocations: [
+            {
+              key: 'allocation-a-2',
+              rules: [],
+              splits: [
                 {
-                  name: 'treatment',
-                  value: 'treatment',
-                  typedValue: 'treatment',
-                  shardRange: {
-                    start: 0,
-                    end: 0,
-                  },
+                  shards: [],
+                  variationKey: 'a',
                 },
               ],
+              doLog: true,
             },
-          },
+          ],
         },
       });
       client.getStringAssignment('subject-10', flagKey);
@@ -503,34 +488,19 @@ describe('EppoClient E2E test', () => {
       storage.setEntries({
         [flagKey]: {
           ...mockFlag,
-          allocations: {
-            allocation1: {
-              percentExposure: 1,
-              statusQuoVariationKey: null,
-              shippedVariationKey: null,
-              holdouts: [],
-              variations: [
+          allocations: [
+            {
+              key: 'allocation-a-3',
+              rules: [],
+              splits: [
                 {
-                  name: 'control',
-                  value: 'control',
-                  typedValue: 'control',
-                  shardRange: {
-                    start: 0,
-                    end: 0,
-                  },
-                },
-                {
-                  name: 'treatment',
-                  value: 'treatment',
-                  typedValue: 'treatment',
-                  shardRange: {
-                    start: 0,
-                    end: 10000,
-                  },
+                  shards: [],
+                  variationKey: 'a',
                 },
               ],
+              doLog: true,
             },
-          },
+          ],
         },
       });
       client.getStringAssignment('subject-10', flagKey);
@@ -538,7 +508,6 @@ describe('EppoClient E2E test', () => {
     });
 
     it('logs the same subject/flag/variation after two changes', () => {
-      // TODO: update test to UFC format
       client.useNonExpiringInMemoryAssignmentCache();
 
       // original configuration version
@@ -551,25 +520,19 @@ describe('EppoClient E2E test', () => {
       storage.setEntries({
         [flagKey]: {
           ...mockFlag,
-          allocations: {
-            allocation1: {
-              percentExposure: 1,
-              statusQuoVariationKey: null,
-              shippedVariationKey: null,
-              holdouts: [],
-              variations: [
+          allocations: [
+            {
+              key: 'allocation-b',
+              rules: [],
+              splits: [
                 {
-                  name: 'some-new-treatment',
-                  value: 'some-new-treatment',
-                  typedValue: 'some-new-treatment',
-                  shardRange: {
-                    start: 0,
-                    end: 10000,
-                  },
+                  shards: [],
+                  variationKey: 'b',
                 },
               ],
+              doLog: true,
             },
-          },
+          ],
         },
       });
 
@@ -579,6 +542,7 @@ describe('EppoClient E2E test', () => {
       // change the flag again, back to the original
       storage.setEntries({ [flagKey]: mockFlag });
 
+      // Question: Why do we need to log the same assignment again?
       client.getStringAssignment('subject-10', flagKey); // important: log this assignment
       client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index aa5112ed..f2363339 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -218,6 +218,7 @@ export default class EppoClient implements IEppoClient {
     assignmentHooks?: IAssignmentHooks | undefined,
     obfuscated = false,
   ): boolean | null {
+    console.log(this.getAssignmentVariation);
     return (
       this.getAssignmentVariation(
         subjectKey,

From d676f4ce3cff708cb4e52983d515c333be32dfc4 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 27 Mar 2024 18:41:07 -0700
Subject: [PATCH 06/39] update event cache test

---
 src/client/eppo-client.spec.ts | 32 +++++++++++++++++++++++++++-----
 1 file changed, 27 insertions(+), 5 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 5b1b668b..cad2d9af 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -516,18 +516,18 @@ describe('EppoClient E2E test', () => {
       client.getStringAssignment('subject-10', flagKey); // log this assignment
       client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
-      // change the flag
+      // change the variation
       storage.setEntries({
         [flagKey]: {
           ...mockFlag,
           allocations: [
             {
-              key: 'allocation-b',
+              key: 'allocation-a', // note: same key
               rules: [],
               splits: [
                 {
                   shards: [],
-                  variationKey: 'b',
+                  variationKey: 'b', // but different variation!
                 },
               ],
               doLog: true,
@@ -542,11 +542,33 @@ describe('EppoClient E2E test', () => {
       // change the flag again, back to the original
       storage.setEntries({ [flagKey]: mockFlag });
 
-      // Question: Why do we need to log the same assignment again?
       client.getStringAssignment('subject-10', flagKey); // important: log this assignment
       client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
 
-      expect(td.explain(mockLogger.logAssignment).callCount).toEqual(3);
+      // change the allocation
+      storage.setEntries({
+        [flagKey]: {
+          ...mockFlag,
+          allocations: [
+            {
+              key: 'allocation-b', // note: different key
+              rules: [],
+              splits: [
+                {
+                  shards: [],
+                  variationKey: 'b', // variation has been seen before
+                },
+              ],
+              doLog: true,
+            },
+          ],
+        },
+      });
+
+      client.getStringAssignment('subject-10', flagKey); // log this assignment
+      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
+
+      expect(td.explain(mockLogger.logAssignment).callCount).toEqual(4);
     });
   });
 

From 12eb7d1a3b672dc142664eb413867cbffcbfafa8 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 27 Mar 2024 19:00:48 -0700
Subject: [PATCH 07/39] .bind(this)

---
 src/client/eppo-client.spec.ts | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index cad2d9af..a92e33d7 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -268,35 +268,35 @@ describe('EppoClient E2E test', () => {
           case VariationType.BOOLEAN: {
             assignments = getTestAssignments(
               { flag, variationType, subjects },
-              client.getBoolAssignment,
+              client.getBoolAssignment.bind(client),
             );
             break;
           }
           case VariationType.NUMERIC: {
             assignments = getTestAssignments(
               { flag, variationType, subjects },
-              client.getNumericAssignment,
+              client.getNumericAssignment.bind(client),
             );
             break;
           }
           case VariationType.INTEGER: {
             assignments = getTestAssignments(
               { flag, variationType, subjects },
-              client.getIntegerAssignment,
+              client.getIntegerAssignment.bind(client),
             );
             break;
           }
           case VariationType.STRING: {
             assignments = getTestAssignments(
               { flag, variationType, subjects },
-              client.getStringAssignment,
+              client.getStringAssignment.bind(client),
             );
             break;
           }
           case VariationType.JSON: {
             assignments = getTestAssignments(
               { flag, variationType, subjects },
-              client.getStringAssignment,
+              client.getStringAssignment.bind(client),
             );
             break;
           }

From e9bb4b3f7a2b1ce866757668e0ac20d6b25c96b4 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 27 Mar 2024 19:07:15 -0700
Subject: [PATCH 08/39] pass all the tests

---
 src/client/eppo-client.spec.ts |  3 +--
 src/eppo_value.ts              | 10 ++++++++--
 2 files changed, 9 insertions(+), 4 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index a92e33d7..0ba6b760 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -296,7 +296,7 @@ describe('EppoClient E2E test', () => {
           case VariationType.JSON: {
             assignments = getTestAssignments(
               { flag, variationType, subjects },
-              client.getStringAssignment.bind(client),
+              client.getJSONAssignment.bind(client),
             );
             break;
           }
@@ -304,7 +304,6 @@ describe('EppoClient E2E test', () => {
             throw new Error(`Unknown variation type: ${variationType}`);
           }
         }
-        console.log(assignments);
         for (const { subject, assignment } of assignments) {
           expect(assignment).toEqual(subject.assignment);
         }
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index a04af801..c8115d2a 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -116,8 +116,14 @@ export class EppoValue {
     return new EppoValue(EppoValueType.StringType, undefined, undefined, value, undefined);
   }
 
-  static JSON(value: object): EppoValue {
-    return new EppoValue(EppoValueType.JSONType, undefined, undefined, undefined, value);
+  static JSON(value: string | object): EppoValue {
+    return new EppoValue(
+      EppoValueType.JSONType,
+      undefined,
+      undefined,
+      undefined,
+      typeof value === 'string' ? JSON.parse(value) : value,
+    );
   }
 
   static Null(): EppoValue {

From 684da8ad92ebef095a12fb330b582311e868a029 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 28 Mar 2024 16:34:47 -0700
Subject: [PATCH 09/39] Update script to create obfuscated UFC

---
 package.json                   |  2 +-
 test/writeObfuscatedMockRac.ts | 36 -----------------------
 test/writeObfuscatedMockUFC.ts | 52 ++++++++++++++++++++++++++++++++++
 3 files changed, 53 insertions(+), 37 deletions(-)
 delete mode 100644 test/writeObfuscatedMockRac.ts
 create mode 100644 test/writeObfuscatedMockUFC.ts

diff --git a/package.json b/package.json
index 6d4c906e..20db6832 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
     "typecheck": "tsc",
     "test": "yarn test:unit",
     "test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
-    "obfuscate-mock-rac": "ts-node test/writeObfuscatedMockRac"
+    "obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC"
   },
   "jsdelivr": "dist/eppo-sdk.js",
   "repository": {
diff --git a/test/writeObfuscatedMockRac.ts b/test/writeObfuscatedMockRac.ts
deleted file mode 100644
index 06f66cda..00000000
--- a/test/writeObfuscatedMockRac.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as fs from 'fs';
-
-import { encodeBase64, getMD5Hash } from '../src/obfuscation';
-
-import {
-  MOCK_RAC_RESPONSE_FILE,
-  OBFUSCATED_MOCK_RAC_RESPONSE_FILE,
-  TEST_DATA_DIR,
-  readMockRacResponse,
-} from './testHelpers';
-
-export function generateObfuscatedMockRac() {
-  const rac = readMockRacResponse(MOCK_RAC_RESPONSE_FILE);
-  const keys = Object.keys(rac.flags);
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  const flagsCopy: Record<string, any> = {};
-  keys.forEach((key) => {
-    flagsCopy[getMD5Hash(key)] = rac.flags[key];
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    flagsCopy[getMD5Hash(key)].rules?.forEach((rule: any) =>
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      rule.conditions.forEach((condition: any) => {
-        condition['value'] = ['ONE_OF', 'NOT_ONE_OF'].includes(condition['operator'])
-          ? condition['value'].map((value: string) => getMD5Hash(value.toLowerCase()))
-          : encodeBase64(`${condition['value']}`);
-        condition['operator'] = getMD5Hash(condition['operator']);
-        condition['attribute'] = getMD5Hash(condition['attribute']);
-      }),
-    );
-  });
-  return { flags: flagsCopy };
-}
-
-const obfuscatedRacFilePath = TEST_DATA_DIR + OBFUSCATED_MOCK_RAC_RESPONSE_FILE;
-const obfuscatedRac = generateObfuscatedMockRac();
-fs.writeFileSync(obfuscatedRacFilePath, JSON.stringify(obfuscatedRac, null, 2));
diff --git a/test/writeObfuscatedMockUFC.ts b/test/writeObfuscatedMockUFC.ts
new file mode 100644
index 00000000..5ffceb3d
--- /dev/null
+++ b/test/writeObfuscatedMockUFC.ts
@@ -0,0 +1,52 @@
+import * as fs from 'fs';
+
+import { Flag, Rule } from '../src/interfaces';
+import { encodeBase64, getMD5Hash } from '../src/obfuscation';
+
+import {
+  MOCK_UFC_RESPONSE_FILE,
+  OBFUSCATED_MOCK_UFC_RESPONSE_FILE,
+  readMockUFCResponse,
+  TEST_DATA_DIR,
+} from './testHelpers';
+
+function obfuscateRule(rule: Rule) {
+  return {
+    ...rule,
+    conditions: rule.conditions.map((condition) => ({
+      ...condition,
+      attribute: getMD5Hash(condition.attribute),
+      operator: getMD5Hash(condition.operator),
+      value:
+        ['ONE_OF', 'NOT_ONE_OF'].includes(condition.operator) && typeof condition.value === 'object'
+          ? condition.value.map((value) => getMD5Hash(value.toLowerCase()))
+          : encodeBase64(`${condition.value}`),
+    })),
+  };
+}
+
+function obfuscateFlag(flag: Flag) {
+  return {
+    ...flag,
+    key: getMD5Hash(flag.key),
+    allocations: flag.allocations.map((allocation) => ({
+      ...allocation,
+      rules: allocation.rules.map(obfuscateRule),
+    })),
+  };
+}
+
+export function generateObfuscatedMockRac() {
+  const ufc = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
+  const keys = Object.keys(ufc.flags);
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  const flagsCopy: Record<string, any> = {};
+  keys.forEach((key) => {
+    flagsCopy[getMD5Hash(key)] = obfuscateFlag(ufc.flags[key]);
+  });
+  return { flags: flagsCopy };
+}
+
+const obfuscatedRacFilePath = TEST_DATA_DIR + OBFUSCATED_MOCK_UFC_RESPONSE_FILE;
+const obfuscatedRac = generateObfuscatedMockRac();
+fs.writeFileSync(obfuscatedRacFilePath, JSON.stringify(obfuscatedRac, null, 2));

From aa6436634a173fac2a69e1d4452b15286f139437 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 28 Mar 2024 23:23:46 -0700
Subject: [PATCH 10/39] add obfuscation and make all tests pass

---
 src/client/eppo-client.spec.ts                | 691 +++++++-----------
 src/client/eppo-client.ts                     |   9 +-
 src/dto/rule-dto.ts                           |  27 -
 src/eval.spec.ts                              |  61 +-
 src/eval.ts                                   |   2 +-
 ...tor.ts => flag-configuration-requestor.ts} |   2 +-
 src/index.ts                                  |   2 +-
 src/interfaces.ts                             |  68 +-
 src/rule_evaluator.spec.ts                    |  82 ---
 src/rule_evaluator.ts                         | 179 -----
 src/rules.spec.ts                             | 323 ++++++++
 src/rules.ts                                  | 300 ++++++++
 12 files changed, 968 insertions(+), 778 deletions(-)
 delete mode 100644 src/dto/rule-dto.ts
 rename src/{experiment-configuration-requestor.ts => flag-configuration-requestor.ts} (92%)
 delete mode 100644 src/rule_evaluator.spec.ts
 delete mode 100644 src/rule_evaluator.ts
 create mode 100644 src/rules.spec.ts
 create mode 100644 src/rules.ts

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 0ba6b760..3d2075a8 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -10,7 +10,6 @@ import {
   MOCK_UFC_RESPONSE_FILE,
   OBFUSCATED_MOCK_UFC_RESPONSE_FILE,
   SubjectTestCase,
-  ValueTestType,
   readAssignmentTestData,
   readMockUFCResponse,
 } from '../../test/testHelpers';
@@ -18,20 +17,19 @@ import { IAssignmentHooks } from '../assignment-hooks';
 import { IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
 import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
-import { OperatorType } from '../dto/rule-dto';
-import { EppoValue } from '../eppo_value';
 import { Evaluator } from '../eval';
-import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
+import FlagConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
 import { MD5Sharder } from '../sharders';
-import { AttributeType, SubjectAttributes } from '../types';
 
 import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const packageJson = require('../../package.json');
 
+const flagEndpoint = /flag_config\/v1\/config*/;
+
 class TestConfigurationStore implements IConfigurationStore {
   private store: Record<string, string> = {};
 
@@ -55,6 +53,7 @@ function getTestAssignments(
   testCase: IAssignmentTestCase,
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   assignmentFn: any,
+  obfuscated = false,
 ): { subject: SubjectTestCase; assignment: string | boolean | number | null | object }[] {
   const assignments: {
     subject: SubjectTestCase;
@@ -67,7 +66,7 @@ function getTestAssignments(
       subject.subjectAttributes,
       null,
       undefined,
-      false,
+      obfuscated,
     );
     assignments.push({ subject: subject, assignment: assignment });
   }
@@ -86,10 +85,7 @@ export async function init(configurationStore: IConfigurationStore) {
     sdkVersion: packageJson.version,
   });
 
-  const configurationRequestor = new ExperimentConfigurationRequestor(
-    configurationStore,
-    httpClient,
-  );
+  const configurationRequestor = new FlagConfigurationRequestor(configurationStore, httpClient);
   await configurationRequestor.fetchAndStoreConfigurations();
 }
 
@@ -100,9 +96,9 @@ describe('EppoClient E2E test', () => {
 
   beforeAll(async () => {
     mock.setup();
-    mock.get(/flag_config\/v1\/config*/, (_req, res) => {
-      const rac = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
-      return res.status(200).body(JSON.stringify(rac));
+    mock.get(flagEndpoint, (_req, res) => {
+      const ufc = readMockUFCResponse(MOCK_UFC_RESPONSE_FILE);
+      return res.status(200).body(JSON.stringify(ufc));
     });
 
     await init(storage);
@@ -311,6 +307,89 @@ describe('EppoClient E2E test', () => {
     );
   });
 
+  describe('UFC Obfuscated Test Cases', () => {
+    const storage = new TestConfigurationStore();
+    const evaluator = new Evaluator(new MD5Sharder());
+    const globalClient = new EppoClient(evaluator, storage);
+
+    beforeAll(async () => {
+      mock.setup();
+      mock.get(flagEndpoint, (_req, res) => {
+        const ufc = readMockUFCResponse(OBFUSCATED_MOCK_UFC_RESPONSE_FILE);
+        console.log(ufc);
+        return res.status(200).body(JSON.stringify(ufc));
+      });
+      await init(storage);
+    });
+
+    afterAll(() => {
+      mock.teardown();
+    });
+
+    it.each(readAssignmentTestData())(
+      'test variation assignment splits',
+      async ({ flag, variationType, subjects }: IAssignmentTestCase) => {
+        `---- Test Case for ${flag} Experiment ----`;
+
+        const evaluator = new Evaluator(new MD5Sharder());
+        const client = new EppoClient(evaluator, storage);
+
+        let assignments: {
+          subject: SubjectTestCase;
+          assignment: string | boolean | number | null | object;
+        }[] = [];
+        switch (variationType) {
+          case VariationType.BOOLEAN: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getBoolAssignment.bind(client),
+              true,
+            );
+            break;
+          }
+          case VariationType.NUMERIC: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getNumericAssignment.bind(client),
+              true,
+            );
+            break;
+          }
+          case VariationType.INTEGER: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getIntegerAssignment.bind(client),
+              true,
+            );
+            break;
+          }
+          case VariationType.STRING: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getStringAssignment.bind(client),
+              true,
+            );
+            break;
+          }
+          case VariationType.JSON: {
+            assignments = getTestAssignments(
+              { flag, variationType, subjects },
+              client.getJSONAssignment.bind(client),
+              true,
+            );
+            break;
+          }
+          default: {
+            throw new Error(`Unknown variation type: ${variationType}`);
+          }
+        }
+        for (const { subject, assignment } of assignments) {
+          expect(assignment).toEqual(subject.assignment);
+        }
+      },
+    );
+  });
+
   // it('returns null if getStringAssignment was called for the subject before any RAC was loaded', () => {
   //   expect(
   //     globalClient.getStringAssignment(sessionOverrideSubject, sessionOverrideExperiment),
@@ -571,408 +650,190 @@ describe('EppoClient E2E test', () => {
     });
   });
 
-  //   describe('getStringAssignment with hooks', () => {
-  //     let client: EppoClient;
-
-  //     beforeAll(() => {
-  //       storage.setEntries({ [flagKey]: mockExperimentConfig });
-  //       client = new EppoClient(storage);
-  //     });
-
-  //     describe('onPreAssignment', () => {
-  //       it('called with experiment key and subject id', () => {
-  //         const mockHooks = td.object<IAssignmentHooks>();
-  //         client.getStringAssignment('subject-identifer', flagKey, {}, mockHooks);
-  //         expect(td.explain(mockHooks.onPreAssignment).callCount).toEqual(1);
-  //         expect(td.explain(mockHooks.onPreAssignment).calls[0].args[0]).toEqual(flagKey);
-  //         expect(td.explain(mockHooks.onPreAssignment).calls[0].args[1]).toEqual('subject-identifer');
-  //       });
-
-  //       it('overrides returned assignment', async () => {
-  //         const mockLogger = td.object<IAssignmentLogger>();
-  //         client.setLogger(mockLogger);
-  //         td.reset();
-  //         const variation = await client.getStringAssignment(
-  //           'subject-identifer',
-  //           flagKey,
-  //           {},
-  //           {
-  //             // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  //             onPreAssignment(experimentKey: string, subject: string): EppoValue | null {
-  //               return EppoValue.String('my-overridden-variation');
-  //             },
-
-  //             // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  //             onPostAssignment(
-  //               experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-  //               subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-  //               variation: EppoValue | null, // eslint-disable-line @typescript-eslint/no-unused-vars
-  //             ): void {
-  //               // no-op
-  //             },
-  //           },
-  //         );
-
-  //         expect(variation).toEqual('my-overridden-variation');
-  //         expect(td.explain(mockLogger.logAssignment).callCount).toEqual(0);
-  //       });
-
-  //       it('uses regular assignment logic if onPreAssignment returns null', async () => {
-  //         const mockLogger = td.object<IAssignmentLogger>();
-  //         client.setLogger(mockLogger);
-  //         td.reset();
-  //         const variation = await client.getStringAssignment(
-  //           'subject-identifer',
-  //           flagKey,
-  //           {},
-  //           {
-  //             // eslint-disable-next-line @typescript-eslint/no-unused-vars
-  //             onPreAssignment(experimentKey: string, subject: string): EppoValue | null {
-  //               return null;
-  //             },
-
-  //             onPostAssignment(
-  //               experimentKey: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-  //               subject: string, // eslint-disable-line @typescript-eslint/no-unused-vars
-  //               variation: EppoValue | null, // eslint-disable-line @typescript-eslint/no-unused-vars
-  //             ): void {
-  //               // no-op
-  //             },
-  //           },
-  //         );
-
-  //         expect(variation).not.toBeNull();
-  //         expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
-  //       });
-  //     });
-
-  //     describe('onPostAssignment', () => {
-  //       it('called with assigned variation after assignment', async () => {
-  //         const mockHooks = td.object<IAssignmentHooks>();
-  //         const subject = 'subject-identifier';
-  //         const variation = client.getStringAssignment(subject, flagKey, {}, mockHooks);
-  //         expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
-  //         expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
-  //         expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(flagKey);
-  //         expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(subject);
-  //         expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(
-  //           EppoValue.String(variation ?? ''),
-  //         );
-  //       });
-  //     });
-  //   });
-  // });
+  describe('Eppo Client constructed with configuration request parameters', () => {
+    let client: EppoClient;
+    let storage: IConfigurationStore;
+    let requestConfiguration: FlagConfigurationRequestParameters;
+    let mockServerResponseFunc: (res: MockResponse) => MockResponse;
 
-  // describe(' EppoClient getStringAssignment From Obfuscated RAC', () => {
-  //   const storage = new TestConfigurationStore();
-  //   const evaluator = new Evaluator(new MD5Sharder());
-  //   const globalClient = new EppoClient(evaluator, storage);
-
-  //   beforeAll(async () => {
-  //     mock.setup();
-  //     mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
-  //       const rac = readMockUFCResponse(OBFUSCATED_MOCK_RAC_RESPONSE_FILE);
-  //       return res.status(200).body(JSON.stringify(rac));
-  //     });
-  //     await init(storage);
-  //   });
-
-  //   afterAll(() => {
-  //     mock.teardown();
-  //   });
-
-  //   it.each(readAssignmentTestData())(
-  //     'test variation assignment splits',
-  //     async ({
-  //       experiment,
-  //       valueType = ValueTestType.StringType,
-  //       subjects,
-  //       subjectsWithAttributes,
-  //       expectedAssignments,
-  //     }: IAssignmentTestCase) => {
-  //       `---- Test Case for ${experiment} Experiment ----`;
-
-  //       const assignments = getAssignmentsWithSubjectAttributes(
-  //         subjectsWithAttributes
-  //           ? subjectsWithAttributes
-  //           : subjects.map((subject) => ({ subjectKey: subject })),
-  //         experiment,
-  //         valueType,
-  //       );
-
-  //       switch (valueType) {
-  //         case ValueTestType.BoolType: {
-  //           const boolAssignments = assignments.map((a) => a?.boolValue ?? null);
-  //           expect(boolAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //         case ValueTestType.NumericType: {
-  //           const numericAssignments = assignments.map((a) => a?.numericValue ?? null);
-  //           expect(numericAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //         case ValueTestType.StringType: {
-  //           const stringAssignments = assignments.map((a) => a?.stringValue ?? null);
-  //           expect(stringAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //         case ValueTestType.JSONType: {
-  //           const jsonStringAssignments = assignments.map((a) => a?.stringValue ?? null);
-  //           expect(jsonStringAssignments).toEqual(expectedAssignments);
-  //           break;
-  //         }
-  //       }
-  //     },
-  //   );
-
-  //   function getAssignmentsWithSubjectAttributes(
-  //     subjectsWithAttributes: {
-  //       subjectKey: string;
-  //       // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  //       subjectAttributes?: Record<string, any>;
-  //     }[],
-  //     experiment: string,
-  //     valueTestType: ValueTestType = ValueTestType.StringType,
-  //   ): (EppoValue | null)[] {
-  //     return subjectsWithAttributes.map((subject) => {
-  //       switch (valueTestType) {
-  //         case ValueTestType.BoolType: {
-  //           const ba = globalClient.getBoolAssignment(
-  //             subject.subjectKey,
-  //             experiment,
-  //             subject.subjectAttributes,
-  //             undefined,
-  //             true,
-  //           );
-  //           if (ba === null) return null;
-  //           return EppoValue.Bool(ba);
-  //         }
-  //         case ValueTestType.NumericType: {
-  //           const na = globalClient.getNumericAssignment(
-  //             subject.subjectKey,
-  //             experiment,
-  //             subject.subjectAttributes,
-  //             undefined,
-  //             true,
-  //           );
-  //           if (na === null) return null;
-  //           return EppoValue.Numeric(na);
-  //         }
-  //         case ValueTestType.StringType: {
-  //           const sa = globalClient.getStringAssignment(
-  //             subject.subjectKey,
-  //             experiment,
-  //             subject.subjectAttributes,
-  //             undefined,
-  //             true,
-  //           );
-  //           if (sa === null) return null;
-  //           return EppoValue.String(sa);
-  //         }
-  //         case ValueTestType.JSONType: {
-  //           const sa = globalClient.getJSONStringAssignment(
-  //             subject.subjectKey,
-  //             experiment,
-  //             subject.subjectAttributes,
-  //             undefined,
-  //             true,
-  //           );
-  //           const oa = globalClient.getParsedJSONAssignment(
-  //             subject.subjectKey,
-  //             experiment,
-  //             subject.subjectAttributes,
-  //             undefined,
-  //             true,
-  //           );
-  //           if (oa == null || sa === null) return null;
-  //           return EppoValue.JSON(sa, oa);
-  //         }
-  //       }
-  //     });
-  //   }
-  // });
+    const evaluator = new Evaluator(new MD5Sharder());
+    const ufcBody = JSON.stringify(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE));
+    const flagKey = 'numeric_flag';
+    const subject = 'alice';
+    const pi = 3.1415926;
+
+    const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
 
-  // describe('Eppo Client constructed with configuration request parameters', () => {
-  //   let client: EppoClient;
-  //   let storage: IConfigurationStore;
-  //   let requestConfiguration: ExperimentConfigurationRequestParameters;
-  //   let mockServerResponseFunc: (res: MockResponse) => MockResponse;
-
-  //   const racBody = JSON.stringify(readMockRacResponse(MOCK_RAC_RESPONSE_FILE));
-  //   const flagKey = 'randomization_algo';
-  //   const subjectForGreenVariation = 'subject-identiferA';
-
-  //   const maxRetryDelay = POLL_INTERVAL_MS * POLL_JITTER_PCT;
-
-  //   beforeAll(() => {
-  //     mock.setup();
-  //     mock.get(/randomized_assignment\/v3\/config*/, (_req, res) => {
-  //       return mockServerResponseFunc(res);
-  //     });
-  //   });
-
-  //   beforeEach(() => {
-  //     storage = new TestConfigurationStore();
-  //     requestConfiguration = {
-  //       apiKey: 'dummy key',
-  //       sdkName: 'js-client-sdk-common',
-  //       sdkVersion: packageJson.version,
-  //     };
-  //     mockServerResponseFunc = (res) => res.status(200).body(racBody);
-
-  //     // We only want to fake setTimeout() and clearTimeout()
-  //     jest.useFakeTimers({
-  //       advanceTimers: true,
-  //       doNotFake: [
-  //         'Date',
-  //         'hrtime',
-  //         'nextTick',
-  //         'performance',
-  //         'queueMicrotask',
-  //         'requestAnimationFrame',
-  //         'cancelAnimationFrame',
-  //         'requestIdleCallback',
-  //         'cancelIdleCallback',
-  //         'setImmediate',
-  //         'clearImmediate',
-  //         'setInterval',
-  //         'clearInterval',
-  //       ],
-  //     });
-  //   });
-
-  //   afterEach(() => {
-  //     jest.clearAllTimers();
-  //     jest.useRealTimers();
-  //   });
-
-  //   afterAll(() => {
-  //     mock.teardown();
-  //   });
-
-  //   it('Fetches initial configuration with parameters in constructor', async () => {
-  //     client = new EppoClient(storage, requestConfiguration);
-  //     client.setIsGracefulFailureMode(false);
-  //     // no configuration loaded
-  //     let variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
-  //     expect(variation).toBeNull();
-  //     // have client fetch configurations
-  //     await client.fetchFlagConfigurations();
-  //     variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
-  //     expect(variation).toBe('green');
-  //   });
-
-  //   it('Fetches initial configuration with parameters provided later', async () => {
-  //     client = new EppoClient(storage);
-  //     client.setIsGracefulFailureMode(false);
-  //     client.setConfigurationRequestParameters(requestConfiguration);
-  //     // no configuration loaded
-  //     let variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
-  //     expect(variation).toBeNull();
-  //     // have client fetch configurations
-  //     await client.fetchFlagConfigurations();
-  //     variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
-  //     expect(variation).toBe('green');
-  //   });
-
-  //   it.each([
-  //     { pollAfterSuccessfulInitialization: false },
-  //     { pollAfterSuccessfulInitialization: true },
-  //   ])('retries initial configuration request with config %p', async (configModification) => {
-  //     let callCount = 0;
-  //     mockServerResponseFunc = (res) => {
-  //       if (++callCount === 1) {
-  //         // Throw an error for the first call
-  //         return res.status(500);
-  //       } else {
-  //         // Return a mock object for subsequent calls
-  //         return res.status(200).body(racBody);
-  //       }
-  //     };
-
-  //     const { pollAfterSuccessfulInitialization } = configModification;
-  //     requestConfiguration = {
-  //       ...requestConfiguration,
-  //       pollAfterSuccessfulInitialization,
-  //     };
-  //     client = new EppoClient(storage, requestConfiguration);
-  //     client.setIsGracefulFailureMode(false);
-  //     // no configuration loaded
-  //     let variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
-  //     expect(variation).toBeNull();
-
-  //     // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
-  //     const fetchPromise = client.fetchFlagConfigurations();
-
-  //     // Advance timers mid-init to allow retrying
-  //     await jest.advanceTimersByTimeAsync(maxRetryDelay);
-
-  //     // Await so it can finish its initialization before this test proceeds
-  //     await fetchPromise;
-
-  //     variation = client.getStringAssignment(subjectForGreenVariation, flagKey);
-  //     expect(variation).toBe('green');
-  //     expect(callCount).toBe(2);
-
-  //     await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
-  //     // By default, no more polling
-  //     expect(callCount).toBe(pollAfterSuccessfulInitialization ? 3 : 2);
-  //   });
-
-  //   it.each([
-  //     {
-  //       pollAfterFailedInitialization: false,
-  //       throwOnFailedInitialization: false,
-  //     },
-  //     { pollAfterFailedInitialization: false, throwOnFailedInitialization: true },
-  //     { pollAfterFailedInitialization: true, throwOnFailedInitialization: false },
-  //     { pollAfterFailedInitialization: true, throwOnFailedInitialization: true },
-  //   ])('initial configuration request fails with config %p', async (configModification) => {
-  //     let callCount = 0;
-  //     mockServerResponseFunc = (res) => {
-  //       if (++callCount === 1) {
-  //         // Throw an error for initialization call
-  //         return res.status(500);
-  //       } else {
-  //         // Return a mock object for subsequent calls
-  //         return res.status(200).body(racBody);
-  //       }
-  //     };
-
-  //     const { pollAfterFailedInitialization, throwOnFailedInitialization } = configModification;
-
-  //     // Note: fake time does not play well with errors bubbled up after setTimeout (event loop,
-  //     // timeout queue, message queue stuff) so we don't allow retries when rethrowing.
-  //     const numInitialRequestRetries = 0;
-
-  //     requestConfiguration = {
-  //       ...requestConfiguration,
-  //       numInitialRequestRetries,
-  //       throwOnFailedInitialization,
-  //       pollAfterFailedInitialization,
-  //     };
-  //     client = new EppoClient(storage, requestConfiguration);
-  //     client.setIsGracefulFailureMode(false);
-  //     // no configuration loaded
-  //     expect(client.getStringAssignment(subjectForGreenVariation, flagKey)).toBeNull();
-
-  //     // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
-  //     if (throwOnFailedInitialization) {
-  //       await expect(client.fetchFlagConfigurations()).rejects.toThrow();
-  //     } else {
-  //       await expect(client.fetchFlagConfigurations()).resolves.toBeUndefined();
-  //     }
-  //     expect(callCount).toBe(1);
-  //     // still no configuration loaded
-  //     expect(client.getStringAssignment(subjectForGreenVariation, flagKey)).toBeNull();
-
-  //     // Advance timers so a post-init poll can take place
-  //     await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
-
-  //     // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
-  //     expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);
-  //     expect(client.getStringAssignment(subjectForGreenVariation, flagKey)).toBe(
-  //       pollAfterFailedInitialization ? 'green' : null,
-  //     );
-  //   });
+    beforeAll(() => {
+      mock.setup();
+      mock.get(flagEndpoint, (_req, res) => {
+        return mockServerResponseFunc(res);
+      });
+    });
+
+    beforeEach(() => {
+      storage = new TestConfigurationStore();
+      requestConfiguration = {
+        apiKey: 'dummy key',
+        sdkName: 'js-client-sdk-common',
+        sdkVersion: packageJson.version,
+      };
+      mockServerResponseFunc = (res) => res.status(200).body(ufcBody);
+
+      // We only want to fake setTimeout() and clearTimeout()
+      jest.useFakeTimers({
+        advanceTimers: true,
+        doNotFake: [
+          'Date',
+          'hrtime',
+          'nextTick',
+          'performance',
+          'queueMicrotask',
+          'requestAnimationFrame',
+          'cancelAnimationFrame',
+          'requestIdleCallback',
+          'cancelIdleCallback',
+          'setImmediate',
+          'clearImmediate',
+          'setInterval',
+          'clearInterval',
+        ],
+      });
+    });
+
+    afterEach(() => {
+      jest.clearAllTimers();
+      jest.useRealTimers();
+    });
+
+    afterAll(() => {
+      mock.teardown();
+    });
+
+    it('Fetches initial configuration with parameters in constructor', async () => {
+      client = new EppoClient(evaluator, storage, requestConfiguration);
+      client.setIsGracefulFailureMode(false);
+      // no configuration loaded
+      let variation = client.getNumericAssignment(subject, flagKey);
+      expect(variation).toBeNull();
+      // have client fetch configurations
+      await client.fetchFlagConfigurations();
+      variation = client.getNumericAssignment(subject, flagKey);
+      expect(variation).toBe(pi);
+    });
+
+    it('Fetches initial configuration with parameters provided later', async () => {
+      client = new EppoClient(evaluator, storage);
+      client.setIsGracefulFailureMode(false);
+      client.setConfigurationRequestParameters(requestConfiguration);
+      // no configuration loaded
+      let variation = client.getNumericAssignment(subject, flagKey);
+      expect(variation).toBeNull();
+      // have client fetch configurations
+      await client.fetchFlagConfigurations();
+      variation = client.getNumericAssignment(subject, flagKey);
+      expect(variation).toBe(pi);
+    });
+
+    it.each([
+      { pollAfterSuccessfulInitialization: false },
+      { pollAfterSuccessfulInitialization: true },
+    ])('retries initial configuration request with config %p', async (configModification) => {
+      let callCount = 0;
+      mockServerResponseFunc = (res) => {
+        if (++callCount === 1) {
+          // Throw an error for the first call
+          return res.status(500);
+        } else {
+          // Return a mock object for subsequent calls
+          return res.status(200).body(ufcBody);
+        }
+      };
+
+      const { pollAfterSuccessfulInitialization } = configModification;
+      requestConfiguration = {
+        ...requestConfiguration,
+        pollAfterSuccessfulInitialization,
+      };
+      client = new EppoClient(evaluator, storage, requestConfiguration);
+      client.setIsGracefulFailureMode(false);
+      // no configuration loaded
+      let variation = client.getNumericAssignment(subject, flagKey);
+      expect(variation).toBeNull();
+
+      // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
+      const fetchPromise = client.fetchFlagConfigurations();
+
+      // Advance timers mid-init to allow retrying
+      await jest.advanceTimersByTimeAsync(maxRetryDelay);
+
+      // Await so it can finish its initialization before this test proceeds
+      await fetchPromise;
+
+      variation = client.getNumericAssignment(subject, flagKey);
+      expect(variation).toBe(pi);
+      expect(callCount).toBe(2);
+
+      await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS);
+      // By default, no more polling
+      expect(callCount).toBe(pollAfterSuccessfulInitialization ? 3 : 2);
+    });
+
+    it.each([
+      {
+        pollAfterFailedInitialization: false,
+        throwOnFailedInitialization: false,
+      },
+      { pollAfterFailedInitialization: false, throwOnFailedInitialization: true },
+      { pollAfterFailedInitialization: true, throwOnFailedInitialization: false },
+      { pollAfterFailedInitialization: true, throwOnFailedInitialization: true },
+    ])('initial configuration request fails with config %p', async (configModification) => {
+      let callCount = 0;
+      mockServerResponseFunc = (res) => {
+        if (++callCount === 1) {
+          // Throw an error for initialization call
+          return res.status(500);
+        } else {
+          // Return a mock object for subsequent calls
+          return res.status(200).body(ufcBody);
+        }
+      };
+
+      const { pollAfterFailedInitialization, throwOnFailedInitialization } = configModification;
+
+      // Note: fake time does not play well with errors bubbled up after setTimeout (event loop,
+      // timeout queue, message queue stuff) so we don't allow retries when rethrowing.
+      const numInitialRequestRetries = 0;
+
+      requestConfiguration = {
+        ...requestConfiguration,
+        numInitialRequestRetries,
+        throwOnFailedInitialization,
+        pollAfterFailedInitialization,
+      };
+      client = new EppoClient(evaluator, storage, requestConfiguration);
+      client.setIsGracefulFailureMode(false);
+      // no configuration loaded
+      expect(client.getNumericAssignment(subject, flagKey)).toBeNull();
+
+      // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
+      if (throwOnFailedInitialization) {
+        await expect(client.fetchFlagConfigurations()).rejects.toThrow();
+      } else {
+        await expect(client.fetchFlagConfigurations()).resolves.toBeUndefined();
+      }
+      expect(callCount).toBe(1);
+      // still no configuration loaded
+      expect(client.getNumericAssignment(subject, flagKey)).toBeNull();
+
+      // Advance timers so a post-init poll can take place
+      await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
+
+      // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
+      expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);
+      expect(client.getNumericAssignment(subject, flagKey)).toBe(
+        pollAfterFailedInitialization ? pi : null,
+      );
+    });
+  });
 });
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index f2363339..b2e47144 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -19,9 +19,10 @@ import {
 } from '../constants';
 import { EppoValue } from '../eppo_value';
 import { Evaluator, FlagEvaluation, noneResult } from '../eval';
-import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
+import ExperimentConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
+import { getMD5Hash } from '../obfuscation';
 import initPoller, { IPoller } from '../poller';
 import { AttributeType } from '../types';
 import { validateNotBlank } from '../validation';
@@ -343,7 +344,7 @@ export default class EppoClient implements IEppoClient {
     validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
     validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
 
-    const flag: Flag = this.configurationStore.get(flagKey);
+    const flag: Flag = this.configurationStore.get(obfuscated ? getMD5Hash(flagKey) : flagKey);
 
     if (flag === null) {
       console.warn(`[Eppo SDK] No assigned variation. Flag not found: ${flagKey}`);
@@ -364,6 +365,10 @@ export default class EppoClient implements IEppoClient {
     }
 
     const result = this.evaluator.evaluateFlag(flag, subjectKey, subjectAttributes, obfuscated);
+    if (obfuscated) {
+      // flag.key is obfuscated, replace with requested flag key
+      result.flagKey = flagKey;
+    }
 
     try {
       if (result && result.doLog) {
diff --git a/src/dto/rule-dto.ts b/src/dto/rule-dto.ts
deleted file mode 100644
index c72f0cef..00000000
--- a/src/dto/rule-dto.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export enum OperatorType {
-  MATCHES = 'MATCHES',
-  GTE = 'GTE',
-  GT = 'GT',
-  LTE = 'LTE',
-  LT = 'LT',
-  ONE_OF = 'ONE_OF',
-  NOT_ONE_OF = 'NOT_ONE_OF',
-}
-
-export enum OperatorValueType {
-  PLAIN_STRING = 'PLAIN_STRING',
-  STRING_ARRAY = 'STRING_ARRAY',
-  SEM_VER = 'SEM_VER',
-  NUMERIC = 'NUMERIC',
-}
-
-export interface Condition {
-  operator: OperatorType;
-  attribute: string;
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  value: any;
-}
-
-export interface IRule {
-  conditions: Condition[];
-}
diff --git a/src/eval.spec.ts b/src/eval.spec.ts
index bcf091fd..a212ced3 100644
--- a/src/eval.spec.ts
+++ b/src/eval.spec.ts
@@ -1,5 +1,7 @@
 import { Evaluator, hashKey, isInShardRange } from './eval';
-import { Flag, Variation, Shard, VariationType, OperatorType } from './interfaces';
+import { Flag, Variation, Shard, VariationType } from './interfaces';
+import { encodeBase64, getMD5Hash } from './obfuscation';
+import { ObfuscatedOperatorType, OperatorType } from './rules';
 import { MD5Sharder, DeterministicSharder } from './sharders';
 
 describe('Evaluator', () => {
@@ -203,7 +205,7 @@ describe('Evaluator', () => {
           rules: [
             {
               conditions: [
-                { operator: OperatorType.MATCHES, attribute: 'email', value: '.*@example.com' },
+                { operator: OperatorType.MATCHES, attribute: 'email', value: '.*@example\\.com$' },
               ],
             },
           ],
@@ -256,7 +258,7 @@ describe('Evaluator', () => {
           rules: [
             {
               conditions: [
-                { operator: OperatorType.MATCHES, attribute: 'email', value: '.*@example.com' },
+                { operator: OperatorType.MATCHES, attribute: 'email', value: '.*@example\\.com$' },
               ],
             },
           ],
@@ -291,6 +293,59 @@ describe('Evaluator', () => {
     expect(result.allocationKey).toEqual('default');
     expect(result.variation).toEqual(VARIATION_A);
   });
+
+  it('should not match first allocation rule and return variation A (obfuscated)', () => {
+    const flag: Flag = {
+      key: 'obfuscated_flag_key',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { a: VARIATION_A, b: VARIATION_B },
+      allocations: [
+        {
+          key: 'first',
+          rules: [
+            {
+              conditions: [
+                {
+                  operator: ObfuscatedOperatorType.MATCHES,
+                  attribute: getMD5Hash('email'),
+                  value: encodeBase64('.*@example\\.com$'),
+                },
+              ],
+            },
+          ],
+          splits: [
+            {
+              variationKey: 'b',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+        {
+          key: 'default',
+          rules: [],
+          splits: [
+            {
+              variationKey: 'a',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10,
+    };
+
+    const evaluator = new Evaluator(new MD5Sharder());
+    const result = evaluator.evaluateFlag(flag, 'subject_key', { email: 'eppo@test.com' }, false);
+    expect(result.flagKey).toEqual('obfuscated_flag_key');
+    expect(result.allocationKey).toEqual('default');
+    expect(result.variation).toEqual(VARIATION_A);
+  });
+
   it('should evaluate sharding and return correct variations', () => {
     const flag: Flag = {
       key: 'flag',
diff --git a/src/eval.ts b/src/eval.ts
index 34ad548b..416b6907 100644
--- a/src/eval.ts
+++ b/src/eval.ts
@@ -1,5 +1,5 @@
 import { Flag, Shard, Range, Variation, Allocation, Split } from './interfaces';
-import { matchesRule } from './rule_evaluator';
+import { matchesRule } from './rules';
 import { Sharder } from './sharders';
 
 export interface FlagEvaluation {
diff --git a/src/experiment-configuration-requestor.ts b/src/flag-configuration-requestor.ts
similarity index 92%
rename from src/experiment-configuration-requestor.ts
rename to src/flag-configuration-requestor.ts
index 4773c14b..561cb899 100644
--- a/src/experiment-configuration-requestor.ts
+++ b/src/flag-configuration-requestor.ts
@@ -8,7 +8,7 @@ interface IUniversalFlagConfig {
   flags: Record<string, Flag>;
 }
 
-export default class ExperimentConfigurationRequestor {
+export default class FlagConfigurationRequestor {
   constructor(private configurationStore: IConfigurationStore, private httpClient: HttpClient) {}
 
   async fetchAndStoreConfigurations(): Promise<Record<string, Flag>> {
diff --git a/src/index.ts b/src/index.ts
index 407d795f..f2c21f1c 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,7 +4,7 @@ import { IAssignmentLogger, IAssignmentEvent } from './assignment-logger';
 import EppoClient, { FlagConfigurationRequestParameters, IEppoClient } from './client/eppo-client';
 import { IConfigurationStore } from './configuration-store';
 import * as constants from './constants';
-import ExperimentConfigurationRequestor from './experiment-configuration-requestor';
+import ExperimentConfigurationRequestor from './flag-configuration-requestor';
 import HttpClient from './http-client';
 import * as validation from './validation';
 
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 27fae25a..765adaa8 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -1,4 +1,4 @@
-import { getMD5Hash } from './obfuscation';
+import { Rule } from './rules';
 
 export enum VariationType {
   STRING = 'string',
@@ -46,69 +46,3 @@ export interface Flag {
   allocations: Allocation[];
   totalShards: number;
 }
-
-export enum OperatorType {
-  MATCHES = 'MATCHES',
-  NOT_MATCHES = 'NOT_MATCHES',
-  GTE = 'GTE',
-  GT = 'GT',
-  LTE = 'LTE',
-  LT = 'LT',
-  ONE_OF = 'ONE_OF',
-  NOT_ONE_OF = 'NOT_ONE_OF',
-}
-
-export enum OperatorValueType {
-  PLAIN_STRING = 'PLAIN_STRING',
-  STRING_ARRAY = 'STRING_ARRAY',
-  SEM_VER = 'SEM_VER',
-  NUMERIC = 'NUMERIC',
-}
-
-interface MatchesCondition {
-  operator: OperatorType.MATCHES;
-  attribute: string;
-  value: string;
-}
-
-interface NotMatchesCondition {
-  operator: OperatorType.NOT_MATCHES;
-  attribute: string;
-  value: string;
-}
-
-interface OneOfCondition {
-  operator: OperatorType.ONE_OF;
-  attribute: string;
-  value: string[];
-}
-
-interface NotOneOfCondition {
-  operator: OperatorType.NOT_ONE_OF;
-  attribute: string;
-  value: string[];
-}
-
-interface SemVerCondition {
-  operator: OperatorType.GTE | OperatorType.GT | OperatorType.LTE | OperatorType.LT;
-  attribute: string;
-  value: string;
-}
-
-interface NumericCondition {
-  operator: OperatorType.GTE | OperatorType.GT | OperatorType.LTE | OperatorType.LT;
-  attribute: string;
-  value: number;
-}
-
-export type Condition =
-  | MatchesCondition
-  | NotMatchesCondition
-  | OneOfCondition
-  | NotOneOfCondition
-  | SemVerCondition
-  | NumericCondition;
-
-export interface Rule {
-  conditions: Condition[];
-}
diff --git a/src/rule_evaluator.spec.ts b/src/rule_evaluator.spec.ts
deleted file mode 100644
index a4692936..00000000
--- a/src/rule_evaluator.spec.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import { OperatorType, IRule } from './dto/rule-dto';
-import { matchesRule } from './rule_evaluator';
-
-describe('matchesRule', () => {
-  const ruleWithEmptyConditions: IRule = {
-    conditions: [],
-  };
-  const numericRule: IRule = {
-    conditions: [
-      {
-        operator: OperatorType.GTE,
-        attribute: 'totalSales',
-        value: 10,
-      },
-      {
-        operator: OperatorType.LTE,
-        attribute: 'totalSales',
-        value: 100,
-      },
-    ],
-  };
-  const semverRule: IRule = {
-    conditions: [
-      {
-        operator: OperatorType.GTE,
-        attribute: 'version',
-        value: '1.0.0',
-      },
-      {
-        operator: OperatorType.LTE,
-        attribute: 'version',
-        value: '2.0.0',
-      },
-    ],
-  };
-  const ruleWithMatchesCondition: IRule = {
-    conditions: [
-      {
-        operator: OperatorType.MATCHES,
-        attribute: 'user_id',
-        value: '[0-9]+',
-      },
-    ],
-  };
-
-  const subjectAttributes = {
-    totalSales: 50,
-    version: '1.5.0',
-    user_id: '12345',
-  };
-
-  it('should return true for a rule with empty conditions', () => {
-    expect(matchesRule(ruleWithEmptyConditions, subjectAttributes, false)).toBe(true);
-  });
-
-  it('should return true for a numeric rule that matches the subject attributes', () => {
-    expect(matchesRule(numericRule, subjectAttributes, false)).toBe(true);
-  });
-
-  it('should return false for a numeric rule that does not match the subject attributes', () => {
-    const failingAttributes = { totalSales: 101 };
-    expect(matchesRule(numericRule, failingAttributes, false)).toBe(false);
-  });
-
-  it('should return true for a semver rule that matches the subject attributes', () => {
-    expect(matchesRule(semverRule, subjectAttributes, false)).toBe(true);
-  });
-
-  it('should return false for a semver rule that does not match the subject attributes', () => {
-    const failingAttributes = { version: '2.1.0' };
-    expect(matchesRule(semverRule, failingAttributes, false)).toBe(false);
-  });
-
-  it('should return true for a rule with matches condition that matches the subject attributes', () => {
-    expect(matchesRule(ruleWithMatchesCondition, subjectAttributes, false)).toBe(true);
-  });
-
-  it('should return false for a rule with matches condition that does not match the subject attributes', () => {
-    const failingAttributes = { user_id: 'abcde' };
-    expect(matchesRule(ruleWithMatchesCondition, failingAttributes, false)).toBe(false);
-  });
-});
diff --git a/src/rule_evaluator.ts b/src/rule_evaluator.ts
deleted file mode 100644
index 40d54fdf..00000000
--- a/src/rule_evaluator.ts
+++ /dev/null
@@ -1,179 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import {
-  valid as validSemver,
-  gt as semverGt,
-  lt as semverLt,
-  gte as semverGte,
-  lte as semverLte,
-} from 'semver';
-
-import { Condition, OperatorType, Rule, OperatorValueType } from './interfaces';
-import { decodeBase64, getMD5Hash } from './obfuscation';
-import { ConditionValueType } from './types';
-
-export function matchesRule(
-  rule: Rule,
-  subjectAttributes: Record<string, any>,
-  obfuscated: boolean,
-): boolean {
-  const conditionEvaluations = evaluateRuleConditions(
-    subjectAttributes,
-    rule.conditions,
-    obfuscated,
-  );
-  return !conditionEvaluations.includes(false);
-}
-
-function evaluateRuleConditions(
-  subjectAttributes: Record<string, any>,
-  conditions: Condition[],
-  obfuscated: boolean,
-): boolean[] {
-  // TODO: obfuscated version
-  return conditions.map((condition) => evaluateCondition(subjectAttributes, condition));
-}
-
-function evaluateCondition(subjectAttributes: Record<string, any>, condition: Condition): boolean {
-  const value = subjectAttributes[condition.attribute];
-
-  const conditionValueType = targetingRuleConditionValuesTypesFromValues(condition.value);
-
-  if (value != null) {
-    switch (condition.operator) {
-      case OperatorType.GTE:
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, condition.value, semverGte);
-        }
-        return compareNumber(value, condition.value, (a, b) => a >= b);
-      case OperatorType.GT:
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, condition.value, semverGt);
-        }
-        return compareNumber(value, condition.value, (a, b) => a > b);
-      case OperatorType.LTE:
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, condition.value, semverLte);
-        }
-        return compareNumber(value, condition.value, (a, b) => a <= b);
-      case OperatorType.LT:
-        if (conditionValueType === OperatorValueType.SEM_VER) {
-          return compareSemVer(value, condition.value, semverLt);
-        }
-        return compareNumber(value, condition.value, (a, b) => a < b);
-      case OperatorType.MATCHES:
-        return new RegExp(condition.value as string).test(value as string);
-      case OperatorType.ONE_OF:
-        return isOneOf(
-          value.toString().toLowerCase(),
-          condition.value.map((value: string) => value.toLowerCase()),
-        );
-      case OperatorType.NOT_ONE_OF:
-        return isNotOneOf(
-          value.toString().toLowerCase(),
-          condition.value.map((value: string) => value.toLowerCase()),
-        );
-    }
-  }
-  return false;
-}
-
-// TODO: implement the obfuscated version of this function
-// function evaluateObfuscatedCondition(
-//   subjectAttributes: Record<string, any>,
-//   condition: Condition,
-// ): boolean {
-//   const hashedSubjectAttributes: Record<string, any> = Object.entries(subjectAttributes).reduce(
-//     (accum, [key, val]) => ({ [getMD5Hash(key)]: val, ...accum }),
-//     {},
-//   );
-//   const value = hashedSubjectAttributes[condition.attribute];
-//   const conditionValueType = targetingRuleConditionValuesTypesFromValues(value);
-
-//   if (value != null) {
-//     switch (condition.operator) {
-//       case getMD5Hash(OperatorType.GTE):
-//         if (conditionValueType === OperatorValueType.SEM_VER) {
-//           return compareSemVer(value, decodeBase64(condition.value), semverGte);
-//         }
-//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a >= b);
-//       case getMD5Hash(OperatorType.GT):
-//         if (conditionValueType === OperatorValueType.SEM_VER) {
-//           return compareSemVer(value, decodeBase64(condition.value), semverGt);
-//         }
-//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a > b);
-//       case getMD5Hash(OperatorType.LTE):
-//         if (conditionValueType === OperatorValueType.SEM_VER) {
-//           return compareSemVer(value, decodeBase64(condition.value), semverLte);
-//         }
-//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a <= b);
-//       case getMD5Hash(OperatorType.LT):
-//         if (conditionValueType === OperatorValueType.SEM_VER) {
-//           return compareSemVer(value, decodeBase64(condition.value), semverLt);
-//         }
-//         return compareNumber(value, Number(decodeBase64(condition.value)), (a, b) => a < b);
-//       case getMD5Hash(OperatorType.MATCHES):
-//         return new RegExp(decodeBase64(condition.value)).test(value as string);
-//       case getMD5Hash(OperatorType.ONE_OF):
-//         return isOneOf(getMD5Hash(value.toString().toLowerCase()), condition.value);
-//       case getMD5Hash(OperatorType.NOT_ONE_OF):
-//         return isNotOneOf(getMD5Hash(value.toString().toLowerCase()), condition.value);
-//     }
-//   }
-//   return false;
-// }
-
-function isOneOf(attributeValue: string, conditionValue: string[]) {
-  return getMatchingStringValues(attributeValue, conditionValue).length > 0;
-}
-
-function isNotOneOf(attributeValue: string, conditionValue: string[]) {
-  return getMatchingStringValues(attributeValue, conditionValue).length === 0;
-}
-
-function getMatchingStringValues(attributeValue: string, conditionValues: string[]): string[] {
-  return conditionValues.filter((value) => value === attributeValue);
-}
-
-function compareNumber(
-  attributeValue: any,
-  conditionValue: any,
-  compareFn: (a: number, b: number) => boolean,
-): boolean {
-  return compareFn(Number(attributeValue), Number(conditionValue));
-}
-
-function compareSemVer(
-  attributeValue: any,
-  conditionValue: any,
-  compareFn: (a: string, b: string) => boolean,
-): boolean {
-  return (
-    !!validSemver(attributeValue) &&
-    !!validSemver(conditionValue) &&
-    compareFn(attributeValue, conditionValue)
-  );
-}
-
-function targetingRuleConditionValuesTypesFromValues(value: ConditionValueType): OperatorValueType {
-  // Check if input is a number
-  if (typeof value === 'number') {
-    return OperatorValueType.NUMERIC;
-  }
-
-  if (Array.isArray(value)) {
-    return OperatorValueType.STRING_ARRAY;
-  }
-
-  // Check if input is a string that represents a SemVer
-  if (typeof value === 'string' && validSemver(value)) {
-    return OperatorValueType.SEM_VER;
-  }
-
-  // Check if input is a string that represents a number
-  if (!isNaN(Number(value))) {
-    return OperatorValueType.NUMERIC;
-  }
-
-  // If none of the above, it's a general string
-  return OperatorValueType.PLAIN_STRING;
-}
diff --git a/src/rules.spec.ts b/src/rules.spec.ts
new file mode 100644
index 00000000..ae38aa92
--- /dev/null
+++ b/src/rules.spec.ts
@@ -0,0 +1,323 @@
+import { encodeBase64, getMD5Hash } from './obfuscation';
+import { ObfuscatedOperatorType, OperatorType, Rule, matchesRule } from './rules';
+
+describe('rules', () => {
+  describe('Operators', () => {
+    it('ObfuscatedOperatorTypes should match hashed OperatorTypes', () => {
+      expect(ObfuscatedOperatorType.GTE).toBe(getMD5Hash(OperatorType.GTE));
+      expect(ObfuscatedOperatorType.LTE).toBe(getMD5Hash(OperatorType.LTE));
+      expect(ObfuscatedOperatorType.LT).toBe(getMD5Hash(OperatorType.LT));
+      expect(ObfuscatedOperatorType.ONE_OF).toBe(getMD5Hash(OperatorType.ONE_OF));
+      expect(ObfuscatedOperatorType.NOT_ONE_OF).toBe(getMD5Hash(OperatorType.NOT_ONE_OF));
+      expect(ObfuscatedOperatorType.MATCHES).toBe(getMD5Hash(OperatorType.MATCHES));
+      expect(ObfuscatedOperatorType.NOT_MATCHES).toBe(getMD5Hash(OperatorType.NOT_MATCHES));
+    });
+  });
+
+  describe('matchesRule | standard rules', () => {
+    const ruleWithEmptyConditions: Rule = {
+      conditions: [],
+    };
+    const numericRule: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.GTE,
+          attribute: 'totalSales',
+          value: 10,
+        },
+        {
+          operator: OperatorType.LTE,
+          attribute: 'totalSales',
+          value: 100,
+        },
+      ],
+    };
+    const ruleWithOneOfCondition: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.ONE_OF,
+          attribute: 'country',
+          value: ['USA', 'Canada'],
+        },
+      ],
+    };
+
+    const ruleWithNotOneOfCondition: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.NOT_ONE_OF,
+          attribute: 'country',
+          value: ['USA', 'Canada'],
+        },
+      ],
+    };
+
+    const semverRule: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.GTE,
+          attribute: 'version',
+          value: '1.0.0',
+        },
+        {
+          operator: OperatorType.LTE,
+          attribute: 'version',
+          value: '2.0.0',
+        },
+      ],
+    };
+    const ruleWithMatchesCondition: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.MATCHES,
+          attribute: 'user_id',
+          value: '[0-9]+',
+        },
+      ],
+    };
+    const ruleWithNotMatchesCondition: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.NOT_MATCHES,
+          attribute: 'user_id',
+          value: '[0-9]+',
+        },
+      ],
+    };
+    const subjectAttributes = {
+      totalSales: 50,
+      version: '1.5.0',
+      user_id: '12345',
+      country: 'USA',
+    };
+
+    it('should return true for a rule with empty conditions', () => {
+      expect(matchesRule(ruleWithEmptyConditions, subjectAttributes, false)).toBe(true);
+    });
+
+    it('should return true for a numeric rule that matches the subject attributes', () => {
+      expect(matchesRule(numericRule, subjectAttributes, false)).toBe(true);
+    });
+
+    it('should return false for a numeric rule that does not match the subject attributes', () => {
+      const failingAttributes = { totalSales: 101 };
+      expect(matchesRule(numericRule, failingAttributes, false)).toBe(false);
+    });
+
+    it('should return true for a rule with ONE_OF condition that matches the subject attributes', () => {
+      expect(matchesRule(ruleWithOneOfCondition, { country: 'USA' }, false)).toBe(true);
+    });
+
+    it('should return false for a rule with ONE_OF condition that does not match the subject attributes', () => {
+      expect(matchesRule(ruleWithOneOfCondition, { country: 'UK' }, false)).toBe(false);
+    });
+
+    it('should return false for a rule with ONE_OF condition when subject attribute is missing', () => {
+      expect(matchesRule(ruleWithOneOfCondition, { age: 10 }, false)).toBe(false);
+    });
+
+    it('should return false for a rule with NOT_ONE_OF condition that matches the subject attributes', () => {
+      expect(matchesRule(ruleWithNotOneOfCondition, { country: 'USA' }, false)).toBe(false);
+    });
+
+    it('should return true for a rule with NOT_ONE_OF condition that does not match the subject attributes', () => {
+      expect(matchesRule(ruleWithNotOneOfCondition, { country: 'UK' }, false)).toBe(true);
+    });
+
+    it('should return false for a rule with NOT_ONE_OF condition when subject attribute is missing', () => {
+      expect(matchesRule(ruleWithNotOneOfCondition, { age: 10 }, false)).toBe(false);
+    });
+    ``;
+    it('should return true for a semver rule that matches the subject attributes', () => {
+      expect(matchesRule(semverRule, subjectAttributes, false)).toBe(true);
+    });
+
+    it('should return false for a semver rule that does not match the subject attributes', () => {
+      const failingAttributes = { version: '2.1.0' };
+      expect(matchesRule(semverRule, failingAttributes, false)).toBe(false);
+    });
+
+    it('should return true for a rule with matches condition that matches the subject attributes', () => {
+      expect(matchesRule(ruleWithMatchesCondition, subjectAttributes, false)).toBe(true);
+    });
+
+    it('should return false for a rule with matches condition that does not match the subject attributes', () => {
+      const failingAttributes = { user_id: 'abcde' };
+      expect(matchesRule(ruleWithMatchesCondition, failingAttributes, false)).toBe(false);
+    });
+
+    it('should return true for a rule with not_matches condition that matches the subject attributes', () => {
+      expect(matchesRule(ruleWithNotMatchesCondition, subjectAttributes, false)).toBe(false);
+    });
+  });
+
+  describe('matchesRule | obfuscated rules', () => {
+    describe('matchesRule with obfuscated conditions', () => {
+      const obfuscatedRuleWithOneOfCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.ONE_OF,
+            attribute: getMD5Hash('country'),
+            value: ['usa', 'canada', 'mexico'].map(getMD5Hash),
+          },
+        ],
+      };
+
+      const obfuscatedRuleWithNotOneOfCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.NOT_ONE_OF,
+            attribute: getMD5Hash('country'),
+            value: ['usa', 'canada', 'mexico'].map(getMD5Hash),
+          },
+        ],
+      };
+
+      const obfuscatedRuleWithGTECondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.GTE,
+            attribute: getMD5Hash('age'),
+            value: encodeBase64('18'),
+          },
+        ],
+      };
+
+      const obfuscatedRuleWithGTCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.GT,
+            attribute: getMD5Hash('age'),
+            value: encodeBase64('18'),
+          },
+        ],
+      };
+      const obfuscatedRuleWithLTECondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.LTE,
+            attribute: getMD5Hash('age'),
+            value: encodeBase64('18'),
+          },
+        ],
+      };
+
+      const obfuscatedRuleWithLTCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.LT,
+            attribute: getMD5Hash('age'),
+            value: encodeBase64('18'),
+          },
+        ],
+      };
+      const obfuscatedRuleWithMatchesCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.MATCHES,
+            attribute: getMD5Hash('email'),
+            value: encodeBase64('.+@example\\.com$'),
+          },
+        ],
+      };
+
+      const obfuscatedRuleWithNotMatchesCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.NOT_MATCHES,
+            attribute: getMD5Hash('email'),
+            value: encodeBase64('.+@example\\.com$'),
+          },
+        ],
+      };
+
+      it('should return true for an obfuscated rule with ONE_OF condition that matches the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithOneOfCondition, { country: 'USA' }, true)).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with ONE_OF condition that does not match the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithOneOfCondition, { country: 'UK' }, true)).toBe(false);
+      });
+
+      it('should return true for an obfuscated rule with NOT_ONE_OF condition that does not match the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithNotOneOfCondition, { country: 'UK' }, true)).toBe(
+          true,
+        );
+      });
+
+      it('should return false for an obfuscated rule with NOT_ONE_OF condition that matches the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithNotOneOfCondition, { country: 'USA' }, true)).toBe(
+          false,
+        );
+      });
+
+      it('should return true for an obfuscated rule with GTE condition that matches the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithGTECondition, { age: 18 }, true)).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with GTE condition that does not match the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithGTECondition, { age: 17 }, true)).toBe(false);
+      });
+
+      it('should return true for an obfuscated rule with GT condition that matches the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithGTCondition, { age: 19 }, true)).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with GT condition that does not match the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithGTCondition, { age: 18 }, true)).toBe(false);
+      });
+
+      it('should return true for an obfuscated rule with LTE condition that matches the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithLTECondition, { age: 18 }, true)).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with LTE condition that does not match the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithLTECondition, { age: 19 }, true)).toBe(false);
+      });
+
+      it('should return true for an obfuscated rule with LT condition that matches the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithLTCondition, { age: 17 }, true)).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with LT condition that does not match the subject attributes', () => {
+        expect(matchesRule(obfuscatedRuleWithLTCondition, { age: 18 }, true)).toBe(false);
+      });
+
+      it('should return true for an obfuscated rule with MATCHES condition that matches the subject attributes', () => {
+        expect(
+          matchesRule(obfuscatedRuleWithMatchesCondition, { email: 'user@example.com' }, true),
+        ).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with MATCHES condition that does not match the subject attributes', () => {
+        expect(
+          matchesRule(
+            obfuscatedRuleWithMatchesCondition,
+            { email: 'user@anotherdomain.com' },
+            true,
+          ),
+        ).toBe(false);
+      });
+
+      it('should return true for an obfuscated rule with NOT_MATCHES condition that does not match the subject attributes', () => {
+        expect(
+          matchesRule(
+            obfuscatedRuleWithNotMatchesCondition,
+            { email: 'user@anotherdomain.com' },
+            true,
+          ),
+        ).toBe(true);
+      });
+
+      it('should return false for an obfuscated rule with NOT_MATCHES condition that matches the subject attributes', () => {
+        expect(
+          matchesRule(obfuscatedRuleWithNotMatchesCondition, { email: 'user@example.com' }, true),
+        ).toBe(false);
+      });
+
+      it('should return false for an obfuscated rule with NOT_MATCHES condition when subject attribute is missing', () => {
+        expect(matchesRule(obfuscatedRuleWithNotMatchesCondition, { age: 30 }, true)).toBe(false);
+      });
+    });
+  });
+});
diff --git a/src/rules.ts b/src/rules.ts
new file mode 100644
index 00000000..1d52807a
--- /dev/null
+++ b/src/rules.ts
@@ -0,0 +1,300 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import {
+  valid as validSemver,
+  gt as semverGt,
+  lt as semverLt,
+  gte as semverGte,
+  lte as semverLte,
+} from 'semver';
+
+import { decodeBase64, getMD5Hash } from './obfuscation';
+import { ConditionValueType } from './types';
+
+export enum OperatorType {
+  MATCHES = 'MATCHES',
+  NOT_MATCHES = 'NOT_MATCHES',
+  GTE = 'GTE',
+  GT = 'GT',
+  LTE = 'LTE',
+  LT = 'LT',
+  ONE_OF = 'ONE_OF',
+  NOT_ONE_OF = 'NOT_ONE_OF',
+}
+
+export enum ObfuscatedOperatorType {
+  MATCHES = '05015086bdd8402218f6aad6528bef08',
+  NOT_MATCHES = '8323761667755378c3a78e0a6ed37a78',
+  GTE = '32d35312e8f24bc1669bd2b45c00d47c',
+  GT = 'cd6a9bd2a175104eed40f0d33a8b4020',
+  LTE = 'cc981ecc65ecf63ad1673cbec9c64198',
+  LT = 'c562607189d77eb9dfb707464c1e7b0b',
+  ONE_OF = '27457ce369f2a74203396a35ef537c0b',
+  NOT_ONE_OF = '602f5ee0b6e84fe29f43ab48b9e1addf',
+}
+
+export enum OperatorValueType {
+  PLAIN_STRING = 'PLAIN_STRING',
+  STRING_ARRAY = 'STRING_ARRAY',
+  SEM_VER = 'SEM_VER',
+  NUMERIC = 'NUMERIC',
+}
+
+type NumericOperator = OperatorType.GTE | OperatorType.GT | OperatorType.LTE | OperatorType.LT;
+
+type ObfuscatedNumericOperator =
+  | ObfuscatedOperatorType.GTE
+  | ObfuscatedOperatorType.GT
+  | ObfuscatedOperatorType.LTE
+  | ObfuscatedOperatorType.LT;
+
+type MatchesCondition = {
+  operator: OperatorType.MATCHES | ObfuscatedOperatorType.MATCHES;
+  attribute: string;
+  value: string;
+};
+
+type NotMatchesCondition = {
+  operator: OperatorType.NOT_MATCHES | ObfuscatedOperatorType.NOT_MATCHES;
+  attribute: string;
+  value: string;
+};
+
+type OneOfCondition = {
+  operator: OperatorType.ONE_OF | ObfuscatedOperatorType.ONE_OF;
+  attribute: string;
+  value: string[];
+};
+
+type NotOneOfCondition = {
+  operator: OperatorType.NOT_ONE_OF | ObfuscatedOperatorType.NOT_ONE_OF;
+  attribute: string;
+  value: string[];
+};
+
+type SemVerCondition = {
+  operator: NumericOperator;
+  attribute: string;
+  value: string;
+};
+
+type StandardNumericCondition = {
+  operator: NumericOperator;
+  attribute: string;
+  value: number;
+};
+
+type ObfuscatedNumericCondition = {
+  operator: ObfuscatedNumericOperator;
+  attribute: string;
+  value: string;
+};
+
+type NumericCondition = StandardNumericCondition | ObfuscatedNumericCondition;
+
+export type Condition =
+  | MatchesCondition
+  | NotMatchesCondition
+  | OneOfCondition
+  | NotOneOfCondition
+  | SemVerCondition
+  | NumericCondition;
+
+export interface Rule {
+  conditions: Condition[];
+}
+
+export function matchesRule(
+  rule: Rule,
+  subjectAttributes: Record<string, any>,
+  obfuscated: boolean,
+): boolean {
+  const conditionEvaluations = evaluateRuleConditions(
+    subjectAttributes,
+    rule.conditions,
+    obfuscated,
+  );
+  return !conditionEvaluations.includes(false);
+}
+
+function evaluateRuleConditions(
+  subjectAttributes: Record<string, any>,
+  conditions: Condition[],
+  obfuscated: boolean,
+): boolean[] {
+  return conditions.map((condition) =>
+    obfuscated
+      ? evaluateObfuscatedCondition(
+          Object.entries(subjectAttributes).reduce(
+            (accum, [key, val]) => ({ [getMD5Hash(key)]: val, ...accum }),
+            {},
+          ),
+          condition,
+        )
+      : evaluateCondition(subjectAttributes, condition),
+  );
+}
+
+function evaluateCondition(subjectAttributes: Record<string, any>, condition: Condition): boolean {
+  const value = subjectAttributes[condition.attribute];
+
+  const conditionValueType = targetingRuleConditionValuesTypesFromValues(condition.value);
+
+  if (value != null) {
+    switch (condition.operator) {
+      case OperatorType.GTE:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, condition.value, semverGte);
+        }
+        return compareNumber(value, condition.value, (a, b) => a >= b);
+      case OperatorType.GT:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, condition.value, semverGt);
+        }
+        return compareNumber(value, condition.value, (a, b) => a > b);
+      case OperatorType.LTE:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, condition.value, semverLte);
+        }
+        return compareNumber(value, condition.value, (a, b) => a <= b);
+      case OperatorType.LT:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, condition.value, semverLt);
+        }
+        return compareNumber(value, condition.value, (a, b) => a < b);
+      case OperatorType.MATCHES:
+        return new RegExp(condition.value as string).test(value as string);
+      case OperatorType.ONE_OF:
+        return isOneOf(
+          value.toString().toLowerCase(),
+          condition.value.map((value: string) => value.toLowerCase()),
+        );
+      case OperatorType.NOT_ONE_OF:
+        return isNotOneOf(
+          value.toString().toLowerCase(),
+          condition.value.map((value: string) => value.toLowerCase()),
+        );
+    }
+  }
+  return false;
+}
+
+// TODO: implement the obfuscated version of this function
+function evaluateObfuscatedCondition(
+  hashedSubjectAttributes: Record<string, any>,
+  condition: Condition,
+): boolean {
+  const value = hashedSubjectAttributes[condition.attribute];
+  const conditionValueType = targetingRuleConditionValuesTypesFromValues(value);
+
+  if (value != null) {
+    switch (condition.operator) {
+      case ObfuscatedOperatorType.GTE:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, decodeBase64(condition.value as string), semverGte);
+        }
+        return compareNumber(
+          value,
+          Number(decodeBase64(condition.value as string)),
+          (a, b) => a >= b,
+        );
+      case ObfuscatedOperatorType.GT:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, decodeBase64(condition.value as string), semverGt);
+        }
+        return compareNumber(
+          value,
+          Number(decodeBase64(condition.value as string)),
+          (a, b) => a > b,
+        );
+      case ObfuscatedOperatorType.LTE:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, decodeBase64(condition.value as string), semverLte);
+        }
+        return compareNumber(
+          value,
+          Number(decodeBase64(condition.value as string)),
+          (a, b) => a <= b,
+        );
+      case ObfuscatedOperatorType.LT:
+        if (conditionValueType === OperatorValueType.SEM_VER) {
+          return compareSemVer(value, decodeBase64(condition.value as string), semverLt);
+        }
+        return compareNumber(
+          value,
+          Number(decodeBase64(condition.value as string)),
+          (a, b) => a < b,
+        );
+      case ObfuscatedOperatorType.MATCHES:
+        return new RegExp(decodeBase64(condition.value as string)).test(value as string);
+      case ObfuscatedOperatorType.NOT_MATCHES:
+        return !new RegExp(decodeBase64(condition.value as string)).test(value as string);
+      case ObfuscatedOperatorType.ONE_OF:
+        return isOneOf(
+          getMD5Hash(value.toString().toLowerCase()),
+          condition.value.map((value: string) => value.toLowerCase()),
+        );
+      case ObfuscatedOperatorType.NOT_ONE_OF:
+        return isNotOneOf(
+          getMD5Hash(value.toString().toLowerCase()),
+          condition.value.map((value: string) => value.toLowerCase()),
+        );
+    }
+  }
+  return false;
+}
+
+function isOneOf(attributeValue: string, conditionValue: string[]) {
+  return getMatchingStringValues(attributeValue, conditionValue).length > 0;
+}
+
+function isNotOneOf(attributeValue: string, conditionValue: string[]) {
+  return getMatchingStringValues(attributeValue, conditionValue).length === 0;
+}
+
+function getMatchingStringValues(attributeValue: string, conditionValues: string[]): string[] {
+  return conditionValues.filter((value) => value === attributeValue);
+}
+
+function compareNumber(
+  attributeValue: any,
+  conditionValue: any,
+  compareFn: (a: number, b: number) => boolean,
+): boolean {
+  return compareFn(Number(attributeValue), Number(conditionValue));
+}
+
+function compareSemVer(
+  attributeValue: any,
+  conditionValue: any,
+  compareFn: (a: string, b: string) => boolean,
+): boolean {
+  return (
+    !!validSemver(attributeValue) &&
+    !!validSemver(conditionValue) &&
+    compareFn(attributeValue, conditionValue)
+  );
+}
+
+function targetingRuleConditionValuesTypesFromValues(value: ConditionValueType): OperatorValueType {
+  // Check if input is a number
+  if (typeof value === 'number') {
+    return OperatorValueType.NUMERIC;
+  }
+
+  if (Array.isArray(value)) {
+    return OperatorValueType.STRING_ARRAY;
+  }
+
+  // Check if input is a string that represents a SemVer
+  if (typeof value === 'string' && validSemver(value)) {
+    return OperatorValueType.SEM_VER;
+  }
+
+  // Check if input is a string that represents a number
+  if (!isNaN(Number(value))) {
+    return OperatorValueType.NUMERIC;
+  }
+
+  // If none of the above, it's a general string
+  return OperatorValueType.PLAIN_STRING;
+}

From 4d6264eda58d2eb94b9fefa8426288edca2a535f Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 28 Mar 2024 23:36:59 -0700
Subject: [PATCH 11/39] cleanup

---
 src/assignment-cache.ts   | 1 -
 src/client/eppo-client.ts | 2 --
 src/eval.ts               | 2 +-
 src/sharders.ts           | 3 ++-
 4 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/assignment-cache.ts b/src/assignment-cache.ts
index aa402e41..4f492caf 100644
--- a/src/assignment-cache.ts
+++ b/src/assignment-cache.ts
@@ -1,6 +1,5 @@
 import { LRUCache } from 'lru-cache';
 
-import { EppoValue } from './eppo_value';
 import { getMD5Hash } from './obfuscation';
 
 export interface AssignmentCacheKey {
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index b2e47144..1df6acfd 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -219,7 +219,6 @@ export default class EppoClient implements IEppoClient {
     assignmentHooks?: IAssignmentHooks | undefined,
     obfuscated = false,
   ): boolean | null {
-    console.log(this.getAssignmentVariation);
     return (
       this.getAssignmentVariation(
         subjectKey,
@@ -372,7 +371,6 @@ export default class EppoClient implements IEppoClient {
 
     try {
       if (result && result.doLog) {
-        // TODO: check assignment cache
         this.logAssignment(result);
       }
     } catch (error) {
diff --git a/src/eval.ts b/src/eval.ts
index 416b6907..8b1f2c25 100644
--- a/src/eval.ts
+++ b/src/eval.ts
@@ -1,4 +1,4 @@
-import { Flag, Shard, Range, Variation, Allocation, Split } from './interfaces';
+import { Flag, Shard, Range, Variation } from './interfaces';
 import { matchesRule } from './rules';
 import { Sharder } from './sharders';
 
diff --git a/src/sharders.ts b/src/sharders.ts
index a702ea1d..ff2223fe 100644
--- a/src/sharders.ts
+++ b/src/sharders.ts
@@ -26,7 +26,8 @@ export class DeterministicSharder extends Sharder {
     this.lookup = lookup;
   }
 
-  getShard(input: string, totalShards: number): number {
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  getShard(input: string, _totalShards: number): number {
     return this.lookup[input] ?? 0;
   }
 }

From 1e9e10eb4168d8c49a0e3bb26ce61dc9db6f7094 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 10:38:55 -0700
Subject: [PATCH 12/39] Address PR comments

---
 src/assignment-logger.spec.ts           | 18 +++++++++
 src/client/eppo-client.spec.ts          | 49 +++++++++++++++++-------
 src/client/eppo-client.ts               |  2 +-
 src/eppo_value.ts                       | 51 ++++++++-----------------
 src/{eval.spec.ts => evaluator.spec.ts} |  2 +-
 src/{eval.ts => evaluator.ts}           | 12 +++---
 src/index.ts                            |  6 +--
 src/rules.spec.ts                       | 16 ++++++--
 src/rules.ts                            |  2 +-
 9 files changed, 93 insertions(+), 65 deletions(-)
 create mode 100644 src/assignment-logger.spec.ts
 rename src/{eval.spec.ts => evaluator.spec.ts} (99%)
 rename src/{eval.ts => evaluator.ts} (86%)

diff --git a/src/assignment-logger.spec.ts b/src/assignment-logger.spec.ts
new file mode 100644
index 00000000..88ea82c0
--- /dev/null
+++ b/src/assignment-logger.spec.ts
@@ -0,0 +1,18 @@
+import { IAssignmentEvent } from './assignment-logger';
+
+describe('IAssignmentEvent', () => {
+  it('should allow adding arbitrary fields', () => {
+    const event: IAssignmentEvent = {
+      allocation: 'allocation_123',
+      experiment: 'experiment_123',
+      featureFlag: 'feature_flag_123',
+      variation: 'variation_123',
+      subject: 'subject_123',
+      timestamp: new Date().toISOString(),
+      subjectAttributes: { age: 25, country: 'USA' },
+      holdoutKey: 'holdout_key_123',
+    };
+
+    expect(event.holdoutKey).toBe('holdout_key_123');
+  });
+});
diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 3d2075a8..b54d6d6e 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -17,7 +17,7 @@ import { IAssignmentHooks } from '../assignment-hooks';
 import { IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
 import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
-import { Evaluator } from '../eval';
+import { Evaluator } from '../evaluator';
 import FlagConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
@@ -90,7 +90,7 @@ export async function init(configurationStore: IConfigurationStore) {
 }
 
 describe('EppoClient E2E test', () => {
-  const evaluator = new Evaluator(new MD5Sharder());
+  const evaluator = new Evaluator();
   const storage = new TestConfigurationStore();
   const globalClient = new EppoClient(evaluator, storage);
 
@@ -147,7 +147,7 @@ describe('EppoClient E2E test', () => {
 
     beforeAll(() => {
       storage.setEntries({ [flagKey]: mockFlag });
-      const evaluator = new Evaluator(new MD5Sharder());
+      const evaluator = new Evaluator();
       client = new EppoClient(evaluator, storage);
 
       td.replace(EppoClient.prototype, 'getAssignmentDetail', function () {
@@ -172,7 +172,9 @@ describe('EppoClient E2E test', () => {
       client.setIsGracefulFailureMode(true);
 
       expect(client.getBoolAssignment('subject-identifer', flagKey, {}, true)).toBe(true);
+      expect(client.getBoolAssignment('subject-identifer', flagKey, {}, false)).toBe(false);
       expect(client.getNumericAssignment('subject-identifer', flagKey, {}, 1)).toBe(1);
+      expect(client.getNumericAssignment('subject-identifer', flagKey, {}, 0)).toBe(0);
       expect(client.getJSONAssignment('subject-identifer', flagKey, {}, {})).toEqual({});
       expect(client.getStringAssignment('subject-identifer', flagKey, {}, 'default')).toBe(
         'default',
@@ -208,7 +210,7 @@ describe('EppoClient E2E test', () => {
     it('Invokes logger for queued events', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const evaluator = new Evaluator(new MD5Sharder());
+      const evaluator = new Evaluator();
       const client = new EppoClient(evaluator, storage);
       client.getStringAssignment('subject-to-be-logged', flagKey);
       client.setLogger(mockLogger);
@@ -222,7 +224,7 @@ describe('EppoClient E2E test', () => {
     it('Does not log same queued event twice', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const evaluator = new Evaluator(new MD5Sharder());
+      const evaluator = new Evaluator();
       const client = new EppoClient(evaluator, storage);
 
       client.getStringAssignment('subject-to-be-logged', flagKey);
@@ -236,7 +238,7 @@ describe('EppoClient E2E test', () => {
     it('Does not invoke logger for events that exceed queue size', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const evaluator = new Evaluator(new MD5Sharder());
+      const evaluator = new Evaluator();
       const client = new EppoClient(evaluator, storage);
 
       for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
@@ -253,13 +255,32 @@ describe('EppoClient E2E test', () => {
       async ({ flag, variationType, subjects }: IAssignmentTestCase) => {
         `---- Test Case for ${flag} Experiment ----`;
 
-        const evaluator = new Evaluator(new MD5Sharder());
+        const evaluator = new Evaluator();
         const client = new EppoClient(evaluator, storage);
 
         let assignments: {
           subject: SubjectTestCase;
           assignment: string | boolean | number | null | object;
         }[] = [];
+
+        const typeAssignmentFunctions = {
+          [VariationType.BOOLEAN]: client.getBoolAssignment.bind(client),
+          [VariationType.NUMERIC]: client.getNumericAssignment.bind(client),
+          [VariationType.INTEGER]: client.getIntegerAssignment.bind(client),
+          [VariationType.STRING]: client.getStringAssignment.bind(client),
+          [VariationType.JSON]: client.getJSONAssignment.bind(client),
+        };
+
+        const assignmentFn = typeAssignmentFunctions[variationType];
+        if (!assignmentFn) {
+          throw new Error(`Unknown variation type: ${variationType}`);
+        }
+
+        // assignments = getTestAssignments(
+        //   { flag, variationType, subjects },
+        //   assignmentFn,
+        //   true,
+        // );
         switch (variationType) {
           case VariationType.BOOLEAN: {
             assignments = getTestAssignments(
@@ -309,7 +330,7 @@ describe('EppoClient E2E test', () => {
 
   describe('UFC Obfuscated Test Cases', () => {
     const storage = new TestConfigurationStore();
-    const evaluator = new Evaluator(new MD5Sharder());
+    const evaluator = new Evaluator();
     const globalClient = new EppoClient(evaluator, storage);
 
     beforeAll(async () => {
@@ -331,7 +352,7 @@ describe('EppoClient E2E test', () => {
       async ({ flag, variationType, subjects }: IAssignmentTestCase) => {
         `---- Test Case for ${flag} Experiment ----`;
 
-        const evaluator = new Evaluator(new MD5Sharder());
+        const evaluator = new Evaluator();
         const client = new EppoClient(evaluator, storage);
 
         let assignments: {
@@ -397,7 +418,7 @@ describe('EppoClient E2E test', () => {
   // });
 
   it('returns default value when key does not exist', async () => {
-    const evaluator = new Evaluator(new MD5Sharder());
+    const evaluator = new Evaluator();
     const client = new EppoClient(evaluator, storage);
 
     const nonExistantFlag = 'non-existent-flag';
@@ -414,7 +435,7 @@ describe('EppoClient E2E test', () => {
     const mockLogger = td.object<IAssignmentLogger>();
 
     storage.setEntries({ [flagKey]: mockFlag });
-    const evaluator = new Evaluator(new MD5Sharder());
+    const evaluator = new Evaluator();
     const client = new EppoClient(evaluator, storage);
     client.setLogger(mockLogger);
 
@@ -436,7 +457,7 @@ describe('EppoClient E2E test', () => {
     td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(new Error('logging error'));
 
     storage.setEntries({ [flagKey]: mockFlag });
-    const evaluator = new Evaluator(new MD5Sharder());
+    const evaluator = new Evaluator();
     const client = new EppoClient(evaluator, storage);
     client.setLogger(mockLogger);
 
@@ -455,7 +476,7 @@ describe('EppoClient E2E test', () => {
       mockLogger = td.object<IAssignmentLogger>();
 
       storage.setEntries({ [flagKey]: mockFlag });
-      evaluator = new Evaluator(new MD5Sharder());
+      evaluator = new Evaluator();
       client = new EppoClient(evaluator, storage);
       client.setLogger(mockLogger);
     });
@@ -656,7 +677,7 @@ describe('EppoClient E2E test', () => {
     let requestConfiguration: FlagConfigurationRequestParameters;
     let mockServerResponseFunc: (res: MockResponse) => MockResponse;
 
-    const evaluator = new Evaluator(new MD5Sharder());
+    const evaluator = new Evaluator();
     const ufcBody = JSON.stringify(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE));
     const flagKey = 'numeric_flag';
     const subject = 'alice';
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 1df6acfd..bdb2b1a2 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -18,7 +18,7 @@ import {
   POLL_INTERVAL_MS,
 } from '../constants';
 import { EppoValue } from '../eppo_value';
-import { Evaluator, FlagEvaluation, noneResult } from '../eval';
+import { Evaluator, FlagEvaluation, noneResult } from '../evaluator';
 import ExperimentConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index c8115d2a..94699474 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -36,23 +36,23 @@ export class EppoValue {
     value: boolean | number | string | object | null | undefined,
     valueType: VariationType,
   ): EppoValue {
-    if (value != null && value != undefined) {
-      switch (valueType) {
-        case VariationType.BOOLEAN:
-          return EppoValue.Bool(value as boolean);
-        case VariationType.NUMERIC:
-          return EppoValue.Numeric(value as number);
-        case VariationType.INTEGER:
-          return EppoValue.Numeric(value as number);
-        case VariationType.STRING:
-          return EppoValue.String(value as string);
-        case VariationType.JSON:
-          return EppoValue.JSON(value as object);
-        default:
-          return EppoValue.String(value as string);
-      }
+    if (value == null) {
+      return EppoValue.Null();
+    }
+    switch (valueType) {
+      case VariationType.BOOLEAN:
+        return EppoValue.Bool(value as boolean);
+      case VariationType.NUMERIC:
+        return EppoValue.Numeric(value as number);
+      case VariationType.INTEGER:
+        return EppoValue.Numeric(value as number);
+      case VariationType.STRING:
+        return EppoValue.String(value as string);
+      case VariationType.JSON:
+        return EppoValue.JSON(value as object);
+      default:
+        return EppoValue.String(value as string);
     }
-    return EppoValue.Null();
   }
 
   toString(): string {
@@ -85,25 +85,6 @@ export class EppoValue {
     return getMD5Hash(value);
   }
 
-  isExpectedType(): boolean {
-    switch (this.valueType) {
-      case EppoValueType.BoolType:
-        return typeof this.boolValue === 'boolean';
-      case EppoValueType.NumericType:
-        return typeof this.numericValue === 'number';
-      case EppoValueType.StringType:
-        return typeof this.stringValue === 'string';
-      case EppoValueType.JSONType:
-        return typeof this.objectValue === 'object';
-      case EppoValueType.NullType:
-        return false;
-    }
-  }
-
-  isNullType(): boolean {
-    return this.valueType === EppoValueType.NullType;
-  }
-
   static Bool(value: boolean): EppoValue {
     return new EppoValue(EppoValueType.BoolType, value, undefined, undefined, undefined);
   }
diff --git a/src/eval.spec.ts b/src/evaluator.spec.ts
similarity index 99%
rename from src/eval.spec.ts
rename to src/evaluator.spec.ts
index a212ced3..f7084dee 100644
--- a/src/eval.spec.ts
+++ b/src/evaluator.spec.ts
@@ -1,4 +1,4 @@
-import { Evaluator, hashKey, isInShardRange } from './eval';
+import { Evaluator, hashKey, isInShardRange } from './evaluator';
 import { Flag, Variation, Shard, VariationType } from './interfaces';
 import { encodeBase64, getMD5Hash } from './obfuscation';
 import { ObfuscatedOperatorType, OperatorType } from './rules';
diff --git a/src/eval.ts b/src/evaluator.ts
similarity index 86%
rename from src/eval.ts
rename to src/evaluator.ts
index 8b1f2c25..c952fef8 100644
--- a/src/eval.ts
+++ b/src/evaluator.ts
@@ -1,6 +1,6 @@
 import { Flag, Shard, Range, Variation } from './interfaces';
 import { matchesRule } from './rules';
-import { Sharder } from './sharders';
+import { MD5Sharder, Sharder } from './sharders';
 
 export interface FlagEvaluation {
   flagKey: string;
@@ -13,10 +13,10 @@ export interface FlagEvaluation {
 }
 
 export class Evaluator {
-  sharder: Sharder; // Assuming a Sharder type exists, replace 'any' with 'Sharder' when available
+  sharder: Sharder;
 
-  constructor(sharder: Sharder) {
-    this.sharder = sharder;
+  constructor(sharder?: Sharder) {
+    this.sharder = sharder ?? new MD5Sharder();
   }
 
   evaluateFlag(
@@ -62,8 +62,8 @@ export class Evaluator {
   }
 
   matchesShard(shard: Shard, subjectKey: string, totalShards: number): boolean {
-    const h = this.sharder.getShard(hashKey(shard.salt, subjectKey), totalShards);
-    return shard.ranges.some((range) => isInShardRange(h, range));
+    const assignedShard = this.sharder.getShard(hashKey(shard.salt, subjectKey), totalShards);
+    return shard.ranges.some((range) => isInShardRange(assignedShard, range));
   }
 }
 
diff --git a/src/index.ts b/src/index.ts
index f2c21f1c..4e6868c0 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -4,7 +4,7 @@ import { IAssignmentLogger, IAssignmentEvent } from './assignment-logger';
 import EppoClient, { FlagConfigurationRequestParameters, IEppoClient } from './client/eppo-client';
 import { IConfigurationStore } from './configuration-store';
 import * as constants from './constants';
-import ExperimentConfigurationRequestor from './flag-configuration-requestor';
+import FlagConfigRequestor from './flag-configuration-requestor';
 import HttpClient from './http-client';
 import * as validation from './validation';
 
@@ -15,10 +15,10 @@ export {
   EppoClient,
   IEppoClient,
   constants,
-  ExperimentConfigurationRequestor,
+  FlagConfigRequestor,
   HttpClient,
   validation,
   IConfigurationStore,
   AssignmentCache,
-  FlagConfigurationRequestParameters as ExperimentConfigurationRequestParameters,
+  FlagConfigurationRequestParameters,
 };
diff --git a/src/rules.spec.ts b/src/rules.spec.ts
index ae38aa92..2550b610 100644
--- a/src/rules.spec.ts
+++ b/src/rules.spec.ts
@@ -37,7 +37,7 @@ describe('rules', () => {
         {
           operator: OperatorType.ONE_OF,
           attribute: 'country',
-          value: ['USA', 'Canada'],
+          value: ['Canada', 'Mexico', 'USA'],
         },
       ],
     };
@@ -47,7 +47,7 @@ describe('rules', () => {
         {
           operator: OperatorType.NOT_ONE_OF,
           attribute: 'country',
-          value: ['USA', 'Canada'],
+          value: ['Canada', 'Mexico', 'USA'],
         },
       ],
     };
@@ -71,7 +71,7 @@ describe('rules', () => {
         {
           operator: OperatorType.MATCHES,
           attribute: 'user_id',
-          value: '[0-9]+',
+          value: '\\d+',
         },
       ],
     };
@@ -116,6 +116,10 @@ describe('rules', () => {
       expect(matchesRule(ruleWithOneOfCondition, { age: 10 }, false)).toBe(false);
     });
 
+    it('should return false for a rule with ONE_OF condition when subject attribute is null', () => {
+      expect(matchesRule(ruleWithOneOfCondition, { country: null }, false)).toBe(false);
+    });
+
     it('should return false for a rule with NOT_ONE_OF condition that matches the subject attributes', () => {
       expect(matchesRule(ruleWithNotOneOfCondition, { country: 'USA' }, false)).toBe(false);
     });
@@ -127,7 +131,11 @@ describe('rules', () => {
     it('should return false for a rule with NOT_ONE_OF condition when subject attribute is missing', () => {
       expect(matchesRule(ruleWithNotOneOfCondition, { age: 10 }, false)).toBe(false);
     });
-    ``;
+
+    it('should return false for a rule with NOT_ONE_OF condition when subject attribute is null', () => {
+      expect(matchesRule(ruleWithNotOneOfCondition, { country: null }, false)).toBe(false);
+    });
+
     it('should return true for a semver rule that matches the subject attributes', () => {
       expect(matchesRule(semverRule, subjectAttributes, false)).toBe(true);
     });
diff --git a/src/rules.ts b/src/rules.ts
index 1d52807a..3e301d83 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -32,7 +32,7 @@ export enum ObfuscatedOperatorType {
   NOT_ONE_OF = '602f5ee0b6e84fe29f43ab48b9e1addf',
 }
 
-export enum OperatorValueType {
+enum OperatorValueType {
   PLAIN_STRING = 'PLAIN_STRING',
   STRING_ARRAY = 'STRING_ARRAY',
   SEM_VER = 'SEM_VER',

From cab5b1f9f0cb135a1da06468ca20d51e70fbddeb Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 13:55:02 -0700
Subject: [PATCH 13/39] Address PR comments

---
 src/client/eppo-client.spec.ts | 271 +++++++++++++--------------------
 src/client/eppo-client.ts      | 103 +++++--------
 src/eppo_value.ts              |   4 +-
 src/interfaces.ts              |  10 +-
 test/testHelpers.ts            |   1 +
 test/writeObfuscatedMockUFC.ts |   3 +-
 6 files changed, 153 insertions(+), 239 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index b54d6d6e..78b34b4e 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -21,7 +21,6 @@ import { Evaluator } from '../evaluator';
 import FlagConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
-import { MD5Sharder } from '../sharders';
 
 import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
 
@@ -63,9 +62,8 @@ function getTestAssignments(
     const assignment = assignmentFn(
       subject.subjectKey,
       testCase.flag,
+      testCase.defaultValue,
       subject.subjectAttributes,
-      null,
-      undefined,
       obfuscated,
     );
     assignments.push({ subject: subject, assignment: assignment });
@@ -159,24 +157,18 @@ describe('EppoClient E2E test', () => {
       td.reset();
     });
 
-    it('returns null when graceful failure if error encountered', async () => {
-      client.setIsGracefulFailureMode(true);
-
-      expect(client.getBoolAssignment('subject-identifer', flagKey, {})).toBeNull();
-      expect(client.getNumericAssignment('subject-identifer', flagKey, {})).toBeNull();
-      expect(client.getJSONAssignment('subject-identifer', flagKey, {})).toBeNull();
-      expect(client.getStringAssignment('subject-identifer', flagKey, {})).toBeNull();
-    });
-
     it('returns default value when graceful failure if error encounterd', async () => {
       client.setIsGracefulFailureMode(true);
 
-      expect(client.getBoolAssignment('subject-identifer', flagKey, {}, true)).toBe(true);
-      expect(client.getBoolAssignment('subject-identifer', flagKey, {}, false)).toBe(false);
-      expect(client.getNumericAssignment('subject-identifer', flagKey, {}, 1)).toBe(1);
-      expect(client.getNumericAssignment('subject-identifer', flagKey, {}, 0)).toBe(0);
+      expect(client.getBoolAssignment('subject-identifer', flagKey, true, {})).toBe(true);
+      expect(client.getBoolAssignment('subject-identifer', flagKey, false, {})).toBe(false);
+      expect(client.getNumericAssignment('subject-identifer', flagKey, 1, {})).toBe(1);
+      expect(client.getNumericAssignment('subject-identifer', flagKey, 0, {})).toBe(0);
       expect(client.getJSONAssignment('subject-identifer', flagKey, {}, {})).toEqual({});
-      expect(client.getStringAssignment('subject-identifer', flagKey, {}, 'default')).toBe(
+      expect(
+        client.getJSONAssignment('subject-identifer', flagKey, { hello: 'world' }, {}),
+      ).toEqual({ hello: 'world' });
+      expect(client.getStringAssignment('subject-identifer', flagKey, 'default', {})).toBe(
         'default',
       );
     });
@@ -185,19 +177,19 @@ describe('EppoClient E2E test', () => {
       client.setIsGracefulFailureMode(false);
 
       expect(() => {
-        client.getBoolAssignment('subject-identifer', flagKey, {});
+        client.getBoolAssignment('subject-identifer', flagKey, true, {});
       }).toThrow();
 
       expect(() => {
-        client.getJSONAssignment('subject-identifer', flagKey, {});
+        client.getJSONAssignment('subject-identifer', flagKey, {}, {});
       }).toThrow();
 
       expect(() => {
-        client.getNumericAssignment('subject-identifer', flagKey, {});
+        client.getNumericAssignment('subject-identifer', flagKey, 1, {});
       }).toThrow();
 
       expect(() => {
-        client.getStringAssignment('subject-identifer', flagKey, {});
+        client.getStringAssignment('subject-identifer', flagKey, 'default', {});
       }).toThrow();
     });
   });
@@ -212,7 +204,7 @@ describe('EppoClient E2E test', () => {
 
       const evaluator = new Evaluator();
       const client = new EppoClient(evaluator, storage);
-      client.getStringAssignment('subject-to-be-logged', flagKey);
+      client.getStringAssignment('subject-to-be-logged', flagKey, 'default-value');
       client.setLogger(mockLogger);
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
@@ -227,7 +219,7 @@ describe('EppoClient E2E test', () => {
       const evaluator = new Evaluator();
       const client = new EppoClient(evaluator, storage);
 
-      client.getStringAssignment('subject-to-be-logged', flagKey);
+      client.getStringAssignment('subject-to-be-logged', flagKey, 'default-value');
       client.setLogger(mockLogger);
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
 
@@ -242,7 +234,7 @@ describe('EppoClient E2E test', () => {
       const client = new EppoClient(evaluator, storage);
 
       for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
-        client.getStringAssignment(`subject-to-be-logged-${i}`, flagKey);
+        client.getStringAssignment(`subject-to-be-logged-${i}`, flagKey, 'default-value');
       }
       client.setLogger(mockLogger);
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(MAX_EVENT_QUEUE_SIZE);
@@ -252,7 +244,7 @@ describe('EppoClient E2E test', () => {
   describe('UFC General Test Cases', () => {
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
-      async ({ flag, variationType, subjects }: IAssignmentTestCase) => {
+      async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
         `---- Test Case for ${flag} Experiment ----`;
 
         const evaluator = new Evaluator();
@@ -276,51 +268,12 @@ describe('EppoClient E2E test', () => {
           throw new Error(`Unknown variation type: ${variationType}`);
         }
 
-        // assignments = getTestAssignments(
-        //   { flag, variationType, subjects },
-        //   assignmentFn,
-        //   true,
-        // );
-        switch (variationType) {
-          case VariationType.BOOLEAN: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getBoolAssignment.bind(client),
-            );
-            break;
-          }
-          case VariationType.NUMERIC: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getNumericAssignment.bind(client),
-            );
-            break;
-          }
-          case VariationType.INTEGER: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getIntegerAssignment.bind(client),
-            );
-            break;
-          }
-          case VariationType.STRING: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getStringAssignment.bind(client),
-            );
-            break;
-          }
-          case VariationType.JSON: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getJSONAssignment.bind(client),
-            );
-            break;
-          }
-          default: {
-            throw new Error(`Unknown variation type: ${variationType}`);
-          }
-        }
+        assignments = getTestAssignments(
+          { flag, variationType, defaultValue, subjects },
+          assignmentFn,
+          false,
+        );
+
         for (const { subject, assignment } of assignments) {
           expect(assignment).toEqual(subject.assignment);
         }
@@ -349,61 +302,31 @@ describe('EppoClient E2E test', () => {
 
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
-      async ({ flag, variationType, subjects }: IAssignmentTestCase) => {
+      async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
         `---- Test Case for ${flag} Experiment ----`;
 
         const evaluator = new Evaluator();
         const client = new EppoClient(evaluator, storage);
 
-        let assignments: {
-          subject: SubjectTestCase;
-          assignment: string | boolean | number | null | object;
-        }[] = [];
-        switch (variationType) {
-          case VariationType.BOOLEAN: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getBoolAssignment.bind(client),
-              true,
-            );
-            break;
-          }
-          case VariationType.NUMERIC: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getNumericAssignment.bind(client),
-              true,
-            );
-            break;
-          }
-          case VariationType.INTEGER: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getIntegerAssignment.bind(client),
-              true,
-            );
-            break;
-          }
-          case VariationType.STRING: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getStringAssignment.bind(client),
-              true,
-            );
-            break;
-          }
-          case VariationType.JSON: {
-            assignments = getTestAssignments(
-              { flag, variationType, subjects },
-              client.getJSONAssignment.bind(client),
-              true,
-            );
-            break;
-          }
-          default: {
-            throw new Error(`Unknown variation type: ${variationType}`);
-          }
+        const typeAssignmentFunctions = {
+          [VariationType.BOOLEAN]: client.getBoolAssignment.bind(client),
+          [VariationType.NUMERIC]: client.getNumericAssignment.bind(client),
+          [VariationType.INTEGER]: client.getIntegerAssignment.bind(client),
+          [VariationType.STRING]: client.getStringAssignment.bind(client),
+          [VariationType.JSON]: client.getJSONAssignment.bind(client),
+        };
+
+        const assignmentFn = typeAssignmentFunctions[variationType];
+        if (!assignmentFn) {
+          throw new Error(`Unknown variation type: ${variationType}`);
         }
+
+        const assignments = getTestAssignments(
+          { flag, variationType, defaultValue, subjects },
+          assignmentFn,
+          true,
+        );
+
         for (const { subject, assignment } of assignments) {
           expect(assignment).toEqual(subject.assignment);
         }
@@ -423,10 +346,10 @@ describe('EppoClient E2E test', () => {
 
     const nonExistantFlag = 'non-existent-flag';
 
-    expect(client.getBoolAssignment('subject-identifer', nonExistantFlag, {}, true)).toBe(true);
-    expect(client.getNumericAssignment('subject-identifer', nonExistantFlag, {}, 1)).toBe(1);
+    expect(client.getBoolAssignment('subject-identifer', nonExistantFlag, true, {})).toBe(true);
+    expect(client.getNumericAssignment('subject-identifer', nonExistantFlag, 1, {})).toBe(1);
     expect(client.getJSONAssignment('subject-identifer', nonExistantFlag, {}, {})).toEqual({});
-    expect(client.getStringAssignment('subject-identifer', nonExistantFlag, {}, 'default')).toBe(
+    expect(client.getStringAssignment('subject-identifer', nonExistantFlag, 'default', {})).toBe(
       'default',
     );
   });
@@ -440,7 +363,12 @@ describe('EppoClient E2E test', () => {
     client.setLogger(mockLogger);
 
     const subjectAttributes = { foo: 3 };
-    const assignment = client.getStringAssignment('subject-10', flagKey, subjectAttributes);
+    const assignment = client.getStringAssignment(
+      'subject-10',
+      flagKey,
+      'default',
+      subjectAttributes,
+    );
 
     expect(assignment).toEqual(variationA.value);
     expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
@@ -462,7 +390,12 @@ describe('EppoClient E2E test', () => {
     client.setLogger(mockLogger);
 
     const subjectAttributes = { foo: 3 };
-    const assignment = client.getStringAssignment('subject-10', flagKey, subjectAttributes);
+    const assignment = client.getStringAssignment(
+      'subject-10',
+      flagKey,
+      'default',
+      subjectAttributes,
+    );
 
     expect(assignment).toEqual('variation-a');
   });
@@ -484,8 +417,8 @@ describe('EppoClient E2E test', () => {
     it('logs duplicate assignments without an assignment cache', () => {
       client.disableAssignmentCache();
 
-      client.getStringAssignment('subject-10', flagKey);
-      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment('subject-10', flagKey, 'default');
 
       // call count should be 2 because there is no cache.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
@@ -494,8 +427,8 @@ describe('EppoClient E2E test', () => {
     it('does not log duplicate assignments', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
-      client.getStringAssignment('subject-10', flagKey);
-      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment('subject-10', flagKey, 'default');
 
       // call count should be 1 because the second call is a cache hit and not logged.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
@@ -504,15 +437,15 @@ describe('EppoClient E2E test', () => {
     it('logs assignment again after the lru cache is full', () => {
       client.useLRUInMemoryAssignmentCache(2);
 
-      client.getStringAssignment('subject-10', flagKey); // logged
-      client.getStringAssignment('subject-10', flagKey); // cached
+      client.getStringAssignment('subject-10', flagKey, 'default'); // logged
+      client.getStringAssignment('subject-10', flagKey, 'default'); // cached
 
-      client.getStringAssignment('subject-11', flagKey); // logged
-      client.getStringAssignment('subject-11', flagKey); // cached
+      client.getStringAssignment('subject-11', flagKey, 'default'); // logged
+      client.getStringAssignment('subject-11', flagKey, 'default'); // cached
 
-      client.getStringAssignment('subject-12', flagKey); // cache evicted subject-10, logged
-      client.getStringAssignment('subject-10', flagKey); // previously evicted, logged
-      client.getStringAssignment('subject-12', flagKey); // cached
+      client.getStringAssignment('subject-12', flagKey, 'default'); // cache evicted subject-10, logged
+      client.getStringAssignment('subject-10', flagKey, 'default'); // previously evicted, logged
+      client.getStringAssignment('subject-12', flagKey, 'default'); // cached
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(4);
     });
@@ -524,8 +457,8 @@ describe('EppoClient E2E test', () => {
 
       client.setLogger(mockLogger);
 
-      client.getStringAssignment('subject-10', flagKey);
-      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment('subject-10', flagKey, 'default');
 
       // call count should be 2 because the first call had an exception
       // therefore we are not sure the logger was successful and try again.
@@ -547,15 +480,15 @@ describe('EppoClient E2E test', () => {
 
       client.useNonExpiringInMemoryAssignmentCache();
 
-      client.getStringAssignment('subject-10', flagKey);
-      client.getStringAssignment('subject-10', flagKey);
-      client.getStringAssignment('subject-10', 'flag-2');
-      client.getStringAssignment('subject-10', 'flag-2');
-      client.getStringAssignment('subject-10', 'flag-3');
-      client.getStringAssignment('subject-10', 'flag-3');
-      client.getStringAssignment('subject-10', flagKey);
-      client.getStringAssignment('subject-10', 'flag-2');
-      client.getStringAssignment('subject-10', 'flag-3');
+      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment('subject-10', 'flag-2', 'default');
+      client.getStringAssignment('subject-10', 'flag-2', 'default');
+      client.getStringAssignment('subject-10', 'flag-3', 'default');
+      client.getStringAssignment('subject-10', 'flag-3', 'default');
+      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment('subject-10', 'flag-2', 'default');
+      client.getStringAssignment('subject-10', 'flag-3', 'default');
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(3);
     });
@@ -582,7 +515,7 @@ describe('EppoClient E2E test', () => {
           ],
         },
       });
-      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey, 'default');
 
       storage.setEntries({
         [flagKey]: {
@@ -602,7 +535,7 @@ describe('EppoClient E2E test', () => {
           ],
         },
       });
-      client.getStringAssignment('subject-10', flagKey);
+      client.getStringAssignment('subject-10', flagKey, 'default');
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
     });
 
@@ -612,8 +545,8 @@ describe('EppoClient E2E test', () => {
       // original configuration version
       storage.setEntries({ [flagKey]: mockFlag });
 
-      client.getStringAssignment('subject-10', flagKey); // log this assignment
-      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey, 'default'); // log this assignment
+      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
 
       // change the variation
       storage.setEntries({
@@ -635,14 +568,14 @@ describe('EppoClient E2E test', () => {
         },
       });
 
-      client.getStringAssignment('subject-10', flagKey); // log this assignment
-      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey, 'default'); // log this assignment
+      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
 
       // change the flag again, back to the original
       storage.setEntries({ [flagKey]: mockFlag });
 
-      client.getStringAssignment('subject-10', flagKey); // important: log this assignment
-      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey, 'default'); // important: log this assignment
+      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
 
       // change the allocation
       storage.setEntries({
@@ -664,8 +597,8 @@ describe('EppoClient E2E test', () => {
         },
       });
 
-      client.getStringAssignment('subject-10', flagKey); // log this assignment
-      client.getStringAssignment('subject-10', flagKey); // cache hit, don't log
+      client.getStringAssignment('subject-10', flagKey, 'default'); // log this assignment
+      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(4);
     });
@@ -735,11 +668,11 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(evaluator, storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey);
-      expect(variation).toBeNull();
+      let variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      expect(variation).toBe(0.0);
       // have client fetch configurations
       await client.fetchFlagConfigurations();
-      variation = client.getNumericAssignment(subject, flagKey);
+      variation = client.getNumericAssignment(subject, flagKey, 0.0);
       expect(variation).toBe(pi);
     });
 
@@ -748,11 +681,11 @@ describe('EppoClient E2E test', () => {
       client.setIsGracefulFailureMode(false);
       client.setConfigurationRequestParameters(requestConfiguration);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey);
-      expect(variation).toBeNull();
+      let variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      expect(variation).toBe(0.0);
       // have client fetch configurations
       await client.fetchFlagConfigurations();
-      variation = client.getNumericAssignment(subject, flagKey);
+      variation = client.getNumericAssignment(subject, flagKey, 0.0);
       expect(variation).toBe(pi);
     });
 
@@ -779,8 +712,8 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(evaluator, storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey);
-      expect(variation).toBeNull();
+      let variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      expect(variation).toBe(0.0);
 
       // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
       const fetchPromise = client.fetchFlagConfigurations();
@@ -791,7 +724,7 @@ describe('EppoClient E2E test', () => {
       // Await so it can finish its initialization before this test proceeds
       await fetchPromise;
 
-      variation = client.getNumericAssignment(subject, flagKey);
+      variation = client.getNumericAssignment(subject, flagKey, 0.0);
       expect(variation).toBe(pi);
       expect(callCount).toBe(2);
 
@@ -835,7 +768,7 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(evaluator, storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      expect(client.getNumericAssignment(subject, flagKey)).toBeNull();
+      expect(client.getNumericAssignment(subject, flagKey, 0.0)).toBe(0.0);
 
       // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
       if (throwOnFailedInitialization) {
@@ -845,15 +778,15 @@ describe('EppoClient E2E test', () => {
       }
       expect(callCount).toBe(1);
       // still no configuration loaded
-      expect(client.getNumericAssignment(subject, flagKey)).toBeNull();
+      expect(client.getNumericAssignment(subject, flagKey, 10.0)).toBe(10.0);
 
       // Advance timers so a post-init poll can take place
       await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
 
       // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
       expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);
-      expect(client.getNumericAssignment(subject, flagKey)).toBe(
-        pollAfterFailedInitialization ? pi : null,
+      expect(client.getNumericAssignment(subject, flagKey, 0.0)).toBe(
+        pollAfterFailedInitialization ? pi : 0.0,
       );
     });
   });
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index bdb2b1a2..dac9f362 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -6,7 +6,6 @@ import {
   LRUInMemoryAssignmentCache,
   NonExpiringInMemoryAssignmentCache,
 } from '../assignment-cache';
-import { IAssignmentHooks } from '../assignment-hooks';
 import { IAssignmentEvent, IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
 import {
@@ -45,38 +44,30 @@ export interface IEppoClient {
   getStringAssignment(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes?: Record<string, any>,
-    defaultValue?: string | null,
-    assignmentHooks?: IAssignmentHooks,
-  ): string | null;
+    defaultValue: string,
+    subjectAttributes?: Record<string, AttributeType>,
+  ): string;
 
   getBoolAssignment(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes?: Record<string, any>,
-    defaultValue?: boolean | null,
-    assignmentHooks?: IAssignmentHooks,
-  ): boolean | null;
+    defaultValue: boolean,
+    subjectAttributes?: Record<string, AttributeType>,
+  ): boolean;
 
   getNumericAssignment(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes?: Record<string, any>,
-    defaultValue?: number | null,
-    assignmentHooks?: IAssignmentHooks,
-  ): number | null;
+    defaultValue: number,
+    subjectAttributes?: Record<string, AttributeType>,
+  ): number;
 
   getJSONAssignment(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes?: Record<string, any>,
-    defaultValue?: object | null,
-    assignmentHooks?: IAssignmentHooks,
-  ): object | null;
+    defaultValue: object,
+    subjectAttributes?: Record<string, AttributeType>,
+  ): object;
 
   setLogger(logger: IAssignmentLogger): void;
 
@@ -93,6 +84,8 @@ export interface IEppoClient {
   stopPolling(): void;
 
   setIsGracefulFailureMode(gracefulFailureMode: boolean): void;
+
+  getFlagKeys(): string[];
 }
 
 export type FlagConfigurationRequestParameters = {
@@ -191,108 +184,96 @@ export default class EppoClient implements IEppoClient {
   public getStringAssignment(
     subjectKey: string,
     flagKey: string,
+    defaultValue: string,
     // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> = {},
-    defaultValue?: string | null,
-    assignmentHooks?: IAssignmentHooks | undefined,
+    subjectAttributes: Record<string, AttributeType> = {},
     obfuscated = false,
-  ): string | null {
+  ): string {
     return (
       this.getAssignmentVariation(
         subjectKey,
         flagKey,
+        EppoValue.String(defaultValue),
         subjectAttributes,
-        defaultValue ? EppoValue.String(defaultValue) : EppoValue.Null(),
-        assignmentHooks,
         obfuscated,
         VariationType.STRING,
-      ).stringValue ?? null
+      ).stringValue ?? defaultValue
     );
   }
 
   getBoolAssignment(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> = {},
-    defaultValue: boolean | null = null,
-    assignmentHooks?: IAssignmentHooks | undefined,
+    defaultValue: boolean,
+    subjectAttributes: Record<string, AttributeType> = {},
     obfuscated = false,
-  ): boolean | null {
+  ): boolean {
     return (
       this.getAssignmentVariation(
         subjectKey,
         flagKey,
+        EppoValue.Bool(defaultValue),
         subjectAttributes,
-        defaultValue ? EppoValue.Bool(defaultValue) : EppoValue.Null(),
-        assignmentHooks,
         obfuscated,
         VariationType.BOOLEAN,
-      ).boolValue ?? null
+      ).boolValue ?? defaultValue
     );
   }
 
   getNumericAssignment(
     subjectKey: string,
     flagKey: string,
-    subjectAttributes?: Record<string, EppoValue>,
-    defaultValue?: number | null,
-    assignmentHooks?: IAssignmentHooks | undefined,
+    defaultValue: number,
+    subjectAttributes?: Record<string, AttributeType>,
     obfuscated = false,
-  ): number | null {
+  ): number {
     return (
       this.getAssignmentVariation(
         subjectKey,
         flagKey,
+        EppoValue.Numeric(defaultValue),
         subjectAttributes,
-        defaultValue ? EppoValue.Numeric(defaultValue) : EppoValue.Null(),
-        assignmentHooks,
         obfuscated,
         VariationType.NUMERIC,
-      ).numericValue ?? null
+      ).numericValue ?? defaultValue
     );
   }
 
   getIntegerAssignment(
     subjectKey: string,
     flagKey: string,
-    subjectAttributes?: Record<string, EppoValue>,
-    defaultValue?: number | null,
-    assignmentHooks?: IAssignmentHooks | undefined,
+    defaultValue: number,
+    subjectAttributes?: Record<string, AttributeType>,
     obfuscated = false,
-  ): number | null {
+  ): number {
     return (
       this.getAssignmentVariation(
         subjectKey,
         flagKey,
+        EppoValue.Numeric(defaultValue),
         subjectAttributes,
-        defaultValue ? EppoValue.Numeric(defaultValue) : EppoValue.Null(),
-        assignmentHooks,
         obfuscated,
         VariationType.INTEGER,
-      ).numericValue ?? null
+      ).numericValue ?? defaultValue
     );
   }
 
   public getJSONAssignment(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> = {},
-    defaultValue?: object | null,
-    assignmentHooks?: IAssignmentHooks | undefined,
+    defaultValue: object,
+    subjectAttributes: Record<string, AttributeType> = {},
     obfuscated = false,
-  ): object | null {
+  ): object {
     return (
       this.getAssignmentVariation(
         subjectKey,
         flagKey,
+        EppoValue.JSON(defaultValue),
         subjectAttributes,
-        defaultValue ? EppoValue.JSON(defaultValue) : EppoValue.Null(),
-        assignmentHooks,
         obfuscated,
         VariationType.JSON,
-      ).objectValue ?? null
+      ).objectValue ?? defaultValue
     );
   }
 
@@ -307,10 +288,8 @@ export default class EppoClient implements IEppoClient {
   private getAssignmentVariation(
     subjectKey: string,
     flagKey: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    subjectAttributes: Record<string, any> = {},
     defaultValue: EppoValue,
-    assignmentHooks: IAssignmentHooks | undefined,
+    subjectAttributes: Record<string, AttributeType> = {},
     obfuscated: boolean,
     expectedVariationType: VariationType,
   ): EppoValue {
@@ -384,7 +363,7 @@ export default class EppoClient implements IEppoClient {
     return expectedType === undefined || actualType === expectedType;
   }
 
-  public get_flag_keys() {
+  public getFlagKeys() {
     /**
      * Returns a list of all flag keys that have been initialized.
      * This can be useful to debug the initialization process.
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index 94699474..0ab9fdae 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -9,7 +9,7 @@ export enum EppoValueType {
   JSONType,
 }
 
-export type IValue = boolean | number | string | undefined;
+export type IValue = boolean | number | string;
 
 export class EppoValue {
   public valueType: EppoValueType;
@@ -33,7 +33,7 @@ export class EppoValue {
   }
 
   static generateEppoValue(
-    value: boolean | number | string | object | null | undefined,
+    value: boolean | number | string | object,
     valueType: VariationType,
   ): EppoValue {
     if (value == null) {
diff --git a/src/interfaces.ts b/src/interfaces.ts
index 765adaa8..d949bd72 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -1,11 +1,11 @@
 import { Rule } from './rules';
 
 export enum VariationType {
-  STRING = 'string',
-  INTEGER = 'integer',
-  NUMERIC = 'numeric',
-  BOOLEAN = 'boolean',
-  JSON = 'json',
+  STRING = 'STRING',
+  INTEGER = 'INTEGER',
+  NUMERIC = 'NUMERIC',
+  BOOLEAN = 'BOOLEAN',
+  JSON = 'JSON',
 }
 
 export interface Variation {
diff --git a/test/testHelpers.ts b/test/testHelpers.ts
index 6d3eebcc..ffa3501d 100644
--- a/test/testHelpers.ts
+++ b/test/testHelpers.ts
@@ -25,6 +25,7 @@ export interface SubjectTestCase {
 export interface IAssignmentTestCase {
   flag: string;
   variationType: VariationType;
+  defaultValue: string | number | boolean | object;
   subjects: SubjectTestCase[];
 }
 
diff --git a/test/writeObfuscatedMockUFC.ts b/test/writeObfuscatedMockUFC.ts
index 5ffceb3d..fc7cd102 100644
--- a/test/writeObfuscatedMockUFC.ts
+++ b/test/writeObfuscatedMockUFC.ts
@@ -1,7 +1,8 @@
 import * as fs from 'fs';
 
-import { Flag, Rule } from '../src/interfaces';
+import { Flag } from '../src/interfaces';
 import { encodeBase64, getMD5Hash } from '../src/obfuscation';
+import { Rule } from '../src/rules';
 
 import {
   MOCK_UFC_RESPONSE_FILE,

From e5d4be1ad12ba1a25a3e55748838d4d86ffd1db8 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 14:23:30 -0700
Subject: [PATCH 14/39] Hardcoded obfuscation tests and other fixes

---
 src/client/eppo-client.spec.ts | 10 +++---
 src/client/eppo-client.ts      |  2 +-
 src/evaluator.spec.ts          | 25 +++++----------
 src/rules.spec.ts              | 57 ++++++++++++++++++++++------------
 4 files changed, 51 insertions(+), 43 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 78b34b4e..6bf03f28 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -344,12 +344,12 @@ describe('EppoClient E2E test', () => {
     const evaluator = new Evaluator();
     const client = new EppoClient(evaluator, storage);
 
-    const nonExistantFlag = 'non-existent-flag';
+    const nonExistentFlag = 'non-existent-flag';
 
-    expect(client.getBoolAssignment('subject-identifer', nonExistantFlag, true, {})).toBe(true);
-    expect(client.getNumericAssignment('subject-identifer', nonExistantFlag, 1, {})).toBe(1);
-    expect(client.getJSONAssignment('subject-identifer', nonExistantFlag, {}, {})).toEqual({});
-    expect(client.getStringAssignment('subject-identifer', nonExistantFlag, 'default', {})).toBe(
+    expect(client.getBoolAssignment('subject-identifer', nonExistentFlag, true, {})).toBe(true);
+    expect(client.getNumericAssignment('subject-identifer', nonExistentFlag, 1, {})).toBe(1);
+    expect(client.getJSONAssignment('subject-identifer', nonExistentFlag, {}, {})).toEqual({});
+    expect(client.getStringAssignment('subject-identifer', nonExistentFlag, 'default', {})).toBe(
       'default',
     );
   });
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index dac9f362..d209d907 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -349,7 +349,7 @@ export default class EppoClient implements IEppoClient {
     }
 
     try {
-      if (result && result.doLog) {
+      if (result?.doLog) {
         this.logAssignment(result);
       }
     } catch (error) {
diff --git a/src/evaluator.spec.ts b/src/evaluator.spec.ts
index f7084dee..1adf8d9a 100644
--- a/src/evaluator.spec.ts
+++ b/src/evaluator.spec.ts
@@ -1,14 +1,16 @@
 import { Evaluator, hashKey, isInShardRange } from './evaluator';
 import { Flag, Variation, Shard, VariationType } from './interfaces';
-import { encodeBase64, getMD5Hash } from './obfuscation';
+import { getMD5Hash } from './obfuscation';
 import { ObfuscatedOperatorType, OperatorType } from './rules';
-import { MD5Sharder, DeterministicSharder } from './sharders';
+import { DeterministicSharder } from './sharders';
 
 describe('Evaluator', () => {
   const VARIATION_A: Variation = { key: 'a', value: 'A' };
   const VARIATION_B: Variation = { key: 'b', value: 'B' };
   const VARIATION_C: Variation = { key: 'c', value: 'C' };
 
+  const evaluator = new Evaluator();
+
   it('should return none result for disabled flag', () => {
     const flag: Flag = {
       key: 'disabled_flag',
@@ -32,7 +34,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
     expect(result.flagKey).toEqual('disabled_flag');
     expect(result.allocationKey).toBeNull();
@@ -46,7 +47,6 @@ describe('Evaluator', () => {
       ranges: [{ start: 0, end: 100 }],
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     expect(evaluator.matchesShard(shard, 'subject_key', 100)).toBeTruthy();
   });
 
@@ -59,7 +59,6 @@ describe('Evaluator', () => {
       ],
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     expect(evaluator.matchesShard(shard, 'subject_key', 100)).toBeTruthy();
 
     const deterministicEvaluator = new Evaluator(new DeterministicSharder({ subject_key: 50 }));
@@ -86,7 +85,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(emptyFlag, 'subject_key', {}, false);
     expect(result.flagKey).toEqual('empty');
     expect(result.allocationKey).toBeNull();
@@ -117,7 +115,6 @@ describe('Evaluator', () => {
       totalShards: 10000,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'user-1', {}, false);
     expect(result.variation).toEqual({ key: 'control', value: 'control-value' });
   });
@@ -151,7 +148,6 @@ describe('Evaluator', () => {
       totalShards: 10000,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     let result = evaluator.evaluateFlag(flag, 'alice', {}, false);
     expect(result.variation).toEqual({ key: 'control', value: 'control' });
 
@@ -185,7 +181,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
     expect(result.flagKey).toEqual('flag');
     expect(result.allocationKey).toEqual('default');
@@ -234,7 +229,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(
       flag,
       'subject_key',
@@ -287,7 +281,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', { email: 'eppo@test.com' }, false);
     expect(result.flagKey).toEqual('flag');
     expect(result.allocationKey).toEqual('default');
@@ -309,7 +302,7 @@ describe('Evaluator', () => {
                 {
                   operator: ObfuscatedOperatorType.MATCHES,
                   attribute: getMD5Hash('email'),
-                  value: encodeBase64('.*@example\\.com$'),
+                  value: 'LipAZXhhbXBsZVxcLmNvbSQ=', //encodeBase64('.*@example\\.com$')
                 },
               ],
             },
@@ -339,7 +332,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', { email: 'eppo@test.com' }, false);
     expect(result.flagKey).toEqual('obfuscated_flag_key');
     expect(result.allocationKey).toEqual('default');
@@ -419,7 +411,7 @@ describe('Evaluator', () => {
     );
   });
 
-  it('should return none result for evaluation prior to allocation', () => {
+  it('should not match on allocation before startAt has passed', () => {
     const now = new Date();
     const flag: Flag = {
       key: 'flag',
@@ -445,7 +437,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
     expect(result.flagKey).toEqual('flag');
     expect(result.allocationKey).toBeNull();
@@ -478,14 +469,13 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
     expect(result.flagKey).toEqual('flag');
     expect(result.allocationKey).toEqual('default');
     expect(result.variation).toEqual(VARIATION_A);
   });
 
-  it('should evaluate flag after allocation period', () => {
+  it('should not match on allocation after endAt has passed', () => {
     const now = new Date();
     const flag: Flag = {
       key: 'flag',
@@ -511,7 +501,6 @@ describe('Evaluator', () => {
       totalShards: 10,
     };
 
-    const evaluator = new Evaluator(new MD5Sharder());
     const result = evaluator.evaluateFlag(flag, 'subject_key', {}, false);
     expect(result.flagKey).toEqual('flag');
     expect(result.allocationKey).toBeNull();
diff --git a/src/rules.spec.ts b/src/rules.spec.ts
index 2550b610..74daa069 100644
--- a/src/rules.spec.ts
+++ b/src/rules.spec.ts
@@ -57,12 +57,12 @@ describe('rules', () => {
         {
           operator: OperatorType.GTE,
           attribute: 'version',
-          value: '1.0.0',
+          value: '1.2.5',
         },
         {
           operator: OperatorType.LTE,
           attribute: 'version',
-          value: '2.0.0',
+          value: '2.4.2',
         },
       ],
     };
@@ -141,7 +141,12 @@ describe('rules', () => {
     });
 
     it('should return false for a semver rule that does not match the subject attributes', () => {
-      const failingAttributes = { version: '2.1.0' };
+      const failingAttributes = { version: '2.6.2' };
+      expect(matchesRule(semverRule, failingAttributes, false)).toBe(false);
+    });
+
+    it('should return false for a semver rule that does not match the subject attributes', () => {
+      const failingAttributes = { version: '1.0.6' };
       expect(matchesRule(semverRule, failingAttributes, false)).toBe(false);
     });
 
@@ -165,8 +170,12 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.ONE_OF,
-            attribute: getMD5Hash('country'),
-            value: ['usa', 'canada', 'mexico'].map(getMD5Hash),
+            attribute: 'e909c2d7067ea37437cf97fe11d91bd0', // getMD5Hash('country')
+            value: [
+              'ada53304c5b9e4a839615b6e8f908eb6',
+              'c2aadac2ca30ca8aadfbe331ae180d28',
+              '4edfc924721abb774d5447bade86ea5d',
+            ], // ['usa', 'canada', 'mexico'].map(getMD5Hash)
           },
         ],
       };
@@ -175,8 +184,12 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.NOT_ONE_OF,
-            attribute: getMD5Hash('country'),
-            value: ['usa', 'canada', 'mexico'].map(getMD5Hash),
+            attribute: 'e909c2d7067ea37437cf97fe11d91bd0', // getMD5Hash('country')
+            value: [
+              'ada53304c5b9e4a839615b6e8f908eb6',
+              'c2aadac2ca30ca8aadfbe331ae180d28',
+              '4edfc924721abb774d5447bade86ea5d',
+            ], // ['usa', 'canada', 'mexico'].map(getMD5Hash)
           },
         ],
       };
@@ -185,8 +198,8 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.GTE,
-            attribute: getMD5Hash('age'),
-            value: encodeBase64('18'),
+            attribute: '7d637d275668ed6d41a9b97e6ad3a556', //getMD5Hash('age')
+            value: 'MTg=', //encodeBase64('18')
           },
         ],
       };
@@ -195,8 +208,8 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.GT,
-            attribute: getMD5Hash('age'),
-            value: encodeBase64('18'),
+            attribute: '7d637d275668ed6d41a9b97e6ad3a556', //getMD5Hash('age')
+            value: 'MTg=', //encodeBase64('18')
           },
         ],
       };
@@ -204,8 +217,8 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.LTE,
-            attribute: getMD5Hash('age'),
-            value: encodeBase64('18'),
+            attribute: '7d637d275668ed6d41a9b97e6ad3a556', //getMD5Hash('age')
+            value: 'MTg=', //encodeBase64('18')
           },
         ],
       };
@@ -214,8 +227,8 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.LT,
-            attribute: getMD5Hash('age'),
-            value: encodeBase64('18'),
+            attribute: '7d637d275668ed6d41a9b97e6ad3a556', //getMD5Hash('age')
+            value: 'MTg=', //encodeBase64('18')
           },
         ],
       };
@@ -223,8 +236,8 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.MATCHES,
-            attribute: getMD5Hash('email'),
-            value: encodeBase64('.+@example\\.com$'),
+            attribute: '0c83f57c786a0b4a39efab23731c7ebc', // getMD5Hash('email')
+            value: 'LitAZXhhbXBsZVwuY29tJA==', //encodeBase64('.+@example\\.com$')
           },
         ],
       };
@@ -233,8 +246,8 @@ describe('rules', () => {
         conditions: [
           {
             operator: ObfuscatedOperatorType.NOT_MATCHES,
-            attribute: getMD5Hash('email'),
-            value: encodeBase64('.+@example\\.com$'),
+            attribute: '0c83f57c786a0b4a39efab23731c7ebc', // getMD5Hash('email')
+            value: 'LitAZXhhbXBsZVwuY29tJA==', //encodeBase64('.+@example\\.com$')
           },
         ],
       };
@@ -259,6 +272,12 @@ describe('rules', () => {
         );
       });
 
+      it('should return false for an obfuscated rule with NOT_ONE_OF condition when the subject attribute is null', () => {
+        expect(matchesRule(obfuscatedRuleWithNotOneOfCondition, { country: null }, true)).toBe(
+          false,
+        );
+      });
+
       it('should return true for an obfuscated rule with GTE condition that matches the subject attributes', () => {
         expect(matchesRule(obfuscatedRuleWithGTECondition, { age: 18 }, true)).toBe(true);
       });

From 7400ed039d231948bf89cd14c180b9cf82d6a62c Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 14:34:49 -0700
Subject: [PATCH 15/39] test for overwriting id for matching rules

---
 src/evaluator.spec.ts | 33 +++++++++++++++++++++++++++++++++
 1 file changed, 33 insertions(+)

diff --git a/src/evaluator.spec.ts b/src/evaluator.spec.ts
index 1adf8d9a..c6c6eafd 100644
--- a/src/evaluator.spec.ts
+++ b/src/evaluator.spec.ts
@@ -158,6 +158,39 @@ describe('Evaluator', () => {
     expect(result.variation).toBeNull();
   });
 
+  it('should evaluate flag based on a targeting condition with overwritten id', () => {
+    const flag: Flag = {
+      key: 'flag-key',
+      enabled: true,
+      variationType: VariationType.STRING,
+      variations: { control: { key: 'control', value: 'control' } },
+      allocations: [
+        {
+          key: 'allocation',
+          rules: [
+            {
+              conditions: [
+                { operator: OperatorType.ONE_OF, attribute: 'id', value: ['alice', 'bob'] },
+              ],
+            },
+          ],
+          splits: [
+            {
+              variationKey: 'control',
+              shards: [],
+              extraLogging: {},
+            },
+          ],
+          doLog: true,
+        },
+      ],
+      totalShards: 10000,
+    };
+
+    const result = evaluator.evaluateFlag(flag, 'alice', { id: 'charlie' }, false);
+    expect(result.variation).toBeNull();
+  });
+
   it('should catch all allocation and return variation A', () => {
     const flag: Flag = {
       key: 'flag',

From 4d9a3a0ef53b35c4bfe38a1450fc24e0a95d769e Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 14:35:14 -0700
Subject: [PATCH 16/39] remove unused import

---
 src/rules.spec.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/rules.spec.ts b/src/rules.spec.ts
index 74daa069..491655b9 100644
--- a/src/rules.spec.ts
+++ b/src/rules.spec.ts
@@ -1,4 +1,4 @@
-import { encodeBase64, getMD5Hash } from './obfuscation';
+import { getMD5Hash } from './obfuscation';
 import { ObfuscatedOperatorType, OperatorType, Rule, matchesRule } from './rules';
 
 describe('rules', () => {

From 67f7cbcb2bb9f73231b30c816d4d8f1bf0accc39 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 14:36:43 -0700
Subject: [PATCH 17/39] remove unneeded lint comment

---
 src/client/eppo-client.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index d209d907..bd4abdb3 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -185,7 +185,6 @@ export default class EppoClient implements IEppoClient {
     subjectKey: string,
     flagKey: string,
     defaultValue: string,
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
     subjectAttributes: Record<string, AttributeType> = {},
     obfuscated = false,
   ): string {

From c6b0960c19d5c087966287cce31ee5c514627ec1 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 14:43:29 -0700
Subject: [PATCH 18/39] Add comment on getAssignmentDetail

---
 src/client/eppo-client.ts | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index bd4abdb3..1c7c1988 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -311,6 +311,19 @@ export default class EppoClient implements IEppoClient {
     }
   }
 
+  /**
+   * [Experimental] Get a detailed return of assignment for a particular subject and flag.
+   *
+   * Note: This method is experimental and may change in future versions.
+   * Please only use for debugging purposes, and not in production.
+   *
+   * @param subjectKey The subject key
+   * @param flagKey The flag key
+   * @param subjectAttributes The subject attributes
+   * @param expectedVariationType The expected variation type
+   * @param obfuscated Whether the flag key is obfuscated
+   * @returns A detailed return of assignment for a particular subject and flag
+   */
   public getAssignmentDetail(
     subjectKey: string,
     flagKey: string,

From d71727e0e328b28766985c28e4ec88967adaa1c3 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 3 Apr 2024 17:05:59 -0700
Subject: [PATCH 19/39] addressing PR comments

---
 src/client/eppo-client.spec.ts | 45 +++++++++++++++-------------------
 src/client/eppo-client.ts      |  7 ++++++
 src/rules.spec.ts              |  2 +-
 src/rules.ts                   |  1 -
 4 files changed, 28 insertions(+), 27 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 6bf03f28..50e047e9 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -88,9 +88,7 @@ export async function init(configurationStore: IConfigurationStore) {
 }
 
 describe('EppoClient E2E test', () => {
-  const evaluator = new Evaluator();
   const storage = new TestConfigurationStore();
-  const globalClient = new EppoClient(evaluator, storage);
 
   beforeAll(async () => {
     mock.setup();
@@ -141,7 +139,6 @@ describe('EppoClient E2E test', () => {
 
   describe('error encountered', () => {
     let client: EppoClient;
-    const mockHooks = td.object<IAssignmentHooks>();
 
     beforeAll(() => {
       storage.setEntries({ [flagKey]: mockFlag });
@@ -157,39 +154,37 @@ describe('EppoClient E2E test', () => {
       td.reset();
     });
 
-    it('returns default value when graceful failure if error encounterd', async () => {
+    it('returns default value when graceful failure if error encountered', async () => {
       client.setIsGracefulFailureMode(true);
 
-      expect(client.getBoolAssignment('subject-identifer', flagKey, true, {})).toBe(true);
-      expect(client.getBoolAssignment('subject-identifer', flagKey, false, {})).toBe(false);
-      expect(client.getNumericAssignment('subject-identifer', flagKey, 1, {})).toBe(1);
-      expect(client.getNumericAssignment('subject-identifer', flagKey, 0, {})).toBe(0);
-      expect(client.getJSONAssignment('subject-identifer', flagKey, {}, {})).toEqual({});
-      expect(
-        client.getJSONAssignment('subject-identifer', flagKey, { hello: 'world' }, {}),
-      ).toEqual({ hello: 'world' });
-      expect(client.getStringAssignment('subject-identifer', flagKey, 'default', {})).toBe(
-        'default',
-      );
+      expect(client.getBoolAssignment('subject-identifer', flagKey, true)).toBe(true);
+      expect(client.getBoolAssignment('subject-identifer', flagKey, false)).toBe(false);
+      expect(client.getNumericAssignment('subject-identifer', flagKey, 1)).toBe(1);
+      expect(client.getNumericAssignment('subject-identifer', flagKey, 0)).toBe(0);
+      expect(client.getJSONAssignment('subject-identifer', flagKey, {})).toEqual({});
+      expect(client.getJSONAssignment('subject-identifer', flagKey, { hello: 'world' })).toEqual({
+        hello: 'world',
+      });
+      expect(client.getStringAssignment('subject-identifer', flagKey, 'default')).toBe('default');
     });
 
     it('throws error when graceful failure is false', async () => {
       client.setIsGracefulFailureMode(false);
 
       expect(() => {
-        client.getBoolAssignment('subject-identifer', flagKey, true, {});
+        client.getBoolAssignment('subject-identifer', flagKey, true);
       }).toThrow();
 
       expect(() => {
-        client.getJSONAssignment('subject-identifer', flagKey, {}, {});
+        client.getJSONAssignment('subject-identifer', flagKey, {});
       }).toThrow();
 
       expect(() => {
-        client.getNumericAssignment('subject-identifer', flagKey, 1, {});
+        client.getNumericAssignment('subject-identifer', flagKey, 1);
       }).toThrow();
 
       expect(() => {
-        client.getStringAssignment('subject-identifer', flagKey, 'default', {});
+        client.getStringAssignment('subject-identifer', flagKey, 'default');
       }).toThrow();
     });
   });
@@ -245,8 +240,6 @@ describe('EppoClient E2E test', () => {
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
-        `---- Test Case for ${flag} Experiment ----`;
-
         const evaluator = new Evaluator();
         const client = new EppoClient(evaluator, storage);
 
@@ -283,8 +276,6 @@ describe('EppoClient E2E test', () => {
 
   describe('UFC Obfuscated Test Cases', () => {
     const storage = new TestConfigurationStore();
-    const evaluator = new Evaluator();
-    const globalClient = new EppoClient(evaluator, storage);
 
     beforeAll(async () => {
       mock.setup();
@@ -303,8 +294,6 @@ describe('EppoClient E2E test', () => {
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
-        `---- Test Case for ${flag} Experiment ----`;
-
         const evaluator = new Evaluator();
         const client = new EppoClient(evaluator, storage);
 
@@ -329,6 +318,12 @@ describe('EppoClient E2E test', () => {
 
         for (const { subject, assignment } of assignments) {
           expect(assignment).toEqual(subject.assignment);
+          if (assignment !== subject.assignment) {
+            console.log(assignment, subject.assignment);
+            throw new Error(
+              `subject ${subject.subjectKey} was assigned ${assignment} when expected ${subject.assignment} for flag ${flag}`,
+            );
+          }
         }
       },
     );
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 1c7c1988..92d3db72 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -62,6 +62,13 @@ export interface IEppoClient {
     subjectAttributes?: Record<string, AttributeType>,
   ): number;
 
+  getIntegerAssignment(
+    subjectKey: string,
+    flagKey: string,
+    defaultValue: number,
+    subjectAttributes?: Record<string, AttributeType>,
+  ): number;
+
   getJSONAssignment(
     subjectKey: string,
     flagKey: string,
diff --git a/src/rules.spec.ts b/src/rules.spec.ts
index 491655b9..a2563e99 100644
--- a/src/rules.spec.ts
+++ b/src/rules.spec.ts
@@ -86,7 +86,7 @@ describe('rules', () => {
     };
     const subjectAttributes = {
       totalSales: 50,
-      version: '1.5.0',
+      version: '1.15.0',
       user_id: '12345',
       country: 'USA',
     };
diff --git a/src/rules.ts b/src/rules.ts
index 3e301d83..14077d43 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -178,7 +178,6 @@ function evaluateCondition(subjectAttributes: Record<string, any>, condition: Co
   return false;
 }
 
-// TODO: implement the obfuscated version of this function
 function evaluateObfuscatedCondition(
   hashedSubjectAttributes: Record<string, any>,
   condition: Condition,

From c650af5f2ffe24410f9cfec6e91cfc57d36201a3 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 10:49:11 -0700
Subject: [PATCH 20/39] update e2e test for more clarity

---
 src/client/eppo-client.spec.ts | 36 ++++++++++++++++++++++------------
 test/testHelpers.ts            |  2 +-
 2 files changed, 25 insertions(+), 13 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 50e047e9..ae8f4911 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -71,6 +71,28 @@ function getTestAssignments(
   return assignments;
 }
 
+function validateTestAssignments(
+  assignments: {
+    subject: SubjectTestCase;
+    assignment: string | boolean | number | object | null;
+  }[],
+  flag: string,
+) {
+  for (const { subject, assignment } of assignments) {
+    if (typeof assignment !== 'object') {
+      // the expect works well for objects, but this comparison does not
+      if (assignment !== subject.assignment) {
+        throw new Error(
+          `subject ${
+            subject.subjectKey
+          } was assigned ${assignment?.toString()} when expected ${subject.assignment?.toString()} for flag ${flag}`,
+        );
+      }
+    }
+    expect(subject.assignment).toEqual(assignment);
+  }
+}
+
 export async function init(configurationStore: IConfigurationStore) {
   const axiosInstance = axios.create({
     baseURL: 'http://127.0.0.1:4000',
@@ -267,9 +289,7 @@ describe('EppoClient E2E test', () => {
           false,
         );
 
-        for (const { subject, assignment } of assignments) {
-          expect(assignment).toEqual(subject.assignment);
-        }
+        validateTestAssignments(assignments, flag);
       },
     );
   });
@@ -316,15 +336,7 @@ describe('EppoClient E2E test', () => {
           true,
         );
 
-        for (const { subject, assignment } of assignments) {
-          expect(assignment).toEqual(subject.assignment);
-          if (assignment !== subject.assignment) {
-            console.log(assignment, subject.assignment);
-            throw new Error(
-              `subject ${subject.subjectKey} was assigned ${assignment} when expected ${subject.assignment} for flag ${flag}`,
-            );
-          }
-        }
+        validateTestAssignments(assignments, flag);
       },
     );
   });
diff --git a/test/testHelpers.ts b/test/testHelpers.ts
index ffa3501d..a0b47388 100644
--- a/test/testHelpers.ts
+++ b/test/testHelpers.ts
@@ -19,7 +19,7 @@ export enum ValueTestType {
 export interface SubjectTestCase {
   subjectKey: string;
   subjectAttributes: Record<string, AttributeType>;
-  assignment: string | null;
+  assignment: string | number | boolean | object;
 }
 
 export interface IAssignmentTestCase {

From bfd47f62e2fc3e2bebd9193ad01fdee5dd4eaed3 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 12:56:44 -0700
Subject: [PATCH 21/39] move obfuscated to client attribute, valueOf eppoValue

---
 src/client/eppo-client.spec.ts |  2 +-
 src/client/eppo-client.ts      | 64 ++++++++++++++++++----------------
 src/eppo_value.ts              |  5 +--
 3 files changed, 35 insertions(+), 36 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index ae8f4911..06f58ec0 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -315,7 +315,7 @@ describe('EppoClient E2E test', () => {
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
         const evaluator = new Evaluator();
-        const client = new EppoClient(evaluator, storage);
+        const client = new EppoClient(evaluator, storage, undefined, true);
 
         const typeAssignmentFunctions = {
           [VariationType.BOOLEAN]: client.getBoolAssignment.bind(client),
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 92d3db72..c9fe4a2b 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -55,14 +55,14 @@ export interface IEppoClient {
     subjectAttributes?: Record<string, AttributeType>,
   ): boolean;
 
-  getNumericAssignment(
+  getIntegerAssignment(
     subjectKey: string,
     flagKey: string,
     defaultValue: number,
     subjectAttributes?: Record<string, AttributeType>,
   ): number;
 
-  getIntegerAssignment(
+  getNumericAssignment(
     subjectKey: string,
     flagKey: string,
     defaultValue: number,
@@ -112,6 +112,7 @@ export default class EppoClient implements IEppoClient {
   private queuedEvents: IAssignmentEvent[] = [];
   private assignmentLogger: IAssignmentLogger | undefined;
   private isGracefulFailureMode = true;
+  private isObfuscated = false;
   private assignmentCache: AssignmentCache<Cacheable> | undefined;
   private configurationStore: IConfigurationStore;
   private configurationRequestParameters: FlagConfigurationRequestParameters | undefined;
@@ -122,10 +123,12 @@ export default class EppoClient implements IEppoClient {
     evaluator: Evaluator,
     configurationStore: IConfigurationStore,
     configurationRequestParameters?: FlagConfigurationRequestParameters,
+    obfuscated = false,
   ) {
     this.evaluator = evaluator;
     this.configurationStore = configurationStore;
     this.configurationRequestParameters = configurationRequestParameters;
+    this.isObfuscated = obfuscated;
   }
 
   public setConfigurationRequestParameters(
@@ -193,7 +196,6 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     defaultValue: string,
     subjectAttributes: Record<string, AttributeType> = {},
-    obfuscated = false,
   ): string {
     return (
       this.getAssignmentVariation(
@@ -201,7 +203,6 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         EppoValue.String(defaultValue),
         subjectAttributes,
-        obfuscated,
         VariationType.STRING,
       ).stringValue ?? defaultValue
     );
@@ -212,7 +213,6 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     defaultValue: boolean,
     subjectAttributes: Record<string, AttributeType> = {},
-    obfuscated = false,
   ): boolean {
     return (
       this.getAssignmentVariation(
@@ -220,18 +220,16 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         EppoValue.Bool(defaultValue),
         subjectAttributes,
-        obfuscated,
         VariationType.BOOLEAN,
       ).boolValue ?? defaultValue
     );
   }
 
-  getNumericAssignment(
+  getIntegerAssignment(
     subjectKey: string,
     flagKey: string,
     defaultValue: number,
     subjectAttributes?: Record<string, AttributeType>,
-    obfuscated = false,
   ): number {
     return (
       this.getAssignmentVariation(
@@ -239,18 +237,16 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         EppoValue.Numeric(defaultValue),
         subjectAttributes,
-        obfuscated,
-        VariationType.NUMERIC,
+        VariationType.INTEGER,
       ).numericValue ?? defaultValue
     );
   }
 
-  getIntegerAssignment(
+  getNumericAssignment(
     subjectKey: string,
     flagKey: string,
     defaultValue: number,
     subjectAttributes?: Record<string, AttributeType>,
-    obfuscated = false,
   ): number {
     return (
       this.getAssignmentVariation(
@@ -258,8 +254,7 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         EppoValue.Numeric(defaultValue),
         subjectAttributes,
-        obfuscated,
-        VariationType.INTEGER,
+        VariationType.NUMERIC,
       ).numericValue ?? defaultValue
     );
   }
@@ -269,7 +264,6 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     defaultValue: object,
     subjectAttributes: Record<string, AttributeType> = {},
-    obfuscated = false,
   ): object {
     return (
       this.getAssignmentVariation(
@@ -277,26 +271,16 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         EppoValue.JSON(defaultValue),
         subjectAttributes,
-        obfuscated,
         VariationType.JSON,
       ).objectValue ?? defaultValue
     );
   }
 
-  private rethrowIfNotGraceful(err: Error, defaultValue?: EppoValue): EppoValue {
-    if (this.isGracefulFailureMode) {
-      console.error(`[Eppo SDK] Error getting assignment: ${err.message}`);
-      return defaultValue ?? EppoValue.Null();
-    }
-    throw err;
-  }
-
   private getAssignmentVariation(
     subjectKey: string,
     flagKey: string,
     defaultValue: EppoValue,
     subjectAttributes: Record<string, AttributeType> = {},
-    obfuscated: boolean,
     expectedVariationType: VariationType,
   ): EppoValue {
     try {
@@ -305,19 +289,26 @@ export default class EppoClient implements IEppoClient {
         flagKey,
         subjectAttributes,
         expectedVariationType,
-        obfuscated,
       );
 
       if (!result.variation) {
         return defaultValue;
       }
 
-      return EppoValue.generateEppoValue(result.variation.value, expectedVariationType);
+      return EppoValue.valueOf(result.variation.value, expectedVariationType);
     } catch (error) {
       return this.rethrowIfNotGraceful(error, defaultValue);
     }
   }
 
+  private rethrowIfNotGraceful(err: Error, defaultValue?: EppoValue): EppoValue {
+    if (this.isGracefulFailureMode) {
+      console.error(`[Eppo SDK] Error getting assignment: ${err.message}`);
+      return defaultValue ?? EppoValue.Null();
+    }
+    throw err;
+  }
+
   /**
    * [Experimental] Get a detailed return of assignment for a particular subject and flag.
    *
@@ -336,12 +327,11 @@ export default class EppoClient implements IEppoClient {
     flagKey: string,
     subjectAttributes: Record<string, AttributeType> = {},
     expectedVariationType?: VariationType,
-    obfuscated = false,
   ): FlagEvaluation {
     validateNotBlank(subjectKey, 'Invalid argument: subjectKey cannot be blank');
     validateNotBlank(flagKey, 'Invalid argument: flagKey cannot be blank');
 
-    const flag: Flag = this.configurationStore.get(obfuscated ? getMD5Hash(flagKey) : flagKey);
+    const flag = this.getFlag(flagKey);
 
     if (flag === null) {
       console.warn(`[Eppo SDK] No assigned variation. Flag not found: ${flagKey}`);
@@ -361,8 +351,13 @@ export default class EppoClient implements IEppoClient {
       return noneResult(flagKey, subjectKey, subjectAttributes);
     }
 
-    const result = this.evaluator.evaluateFlag(flag, subjectKey, subjectAttributes, obfuscated);
-    if (obfuscated) {
+    const result = this.evaluator.evaluateFlag(
+      flag,
+      subjectKey,
+      subjectAttributes,
+      this.isObfuscated,
+    );
+    if (this.isObfuscated) {
       // flag.key is obfuscated, replace with requested flag key
       result.flagKey = flagKey;
     }
@@ -378,6 +373,13 @@ export default class EppoClient implements IEppoClient {
     return result;
   }
 
+  private getFlag(flagKey: string): Flag | null {
+    const flag: Flag = this.configurationStore.get(
+      this.isObfuscated ? getMD5Hash(flagKey) : flagKey,
+    );
+    return flag;
+  }
+
   private checkTypeMatch(expectedType?: VariationType, actualType?: VariationType): boolean {
     return expectedType === undefined || actualType === expectedType;
   }
diff --git a/src/eppo_value.ts b/src/eppo_value.ts
index 0ab9fdae..1beb3544 100644
--- a/src/eppo_value.ts
+++ b/src/eppo_value.ts
@@ -32,10 +32,7 @@ export class EppoValue {
     this.objectValue = objectValue;
   }
 
-  static generateEppoValue(
-    value: boolean | number | string | object,
-    valueType: VariationType,
-  ): EppoValue {
+  static valueOf(value: boolean | number | string | object, valueType: VariationType): EppoValue {
     if (value == null) {
       return EppoValue.Null();
     }

From bfc201ee9c819e811b7163c8ecd73b5edec5e361 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 13:22:37 -0700
Subject: [PATCH 22/39] add version metadata, add isInitialized

---
 .gitignore                     |  3 +++
 package.json                   |  1 +
 src/assignment-logger.ts       |  2 ++
 src/client/eppo-client.spec.ts | 11 ++++++++---
 src/client/eppo-client.ts      | 12 ++++++++++++
 src/configuration-store.ts     |  1 +
 6 files changed, 27 insertions(+), 3 deletions(-)

diff --git a/.gitignore b/.gitignore
index a93ba9cc..542eb287 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,6 @@ yarn-error.log
 
 test/data
 .vscode/settings.json
+
+# automatically generated version
+src/version.ts
\ No newline at end of file
diff --git a/package.json b/package.json
index 20db6832..b3d2543d 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
     "typecheck": "tsc",
     "test": "yarn test:unit",
     "test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
+    "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
     "obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC"
   },
   "jsdelivr": "dist/eppo-sdk.js",
diff --git a/src/assignment-logger.ts b/src/assignment-logger.ts
index 4c2cefcf..bda5c8ee 100644
--- a/src/assignment-logger.ts
+++ b/src/assignment-logger.ts
@@ -44,6 +44,8 @@ export interface IAssignmentEvent {
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   subjectAttributes: Record<string, any>;
   [propName: string]: unknown;
+
+  metaData?: Record<string, unknown>;
 }
 
 /**
diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 06f58ec0..6bafdca0 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -13,7 +13,6 @@ import {
   readAssignmentTestData,
   readMockUFCResponse,
 } from '../../test/testHelpers';
-import { IAssignmentHooks } from '../assignment-hooks';
 import { IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
 import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
@@ -31,6 +30,7 @@ const flagEndpoint = /flag_config\/v1\/config*/;
 
 class TestConfigurationStore implements IConfigurationStore {
   private store: Record<string, string> = {};
+  private _isInitialized = false;
 
   public get<T>(key: string): T {
     const rval = this.store[key];
@@ -41,11 +41,16 @@ class TestConfigurationStore implements IConfigurationStore {
     Object.entries(entries).forEach(([key, val]) => {
       this.store[key] = JSON.stringify(val);
     });
+    this._isInitialized = true;
   }
 
   public getKeys(): string[] {
     return Object.keys(this.store);
   }
+
+  public isInitialized(): boolean {
+    return this._isInitialized;
+  }
 }
 
 function getTestAssignments(
@@ -675,8 +680,8 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(evaluator, storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey, 0.0);
-      expect(variation).toBe(0.0);
+      let variation = client.getNumericAssignment(subject, flagKey, 123.4);
+      expect(variation).toBe(123.4);
       // have client fetch configurations
       await client.fetchFlagConfigurations();
       variation = client.getNumericAssignment(subject, flagKey, 0.0);
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index c9fe4a2b..113f8912 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -25,6 +25,7 @@ import { getMD5Hash } from '../obfuscation';
 import initPoller, { IPoller } from '../poller';
 import { AttributeType } from '../types';
 import { validateNotBlank } from '../validation';
+import { LIB_VERSION } from '../version';
 
 /**
  * Client for assigning experiment variations.
@@ -93,6 +94,8 @@ export interface IEppoClient {
   setIsGracefulFailureMode(gracefulFailureMode: boolean): void;
 
   getFlagKeys(): string[];
+
+  isInitialized(): boolean;
 }
 
 export type FlagConfigurationRequestParameters = {
@@ -394,6 +397,10 @@ export default class EppoClient implements IEppoClient {
     return this.configurationStore.getKeys();
   }
 
+  public isInitialized() {
+    return this.configurationStore.isInitialized();
+  }
+
   public setLogger(logger: IAssignmentLogger) {
     this.assignmentLogger = logger;
     this.flushQueuedEvents(); // log any events that may have been queued while initializing
@@ -444,6 +451,11 @@ export default class EppoClient implements IEppoClient {
       subject: result.subjectKey,
       timestamp: new Date().toISOString(),
       subjectAttributes: result.subjectAttributes,
+      metaData: {
+        obfuscated: this.isObfuscated,
+        sdkLanguage: 'javascript',
+        sdkLibVersion: LIB_VERSION,
+      },
     };
 
     if (
diff --git a/src/configuration-store.ts b/src/configuration-store.ts
index e2116dc1..e901c60c 100644
--- a/src/configuration-store.ts
+++ b/src/configuration-store.ts
@@ -2,4 +2,5 @@ export interface IConfigurationStore {
   get<T>(key: string): T;
   getKeys(): string[];
   setEntries<T>(entries: Record<string, T>): void;
+  isInitialized(): boolean;
 }

From 8e5dbeb53ccb72d299f3ca36cae1d4bbd60f1083 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 13:25:58 -0700
Subject: [PATCH 23/39] Add uninitialized test back in

---
 src/client/eppo-client.spec.ts | 12 +++++++-----
 1 file changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 6bafdca0..07b4d9d0 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -346,11 +346,13 @@ describe('EppoClient E2E test', () => {
     );
   });
 
-  // it('returns null if getStringAssignment was called for the subject before any RAC was loaded', () => {
-  //   expect(
-  //     globalClient.getStringAssignment(sessionOverrideSubject, sessionOverrideExperiment),
-  //   ).toEqual(null);
-  // });
+  it('returns null if getStringAssignment was called for the subject before any UFC was loaded', () => {
+    const localClient = new EppoClient(new Evaluator(), new TestConfigurationStore());
+    expect(localClient.getStringAssignment('subject-1', flagKey, 'hello world')).toEqual(
+      'hello world',
+    );
+    expect(localClient.isInitialized()).toBe(false);
+  });
 
   it('returns default value when key does not exist', async () => {
     const evaluator = new Evaluator();

From 7a3f392c0a40c891d5164470ce86a67f14fce726 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 14:49:13 -0700
Subject: [PATCH 24/39] add version update to pre-commit

---
 package.json | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/package.json b/package.json
index b3d2543d..cd3574dc 100644
--- a/package.json
+++ b/package.json
@@ -21,11 +21,10 @@
     "lint:fix": "eslint --fix '**/*.{ts,tsx}' --cache",
     "lint:fix-pre-commit": "eslint -c .eslintrc.pre-commit.js --fix '**/*.{ts,tsx}' --no-eslintrc --cache",
     "prepare": "make prepare",
-    "pre-commit": "lint-staged && tsc",
+    "pre-commit": "lint-staged && node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts && tsc",
     "typecheck": "tsc",
     "test": "yarn test:unit",
     "test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",
-    "prebuild": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts",
     "obfuscate-mock-ufc": "ts-node test/writeObfuscatedMockUFC"
   },
   "jsdelivr": "dist/eppo-sdk.js",

From 25d8ebff14c7ce10f5fddbc6cc695f52b789afc4 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 14:50:21 -0700
Subject: [PATCH 25/39] add version in pre-commit

---
 .gitignore     | 3 ---
 package.json   | 2 +-
 src/version.ts | 1 +
 3 files changed, 2 insertions(+), 4 deletions(-)
 create mode 100644 src/version.ts

diff --git a/.gitignore b/.gitignore
index 542eb287..a93ba9cc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,3 @@ yarn-error.log
 
 test/data
 .vscode/settings.json
-
-# automatically generated version
-src/version.ts
\ No newline at end of file
diff --git a/package.json b/package.json
index cd3574dc..11bafa7d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@eppo/js-client-sdk-common",
-  "version": "2.2.1",
+  "version": "3.0.0",
   "description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
   "main": "dist/index.js",
   "files": [
diff --git a/src/version.ts b/src/version.ts
new file mode 100644
index 00000000..25e2b759
--- /dev/null
+++ b/src/version.ts
@@ -0,0 +1 @@
+export const LIB_VERSION = "2.2.1";

From 834606e75f8e9a06846357732592b5cc3489969c Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 14:50:42 -0700
Subject: [PATCH 26/39] test

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 11bafa7d..75bc0119 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@eppo/js-client-sdk-common",
-  "version": "3.0.0",
+  "version": "2.0.0",
   "description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
   "main": "dist/index.js",
   "files": [

From 370d80a17bd7b75067d4f128dd6cf922be8711e9 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 14:51:13 -0700
Subject: [PATCH 27/39] update version

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 75bc0119..11bafa7d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@eppo/js-client-sdk-common",
-  "version": "2.0.0",
+  "version": "3.0.0",
   "description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
   "main": "dist/index.js",
   "files": [

From d058c4622adcab90e48d8df2d17617b374febe24 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 14:53:12 -0700
Subject: [PATCH 28/39] bump version

---
 src/version.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/version.ts b/src/version.ts
index 25e2b759..9a1f5e22 100644
--- a/src/version.ts
+++ b/src/version.ts
@@ -1 +1 @@
-export const LIB_VERSION = "2.2.1";
+export const LIB_VERSION = '3.0.0';

From cf991bce7421e5b6b0949ecaf6a1b73b9fd7aa45 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 4 Apr 2024 14:57:10 -0700
Subject: [PATCH 29/39] dont be fancy

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 94e2a9b7..3dac49d9 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
     "lint:fix": "eslint --fix '**/*.{ts,tsx}' --cache",
     "lint:fix-pre-commit": "eslint -c .eslintrc.pre-commit.js --fix '**/*.{ts,tsx}' --no-eslintrc --cache",
     "prepare": "make prepare",
-    "pre-commit": "lint-staged && node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts && tsc",
+    "pre-commit": "lint-staged && tsc",
     "typecheck": "tsc",
     "test": "yarn test:unit",
     "test:unit": "NODE_ENV=test jest '.*\\.spec\\.ts'",

From 6d063c4ea8d4e07f08574926f91a972e2790ce35 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 10 Apr 2024 13:41:21 -0700
Subject: [PATCH 30/39] remove evaluator as arg to eppo client constructor

---
 src/client/eppo-client.spec.ts | 42 ++++++++++++----------------------
 src/client/eppo-client.ts      |  3 +--
 2 files changed, 16 insertions(+), 29 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 07b4d9d0..98b54075 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -169,8 +169,7 @@ describe('EppoClient E2E test', () => {
 
     beforeAll(() => {
       storage.setEntries({ [flagKey]: mockFlag });
-      const evaluator = new Evaluator();
-      client = new EppoClient(evaluator, storage);
+      client = new EppoClient(storage);
 
       td.replace(EppoClient.prototype, 'getAssignmentDetail', function () {
         throw new Error('Mock test error');
@@ -224,8 +223,7 @@ describe('EppoClient E2E test', () => {
     it('Invokes logger for queued events', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const evaluator = new Evaluator();
-      const client = new EppoClient(evaluator, storage);
+      const client = new EppoClient(storage);
       client.getStringAssignment('subject-to-be-logged', flagKey, 'default-value');
       client.setLogger(mockLogger);
 
@@ -238,8 +236,7 @@ describe('EppoClient E2E test', () => {
     it('Does not log same queued event twice', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const evaluator = new Evaluator();
-      const client = new EppoClient(evaluator, storage);
+      const client = new EppoClient(storage);
 
       client.getStringAssignment('subject-to-be-logged', flagKey, 'default-value');
       client.setLogger(mockLogger);
@@ -252,8 +249,7 @@ describe('EppoClient E2E test', () => {
     it('Does not invoke logger for events that exceed queue size', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
-      const evaluator = new Evaluator();
-      const client = new EppoClient(evaluator, storage);
+      const client = new EppoClient(storage);
 
       for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
         client.getStringAssignment(`subject-to-be-logged-${i}`, flagKey, 'default-value');
@@ -267,8 +263,7 @@ describe('EppoClient E2E test', () => {
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
-        const evaluator = new Evaluator();
-        const client = new EppoClient(evaluator, storage);
+        const client = new EppoClient(storage);
 
         let assignments: {
           subject: SubjectTestCase;
@@ -319,8 +314,7 @@ describe('EppoClient E2E test', () => {
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
-        const evaluator = new Evaluator();
-        const client = new EppoClient(evaluator, storage, undefined, true);
+        const client = new EppoClient(storage, undefined, true);
 
         const typeAssignmentFunctions = {
           [VariationType.BOOLEAN]: client.getBoolAssignment.bind(client),
@@ -347,7 +341,7 @@ describe('EppoClient E2E test', () => {
   });
 
   it('returns null if getStringAssignment was called for the subject before any UFC was loaded', () => {
-    const localClient = new EppoClient(new Evaluator(), new TestConfigurationStore());
+    const localClient = new EppoClient(new TestConfigurationStore());
     expect(localClient.getStringAssignment('subject-1', flagKey, 'hello world')).toEqual(
       'hello world',
     );
@@ -355,8 +349,7 @@ describe('EppoClient E2E test', () => {
   });
 
   it('returns default value when key does not exist', async () => {
-    const evaluator = new Evaluator();
-    const client = new EppoClient(evaluator, storage);
+    const client = new EppoClient(storage);
 
     const nonExistentFlag = 'non-existent-flag';
 
@@ -372,8 +365,7 @@ describe('EppoClient E2E test', () => {
     const mockLogger = td.object<IAssignmentLogger>();
 
     storage.setEntries({ [flagKey]: mockFlag });
-    const evaluator = new Evaluator();
-    const client = new EppoClient(evaluator, storage);
+    const client = new EppoClient(storage);
     client.setLogger(mockLogger);
 
     const subjectAttributes = { foo: 3 };
@@ -399,8 +391,7 @@ describe('EppoClient E2E test', () => {
     td.when(mockLogger.logAssignment(td.matchers.anything())).thenThrow(new Error('logging error'));
 
     storage.setEntries({ [flagKey]: mockFlag });
-    const evaluator = new Evaluator();
-    const client = new EppoClient(evaluator, storage);
+    const client = new EppoClient(storage);
     client.setLogger(mockLogger);
 
     const subjectAttributes = { foo: 3 };
@@ -416,15 +407,13 @@ describe('EppoClient E2E test', () => {
 
   describe('assignment logging deduplication', () => {
     let client: EppoClient;
-    let evaluator: Evaluator;
     let mockLogger: IAssignmentLogger;
 
     beforeEach(() => {
       mockLogger = td.object<IAssignmentLogger>();
 
       storage.setEntries({ [flagKey]: mockFlag });
-      evaluator = new Evaluator();
-      client = new EppoClient(evaluator, storage);
+      client = new EppoClient(storage);
       client.setLogger(mockLogger);
     });
 
@@ -624,7 +613,6 @@ describe('EppoClient E2E test', () => {
     let requestConfiguration: FlagConfigurationRequestParameters;
     let mockServerResponseFunc: (res: MockResponse) => MockResponse;
 
-    const evaluator = new Evaluator();
     const ufcBody = JSON.stringify(readMockUFCResponse(MOCK_UFC_RESPONSE_FILE));
     const flagKey = 'numeric_flag';
     const subject = 'alice';
@@ -679,7 +667,7 @@ describe('EppoClient E2E test', () => {
     });
 
     it('Fetches initial configuration with parameters in constructor', async () => {
-      client = new EppoClient(evaluator, storage, requestConfiguration);
+      client = new EppoClient(storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
       let variation = client.getNumericAssignment(subject, flagKey, 123.4);
@@ -691,7 +679,7 @@ describe('EppoClient E2E test', () => {
     });
 
     it('Fetches initial configuration with parameters provided later', async () => {
-      client = new EppoClient(evaluator, storage);
+      client = new EppoClient(storage);
       client.setIsGracefulFailureMode(false);
       client.setConfigurationRequestParameters(requestConfiguration);
       // no configuration loaded
@@ -723,7 +711,7 @@ describe('EppoClient E2E test', () => {
         ...requestConfiguration,
         pollAfterSuccessfulInitialization,
       };
-      client = new EppoClient(evaluator, storage, requestConfiguration);
+      client = new EppoClient(storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
       let variation = client.getNumericAssignment(subject, flagKey, 0.0);
@@ -779,7 +767,7 @@ describe('EppoClient E2E test', () => {
         throwOnFailedInitialization,
         pollAfterFailedInitialization,
       };
-      client = new EppoClient(evaluator, storage, requestConfiguration);
+      client = new EppoClient(storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
       expect(client.getNumericAssignment(subject, flagKey, 0.0)).toBe(0.0);
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 113f8912..f8f634ad 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -123,12 +123,11 @@ export default class EppoClient implements IEppoClient {
   private evaluator: Evaluator;
 
   constructor(
-    evaluator: Evaluator,
     configurationStore: IConfigurationStore,
     configurationRequestParameters?: FlagConfigurationRequestParameters,
     obfuscated = false,
   ) {
-    this.evaluator = evaluator;
+    this.evaluator = new Evaluator();
     this.configurationStore = configurationStore;
     this.configurationRequestParameters = configurationRequestParameters;
     this.isObfuscated = obfuscated;

From a5cfe951e06a7707a01b873e8ff425f6841687b6 Mon Sep 17 00:00:00 2001
From: Leo Romanovsky <leoromanovsky@users.noreply.github.com>
Date: Fri, 5 Apr 2024 10:43:43 -0700
Subject: [PATCH 31/39] Replace lru-cache package with simple local
 implementation (FF-1876) (#47)

---
 package.json            |  1 -
 src/assignment-cache.ts |  7 ++---
 src/lru-cache.spec.ts   | 64 +++++++++++++++++++++++++++++++++++++++++
 src/lru-cache.ts        | 60 ++++++++++++++++++++++++++++++++++++++
 yarn.lock               |  5 ----
 5 files changed, 127 insertions(+), 10 deletions(-)
 create mode 100644 src/lru-cache.spec.ts
 create mode 100644 src/lru-cache.ts

diff --git a/package.json b/package.json
index 3dac49d9..b64f7961 100644
--- a/package.json
+++ b/package.json
@@ -65,7 +65,6 @@
   },
   "dependencies": {
     "axios": "^1.6.0",
-    "lru-cache": "^10.0.1",
     "md5": "^2.3.0",
     "pino": "^8.19.0",
     "semver": "^7.5.4",
diff --git a/src/assignment-cache.ts b/src/assignment-cache.ts
index 4f492caf..34f8fe9e 100644
--- a/src/assignment-cache.ts
+++ b/src/assignment-cache.ts
@@ -1,5 +1,4 @@
-import { LRUCache } from 'lru-cache';
-
+import { LRUCache } from './lru-cache';
 import { getMD5Hash } from './obfuscation';
 
 export interface AssignmentCacheKey {
@@ -69,8 +68,8 @@ export class NonExpiringInMemoryAssignmentCache extends AssignmentCache<Map<stri
  * multiple users. In this case, the cache size should be set to the maximum number
  * of users that can be active at the same time.
  */
-export class LRUInMemoryAssignmentCache extends AssignmentCache<LRUCache<string, string>> {
+export class LRUInMemoryAssignmentCache extends AssignmentCache<LRUCache> {
   constructor(maxSize: number) {
-    super(new LRUCache<string, string>({ max: maxSize }));
+    super(new LRUCache(maxSize));
   }
 }
diff --git a/src/lru-cache.spec.ts b/src/lru-cache.spec.ts
new file mode 100644
index 00000000..66b6b945
--- /dev/null
+++ b/src/lru-cache.spec.ts
@@ -0,0 +1,64 @@
+import { LRUCache } from './lru-cache';
+
+describe('LRUCache', () => {
+  let cache: LRUCache;
+
+  beforeEach(() => {
+    cache = new LRUCache(2);
+  });
+
+  it('should insert and retrieve a value', () => {
+    cache.set('a', 'apple');
+    expect(cache.get('a')).toBe('apple');
+  });
+
+  it('should return undefined for missing values', () => {
+    expect(cache.get('missing')).toBeUndefined();
+  });
+
+  it('should overwrite existing values', () => {
+    cache.set('a', 'apple');
+    cache.set('a', 'avocado');
+    expect(cache.get('a')).toBe('avocado');
+  });
+
+  it('should evict least recently used item', () => {
+    cache.set('a', 'apple');
+    cache.set('b', 'banana');
+    cache.set('c', 'cherry');
+    expect(cache.get('a')).toBeUndefined();
+    expect(cache.get('b')).toBe('banana');
+    expect(cache.get('c')).toBe('cherry');
+  });
+
+  it('should move recently used item to the end of the cache', () => {
+    cache.set('a', 'apple');
+    cache.set('b', 'banana');
+    cache.get('a'); // Access 'a' to make it recently used
+    cache.set('c', 'cherry');
+    expect(cache.get('a')).toBe('apple');
+    expect(cache.get('b')).toBeUndefined();
+    expect(cache.get('c')).toBe('cherry');
+  });
+
+  it('should check if a key exists', () => {
+    cache.set('a', 'apple');
+    expect(cache.has('a')).toBeTruthy();
+    expect(cache.has('b')).toBeFalsy();
+  });
+
+  it('should handle the cache capacity of zero', () => {
+    const zeroCache = new LRUCache(0);
+    zeroCache.set('a', 'apple');
+    expect(zeroCache.get('a')).toBeUndefined();
+  });
+
+  it('should handle the cache capacity of one', () => {
+    const oneCache = new LRUCache(1);
+    oneCache.set('a', 'apple');
+    expect(oneCache.get('a')).toBe('apple');
+    oneCache.set('b', 'banana');
+    expect(oneCache.get('a')).toBeUndefined();
+    expect(oneCache.get('b')).toBe('banana');
+  });
+});
diff --git a/src/lru-cache.ts b/src/lru-cache.ts
new file mode 100644
index 00000000..77c56ccb
--- /dev/null
+++ b/src/lru-cache.ts
@@ -0,0 +1,60 @@
+/**
+ * LRUCache is a cache that stores a maximum number of items.
+ *
+ * Items are removed from the cache when the cache is full.
+ *
+ * The cache is implemented as a Map, which is a built-in JavaScript object.
+ * The Map object holds key-value pairs and remembers the order of key-value pairs as they were inserted.
+ */
+export class LRUCache {
+  private capacity: number;
+  private cache: Map<string, string>;
+
+  constructor(capacity: number) {
+    this.capacity = capacity;
+    this.cache = new Map<string, string>();
+  }
+
+  has(key: string): boolean {
+    return this.cache.has(key);
+  }
+
+  get(key: string): string | undefined {
+    if (!this.cache.has(key)) {
+      return undefined;
+    }
+
+    const value = this.cache.get(key);
+
+    if (value !== undefined) {
+      // the delete and set operations are used together to ensure that the most recently accessed
+      // or added item is always considered the "newest" in terms of access order.
+      // This is crucial for maintaining the correct order of elements in the cache,
+      // which directly impacts which item is considered the least recently used (LRU) and
+      // thus eligible for eviction when the cache reaches its capacity.
+      this.cache.delete(key);
+      this.cache.set(key, value);
+    }
+
+    return value;
+  }
+
+  set(key: string, value: string): void {
+    if (this.capacity === 0) {
+      return;
+    }
+
+    if (this.cache.has(key)) {
+      this.cache.delete(key);
+    } else if (this.cache.size >= this.capacity) {
+      // To evict the least recently used (LRU) item, we retrieve the first key in the Map.
+      // This is possible because the Map object in JavaScript maintains the insertion order of the keys.
+      // Therefore, the first key represents the oldest entry, which is the least recently used item in our cache.
+      // We use Map.prototype.keys().next().value to obtain this oldest key and then delete it from the cache.
+      const oldestKey = this.cache.keys().next().value;
+      this.cache.delete(oldestKey);
+    }
+
+    this.cache.set(key, value);
+  }
+}
diff --git a/yarn.lock b/yarn.lock
index 266c9e47..7778ab6b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3116,11 +3116,6 @@ lodash@^4.17.21:
   resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
   integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
 
-lru-cache@^10.0.1:
-  version "10.0.1"
-  resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz"
-  integrity sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==
-
 lru-cache@^5.1.1:
   version "5.1.1"
   resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz"

From 5958d196d1d7d8bbe110f39e07c66577be1ca025 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Thu, 11 Apr 2024 21:16:19 -0700
Subject: [PATCH 32/39] update endpoint to match backend

---
 src/client/eppo-client.spec.ts      | 2 +-
 src/flag-configuration-requestor.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 98b54075..e71779a4 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -26,7 +26,7 @@ import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const packageJson = require('../../package.json');
 
-const flagEndpoint = /flag_config\/v1\/config*/;
+const flagEndpoint = /flag-config\/v1\/config*/;
 
 class TestConfigurationStore implements IConfigurationStore {
   private store: Record<string, string> = {};
diff --git a/src/flag-configuration-requestor.ts b/src/flag-configuration-requestor.ts
index 561cb899..6246db39 100644
--- a/src/flag-configuration-requestor.ts
+++ b/src/flag-configuration-requestor.ts
@@ -2,7 +2,7 @@ import { IConfigurationStore } from './configuration-store';
 import HttpClient from './http-client';
 import { Flag } from './interfaces';
 
-const UFC_ENDPOINT = '/flag_config/v1/config';
+const UFC_ENDPOINT = '/flag-config/v1/config';
 
 interface IUniversalFlagConfig {
   flags: Record<string, Flag>;

From ee3d43aa53897491f73c6e9b1a919c043afcbd36 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Mon, 15 Apr 2024 13:19:49 -0700
Subject: [PATCH 33/39] [ufc] add null operator and more fixes (#50)

---
 src/client/eppo-client.spec.ts |   2 -
 src/client/eppo-client.ts      |  31 ++++++++-
 src/evaluator.spec.ts          | 116 ++++++++++++++++++++++++++++++---
 src/evaluator.ts               |  26 +++++---
 src/interfaces.ts              |   6 +-
 src/rules.spec.ts              |  90 +++++++++++++++++++++++++
 src/rules.ts                   |  33 +++++++++-
 test/writeObfuscatedMockUFC.ts |  21 ++++--
 8 files changed, 292 insertions(+), 33 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index e71779a4..c7f7d6bd 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -16,7 +16,6 @@ import {
 import { IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
 import { MAX_EVENT_QUEUE_SIZE, POLL_INTERVAL_MS, POLL_JITTER_PCT } from '../constants';
-import { Evaluator } from '../evaluator';
 import FlagConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
@@ -301,7 +300,6 @@ describe('EppoClient E2E test', () => {
       mock.setup();
       mock.get(flagEndpoint, (_req, res) => {
         const ufc = readMockUFCResponse(OBFUSCATED_MOCK_UFC_RESPONSE_FILE);
-        console.log(ufc);
         return res.status(200).body(JSON.stringify(ufc));
       });
       await init(storage);
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index f8f634ad..394fd757 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -23,7 +23,7 @@ import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
 import { getMD5Hash } from '../obfuscation';
 import initPoller, { IPoller } from '../poller';
-import { AttributeType } from '../types';
+import { AttributeType, ValueType } from '../types';
 import { validateNotBlank } from '../validation';
 import { LIB_VERSION } from '../version';
 
@@ -364,6 +364,13 @@ export default class EppoClient implements IEppoClient {
       result.flagKey = flagKey;
     }
 
+    if (
+      result?.variation &&
+      !this.checkValueTypeMatch(expectedVariationType, result.variation.value)
+    ) {
+      return noneResult(flagKey, subjectKey, subjectAttributes);
+    }
+
     try {
       if (result?.doLog) {
         this.logAssignment(result);
@@ -386,6 +393,28 @@ export default class EppoClient implements IEppoClient {
     return expectedType === undefined || actualType === expectedType;
   }
 
+  private checkValueTypeMatch(expectedType: VariationType | undefined, value: ValueType): boolean {
+    if (expectedType == undefined) {
+      return true;
+    }
+
+    switch (expectedType) {
+      case VariationType.STRING:
+        return typeof value === 'string';
+      case VariationType.BOOLEAN:
+        return typeof value === 'boolean';
+      case VariationType.INTEGER:
+        return typeof value === 'number' && Number.isInteger(value);
+      case VariationType.NUMERIC:
+        return typeof value === 'number';
+      case VariationType.JSON:
+        // note: converting to object downstream
+        return typeof value === 'string';
+      default:
+        return false;
+    }
+  }
+
   public getFlagKeys() {
     /**
      * Returns a list of all flag keys that have been initialized.
diff --git a/src/evaluator.spec.ts b/src/evaluator.spec.ts
index c6c6eafd..88ab9732 100644
--- a/src/evaluator.spec.ts
+++ b/src/evaluator.spec.ts
@@ -1,7 +1,7 @@
-import { Evaluator, hashKey, isInShardRange } from './evaluator';
+import { Evaluator, hashKey, isInShardRange, matchesRules } from './evaluator';
 import { Flag, Variation, Shard, VariationType } from './interfaces';
 import { getMD5Hash } from './obfuscation';
-import { ObfuscatedOperatorType, OperatorType } from './rules';
+import { ObfuscatedOperatorType, OperatorType, Rule } from './rules';
 import { DeterministicSharder } from './sharders';
 
 describe('Evaluator', () => {
@@ -380,7 +380,6 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'first',
-          rules: [],
           splits: [
             {
               variationKey: 'a',
@@ -403,7 +402,6 @@ describe('Evaluator', () => {
         },
         {
           key: 'default',
-          rules: [],
           splits: [
             {
               variationKey: 'c',
@@ -454,8 +452,8 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'default',
-          startAt: new Date(now.getFullYear() + 1, 0, 1),
-          endAt: new Date(now.getFullYear() + 1, 1, 1),
+          startAt: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
+          endAt: new Date(now.getFullYear() + 1, 1, 1).toISOString(),
           rules: [],
           splits: [
             {
@@ -486,8 +484,8 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'default',
-          startAt: new Date(now.getFullYear() - 1, 0, 1),
-          endAt: new Date(now.getFullYear() + 1, 0, 1),
+          startAt: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
+          endAt: new Date(now.getFullYear() + 1, 0, 1).toISOString(),
           rules: [],
           splits: [
             {
@@ -518,8 +516,8 @@ describe('Evaluator', () => {
       allocations: [
         {
           key: 'default',
-          startAt: new Date(now.getFullYear() - 2, 0, 1),
-          endAt: new Date(now.getFullYear() - 1, 0, 1),
+          startAt: new Date(now.getFullYear() - 2, 0, 1).toISOString(),
+          endAt: new Date(now.getFullYear() - 1, 0, 1).toISOString(),
           rules: [],
           splits: [
             {
@@ -554,3 +552,101 @@ describe('Evaluator', () => {
     expect(isInShardRange(1, { start: 1, end: 1 })).toBeFalsy();
   });
 });
+
+describe('matchesRules', () => {
+  describe('matchesRules function', () => {
+    it('should return true when there are no rules', () => {
+      const rules: Rule[] = [];
+      const subjectAttributes = { id: 'test-subject' };
+      const obfuscated = false;
+      expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeTruthy();
+    });
+
+    it('should return true when a rule matches', () => {
+      const rules: Rule[] = [
+        {
+          conditions: [
+            {
+              attribute: 'age',
+              operator: OperatorType.GTE,
+              value: 18,
+            },
+          ],
+        },
+      ];
+      const subjectAttributes = { id: 'test-subject', age: 20 };
+      const obfuscated = false;
+      expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeTruthy();
+    });
+
+    it('should return true when one of two rules matches', () => {
+      const rules: Rule[] = [
+        {
+          conditions: [
+            {
+              attribute: 'age',
+              operator: OperatorType.GTE,
+              value: 18,
+            },
+          ],
+        },
+        {
+          conditions: [
+            {
+              attribute: 'age',
+              operator: OperatorType.LTE,
+              value: 10,
+            },
+          ],
+        },
+      ];
+      const subjectAttributes = { id: 'test-subject', age: 10 };
+      const obfuscated = false;
+      expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeTruthy();
+    });
+
+    it('should return true when null or rule is passed', () => {
+      const rules: Rule[] = [
+        {
+          conditions: [
+            {
+              attribute: 'age',
+              operator: OperatorType.IS_NULL,
+              value: true,
+            },
+          ],
+        },
+        {
+          conditions: [
+            {
+              attribute: 'age',
+              operator: OperatorType.GTE,
+              value: 20,
+            },
+          ],
+        },
+      ];
+      const obfuscated = false;
+      expect(matchesRules(rules, { id: 'test-subject', age: 20 }, obfuscated)).toBeTruthy();
+      expect(matchesRules(rules, { id: 'test-subject', age: 10 }, obfuscated)).toBeFalsy();
+      expect(matchesRules(rules, { id: 'test-subject', country: 'UK' }, obfuscated)).toBeTruthy();
+    });
+
+    it('should return false when no rules match', () => {
+      const rules: Rule[] = [
+        {
+          conditions: [
+            {
+              attribute: 'age',
+              operator: OperatorType.GTE,
+              value: 18,
+            },
+          ],
+        },
+      ];
+      const subjectAttributes = { id: 'test-subject', age: 16 };
+      const obfuscated = false;
+      expect(matchesRules(rules, subjectAttributes, obfuscated)).toBeFalsy();
+    });
+  });
+});
diff --git a/src/evaluator.ts b/src/evaluator.ts
index c952fef8..4fe9e818 100644
--- a/src/evaluator.ts
+++ b/src/evaluator.ts
@@ -1,11 +1,12 @@
 import { Flag, Shard, Range, Variation } from './interfaces';
-import { matchesRule } from './rules';
+import { Rule, matchesRule } from './rules';
 import { MD5Sharder, Sharder } from './sharders';
+import { SubjectAttributes } from './types';
 
 export interface FlagEvaluation {
   flagKey: string;
   subjectKey: string;
-  subjectAttributes: Record<string, string | number | boolean>;
+  subjectAttributes: SubjectAttributes;
   allocationKey: string | null;
   variation: Variation | null;
   extraLogging: Record<string, string>;
@@ -22,7 +23,7 @@ export class Evaluator {
   evaluateFlag(
     flag: Flag,
     subjectKey: string,
-    subjectAttributes: Record<string, string | number | boolean>,
+    subjectAttributes: SubjectAttributes,
     obfuscated: boolean,
   ): FlagEvaluation {
     if (!flag.enabled) {
@@ -31,14 +32,11 @@ export class Evaluator {
 
     const now = new Date();
     for (const allocation of flag.allocations) {
-      if (allocation.startAt && now < allocation.startAt) continue;
-      if (allocation.endAt && now > allocation.endAt) continue;
+      if (allocation.startAt && now < new Date(allocation.startAt)) continue;
+      if (allocation.endAt && now > new Date(allocation.endAt)) continue;
 
       if (
-        !allocation.rules.length ||
-        allocation.rules.some((rule) =>
-          matchesRule(rule, { id: subjectKey, ...subjectAttributes }, obfuscated),
-        )
+        matchesRules(allocation?.rules ?? [], { id: subjectKey, ...subjectAttributes }, obfuscated)
       ) {
         for (const split of allocation.splits) {
           if (
@@ -78,7 +76,7 @@ export function hashKey(salt: string, subjectKey: string): string {
 export function noneResult(
   flagKey: string,
   subjectKey: string,
-  subjectAttributes: Record<string, string | number | boolean>,
+  subjectAttributes: SubjectAttributes,
 ): FlagEvaluation {
   return {
     flagKey,
@@ -90,3 +88,11 @@ export function noneResult(
     doLog: false,
   };
 }
+
+export function matchesRules(
+  rules: Rule[],
+  subjectAttributes: SubjectAttributes,
+  obfuscated: boolean,
+): boolean {
+  return !rules.length || rules.some((rule) => matchesRule(rule, subjectAttributes, obfuscated));
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index d949bd72..e0f69187 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -31,9 +31,9 @@ export interface Split {
 
 export interface Allocation {
   key: string;
-  rules: Rule[];
-  startAt?: Date;
-  endAt?: Date;
+  rules?: Rule[];
+  startAt?: string; // ISO 8601
+  endAt?: string; // ISO 8601
   splits: Split[];
   doLog: boolean;
 }
diff --git a/src/rules.spec.ts b/src/rules.spec.ts
index a2563e99..167e7273 100644
--- a/src/rules.spec.ts
+++ b/src/rules.spec.ts
@@ -52,6 +52,26 @@ describe('rules', () => {
       ],
     };
 
+    const ruleWithNullCondition: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.IS_NULL,
+          attribute: 'country',
+          value: true,
+        },
+      ],
+    };
+
+    const ruleWithNotNullCondition: Rule = {
+      conditions: [
+        {
+          operator: OperatorType.IS_NULL,
+          attribute: 'country',
+          value: false,
+        },
+      ],
+    };
+
     const semverRule: Rule = {
       conditions: [
         {
@@ -136,6 +156,30 @@ describe('rules', () => {
       expect(matchesRule(ruleWithNotOneOfCondition, { country: null }, false)).toBe(false);
     });
 
+    it('should return true for a rule with IS_NULL condition when subject attribute is null', () => {
+      expect(matchesRule(ruleWithNullCondition, { country: null }, false)).toBe(true);
+    });
+
+    it('should return false for a rule with IS_NULL condition when subject attribute is not null', () => {
+      expect(matchesRule(ruleWithNullCondition, { country: 'UK' }, false)).toBe(false);
+    });
+
+    it('should return true for a rule with IS_NULL condition when subject attribute is missing', () => {
+      expect(matchesRule(ruleWithNullCondition, { age: 10 }, false)).toBe(true);
+    });
+
+    it('should return false for a rule with NOT IS_NULL condition when subject attribute is null', () => {
+      expect(matchesRule(ruleWithNotNullCondition, { country: null }, false)).toBe(false);
+    });
+
+    it('should return true for a rule with NOT IS_NULL condition when subject attribute is not null', () => {
+      expect(matchesRule(ruleWithNotNullCondition, { country: 'UK' }, false)).toBe(true);
+    });
+
+    it('should return false for a rule with NOT IS_NULL condition when subject attribute is missing', () => {
+      expect(matchesRule(ruleWithNotNullCondition, { age: 10 }, false)).toBe(false);
+    });
+
     it('should return true for a semver rule that matches the subject attributes', () => {
       expect(matchesRule(semverRule, subjectAttributes, false)).toBe(true);
     });
@@ -194,6 +238,26 @@ describe('rules', () => {
         ],
       };
 
+      const obfuscatedRuleWithNullCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.IS_NULL,
+            attribute: 'e909c2d7067ea37437cf97fe11d91bd0',
+            value: 'b326b5062b2f0e69046810717534cb09',
+          },
+        ],
+      };
+
+      const obfuscatedRuleWithNotNullCondition: Rule = {
+        conditions: [
+          {
+            operator: ObfuscatedOperatorType.IS_NULL,
+            attribute: 'e909c2d7067ea37437cf97fe11d91bd0',
+            value: '68934a3e9455fa72420237eb05902327',
+          },
+        ],
+      };
+
       const obfuscatedRuleWithGTECondition: Rule = {
         conditions: [
           {
@@ -278,6 +342,32 @@ describe('rules', () => {
         );
       });
 
+      it('should return true for a rule with IS_NULL condition when subject attribute is null', () => {
+        expect(matchesRule(obfuscatedRuleWithNullCondition, { country: null }, true)).toBe(true);
+      });
+
+      it('should return false for a rule with IS_NULL condition when subject attribute is not null', () => {
+        expect(matchesRule(obfuscatedRuleWithNullCondition, { country: 'UK' }, true)).toBe(false);
+      });
+
+      it('should return true for a rule with IS_NULL condition when subject attribute is missing', () => {
+        expect(matchesRule(obfuscatedRuleWithNullCondition, { age: 10 }, true)).toBe(true);
+      });
+
+      it('should return false for a rule with NOT IS_NULL condition when subject attribute is null', () => {
+        expect(matchesRule(obfuscatedRuleWithNotNullCondition, { country: null }, false)).toBe(
+          false,
+        );
+      });
+
+      it('should return true for a rule with NOT IS_NULL condition when subject attribute is not null', () => {
+        expect(matchesRule(obfuscatedRuleWithNotNullCondition, { country: 'UK' }, true)).toBe(true);
+      });
+
+      it('should return false for a rule with NOT IS_NULL condition when subject attribute is missing', () => {
+        expect(matchesRule(obfuscatedRuleWithNotNullCondition, { age: 10 }, true)).toBe(false);
+      });
+
       it('should return true for an obfuscated rule with GTE condition that matches the subject attributes', () => {
         expect(matchesRule(obfuscatedRuleWithGTECondition, { age: 18 }, true)).toBe(true);
       });
diff --git a/src/rules.ts b/src/rules.ts
index 14077d43..968c4c06 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -19,6 +19,7 @@ export enum OperatorType {
   LT = 'LT',
   ONE_OF = 'ONE_OF',
   NOT_ONE_OF = 'NOT_ONE_OF',
+  IS_NULL = 'IS_NULL',
 }
 
 export enum ObfuscatedOperatorType {
@@ -30,6 +31,7 @@ export enum ObfuscatedOperatorType {
   LT = 'c562607189d77eb9dfb707464c1e7b0b',
   ONE_OF = '27457ce369f2a74203396a35ef537c0b',
   NOT_ONE_OF = '602f5ee0b6e84fe29f43ab48b9e1addf',
+  IS_NULL = 'dbd9c38e0339e6c34bd48cafc59be388',
 }
 
 enum OperatorValueType {
@@ -91,13 +93,28 @@ type ObfuscatedNumericCondition = {
 
 type NumericCondition = StandardNumericCondition | ObfuscatedNumericCondition;
 
+type StandardNullCondition = {
+  operator: OperatorType.IS_NULL;
+  attribute: string;
+  value: boolean;
+};
+
+type ObfuscatedNullCondition = {
+  operator: ObfuscatedOperatorType.IS_NULL;
+  attribute: string;
+  value: string;
+};
+
+type NullCondition = StandardNullCondition | ObfuscatedNullCondition;
+
 export type Condition =
   | MatchesCondition
   | NotMatchesCondition
   | OneOfCondition
   | NotOneOfCondition
   | SemVerCondition
-  | NumericCondition;
+  | NumericCondition
+  | NullCondition;
 
 export interface Rule {
   conditions: Condition[];
@@ -139,6 +156,13 @@ function evaluateCondition(subjectAttributes: Record<string, any>, condition: Co
 
   const conditionValueType = targetingRuleConditionValuesTypesFromValues(condition.value);
 
+  if (condition.operator === OperatorType.IS_NULL) {
+    if (condition.value) {
+      return value === null || value === undefined;
+    }
+    return value !== null && value !== undefined;
+  }
+
   if (value != null) {
     switch (condition.operator) {
       case OperatorType.GTE:
@@ -185,6 +209,13 @@ function evaluateObfuscatedCondition(
   const value = hashedSubjectAttributes[condition.attribute];
   const conditionValueType = targetingRuleConditionValuesTypesFromValues(value);
 
+  if (condition.operator === ObfuscatedOperatorType.IS_NULL) {
+    if (condition.value === getMD5Hash('true')) {
+      return value === null || value === undefined;
+    }
+    return value !== null && value !== undefined;
+  }
+
   if (value != null) {
     switch (condition.operator) {
       case ObfuscatedOperatorType.GTE:
diff --git a/test/writeObfuscatedMockUFC.ts b/test/writeObfuscatedMockUFC.ts
index fc7cd102..7845f9d5 100644
--- a/test/writeObfuscatedMockUFC.ts
+++ b/test/writeObfuscatedMockUFC.ts
@@ -2,7 +2,7 @@ import * as fs from 'fs';
 
 import { Flag } from '../src/interfaces';
 import { encodeBase64, getMD5Hash } from '../src/obfuscation';
-import { Rule } from '../src/rules';
+import { Condition, Rule } from '../src/rules';
 
 import {
   MOCK_UFC_RESPONSE_FILE,
@@ -11,6 +11,18 @@ import {
   TEST_DATA_DIR,
 } from './testHelpers';
 
+function encodeRuleValue(condition: Condition) {
+  switch (condition.operator) {
+    case 'ONE_OF':
+    case 'NOT_ONE_OF':
+      return condition.value.map((value) => getMD5Hash(value.toLowerCase()));
+    case 'IS_NULL':
+      return getMD5Hash(`${condition.value}`);
+    default:
+      return encodeBase64(`${condition.value}`);
+  }
+}
+
 function obfuscateRule(rule: Rule) {
   return {
     ...rule,
@@ -18,10 +30,7 @@ function obfuscateRule(rule: Rule) {
       ...condition,
       attribute: getMD5Hash(condition.attribute),
       operator: getMD5Hash(condition.operator),
-      value:
-        ['ONE_OF', 'NOT_ONE_OF'].includes(condition.operator) && typeof condition.value === 'object'
-          ? condition.value.map((value) => getMD5Hash(value.toLowerCase()))
-          : encodeBase64(`${condition.value}`),
+      value: encodeRuleValue(condition),
     })),
   };
 }
@@ -32,7 +41,7 @@ function obfuscateFlag(flag: Flag) {
     key: getMD5Hash(flag.key),
     allocations: flag.allocations.map((allocation) => ({
       ...allocation,
-      rules: allocation.rules.map(obfuscateRule),
+      rules: allocation.rules?.map(obfuscateRule),
     })),
   };
 }

From 9915183c6b3ff86c281bf6accbed904d1b1fef25 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Mon, 15 Apr 2024 13:27:26 -0700
Subject: [PATCH 34/39] export test helpers

---
 src/client/eppo-client.spec.ts | 48 ++--------------------------------
 test/testHelpers.ts            | 45 +++++++++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 46 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index c7f7d6bd..6b78a5e5 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -10,8 +10,10 @@ import {
   MOCK_UFC_RESPONSE_FILE,
   OBFUSCATED_MOCK_UFC_RESPONSE_FILE,
   SubjectTestCase,
+  getTestAssignments,
   readAssignmentTestData,
   readMockUFCResponse,
+  validateTestAssignments,
 } from '../../test/testHelpers';
 import { IAssignmentLogger } from '../assignment-logger';
 import { IConfigurationStore } from '../configuration-store';
@@ -51,52 +53,6 @@ class TestConfigurationStore implements IConfigurationStore {
     return this._isInitialized;
   }
 }
-
-function getTestAssignments(
-  testCase: IAssignmentTestCase,
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  assignmentFn: any,
-  obfuscated = false,
-): { subject: SubjectTestCase; assignment: string | boolean | number | null | object }[] {
-  const assignments: {
-    subject: SubjectTestCase;
-    assignment: string | boolean | number | null | object;
-  }[] = [];
-  for (const subject of testCase.subjects) {
-    const assignment = assignmentFn(
-      subject.subjectKey,
-      testCase.flag,
-      testCase.defaultValue,
-      subject.subjectAttributes,
-      obfuscated,
-    );
-    assignments.push({ subject: subject, assignment: assignment });
-  }
-  return assignments;
-}
-
-function validateTestAssignments(
-  assignments: {
-    subject: SubjectTestCase;
-    assignment: string | boolean | number | object | null;
-  }[],
-  flag: string,
-) {
-  for (const { subject, assignment } of assignments) {
-    if (typeof assignment !== 'object') {
-      // the expect works well for objects, but this comparison does not
-      if (assignment !== subject.assignment) {
-        throw new Error(
-          `subject ${
-            subject.subjectKey
-          } was assigned ${assignment?.toString()} when expected ${subject.assignment?.toString()} for flag ${flag}`,
-        );
-      }
-    }
-    expect(subject.assignment).toEqual(assignment);
-  }
-}
-
 export async function init(configurationStore: IConfigurationStore) {
   const axiosInstance = axios.create({
     baseURL: 'http://127.0.0.1:4000',
diff --git a/test/testHelpers.ts b/test/testHelpers.ts
index a0b47388..4d9bc120 100644
--- a/test/testHelpers.ts
+++ b/test/testHelpers.ts
@@ -44,3 +44,48 @@ export function readAssignmentTestData(): IAssignmentTestCase[] {
   });
   return testCaseData;
 }
+
+export function getTestAssignments(
+  testCase: IAssignmentTestCase,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  assignmentFn: any,
+  obfuscated = false,
+): { subject: SubjectTestCase; assignment: string | boolean | number | null | object }[] {
+  const assignments: {
+    subject: SubjectTestCase;
+    assignment: string | boolean | number | null | object;
+  }[] = [];
+  for (const subject of testCase.subjects) {
+    const assignment = assignmentFn(
+      subject.subjectKey,
+      testCase.flag,
+      testCase.defaultValue,
+      subject.subjectAttributes,
+      obfuscated,
+    );
+    assignments.push({ subject: subject, assignment: assignment });
+  }
+  return assignments;
+}
+
+export function validateTestAssignments(
+  assignments: {
+    subject: SubjectTestCase;
+    assignment: string | boolean | number | object | null;
+  }[],
+  flag: string,
+) {
+  for (const { subject, assignment } of assignments) {
+    if (typeof assignment !== 'object') {
+      // the expect works well for objects, but this comparison does not
+      if (assignment !== subject.assignment) {
+        throw new Error(
+          `subject ${
+            subject.subjectKey
+          } was assigned ${assignment?.toString()} when expected ${subject.assignment?.toString()} for flag ${flag}`,
+        );
+      }
+    }
+    expect(subject.assignment).toEqual(assignment);
+  }
+}

From fd6e357f8b5410499b72697ca433723982b3e565 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Mon, 15 Apr 2024 15:17:56 -0700
Subject: [PATCH 35/39] export flag and variationtype

---
 src/index.ts | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/index.ts b/src/index.ts
index 4e6868c0..63b9160d 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,6 +6,7 @@ import { IConfigurationStore } from './configuration-store';
 import * as constants from './constants';
 import FlagConfigRequestor from './flag-configuration-requestor';
 import HttpClient from './http-client';
+import { Flag, VariationType } from './interfaces';
 import * as validation from './validation';
 
 export {
@@ -21,4 +22,6 @@ export {
   IConfigurationStore,
   AssignmentCache,
   FlagConfigurationRequestParameters,
+  Flag,
+  VariationType,
 };

From a28e473f551645cf61a507faa6f3faebd83afec1 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Mon, 15 Apr 2024 15:56:27 -0700
Subject: [PATCH 36/39] export attribute type

---
 src/index.ts | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/src/index.ts b/src/index.ts
index 63b9160d..871db7c1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,6 +7,7 @@ import * as constants from './constants';
 import FlagConfigRequestor from './flag-configuration-requestor';
 import HttpClient from './http-client';
 import { Flag, VariationType } from './interfaces';
+import { AttributeType } from './types';
 import * as validation from './validation';
 
 export {
@@ -24,4 +25,5 @@ export {
   FlagConfigurationRequestParameters,
   Flag,
   VariationType,
+  AttributeType,
 };

From 08e36c832c9ea25b6305f7deb65e8c542a0689d4 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Mon, 15 Apr 2024 15:57:37 -0700
Subject: [PATCH 37/39] export more types

---
 src/index.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/index.ts b/src/index.ts
index 871db7c1..b311fce5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -7,7 +7,7 @@ import * as constants from './constants';
 import FlagConfigRequestor from './flag-configuration-requestor';
 import HttpClient from './http-client';
 import { Flag, VariationType } from './interfaces';
-import { AttributeType } from './types';
+import { AttributeType, SubjectAttributes } from './types';
 import * as validation from './validation';
 
 export {
@@ -26,4 +26,5 @@ export {
   Flag,
   VariationType,
   AttributeType,
+  SubjectAttributes,
 };

From 7735cc6a6f80d31537f3aa4fa9257ed484b66b7c Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Wed, 17 Apr 2024 20:11:38 -0700
Subject: [PATCH 38/39] Update function signatures

---
 src/client/eppo-client.spec.ts | 132 +++++++++++++++++----------------
 src/client/eppo-client.ts      |  68 ++++++++---------
 test/testHelpers.ts            |   4 +-
 3 files changed, 104 insertions(+), 100 deletions(-)

diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index 6b78a5e5..bbddfddc 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -138,34 +138,38 @@ describe('EppoClient E2E test', () => {
     it('returns default value when graceful failure if error encountered', async () => {
       client.setIsGracefulFailureMode(true);
 
-      expect(client.getBoolAssignment('subject-identifer', flagKey, true)).toBe(true);
-      expect(client.getBoolAssignment('subject-identifer', flagKey, false)).toBe(false);
-      expect(client.getNumericAssignment('subject-identifer', flagKey, 1)).toBe(1);
-      expect(client.getNumericAssignment('subject-identifer', flagKey, 0)).toBe(0);
-      expect(client.getJSONAssignment('subject-identifer', flagKey, {})).toEqual({});
-      expect(client.getJSONAssignment('subject-identifer', flagKey, { hello: 'world' })).toEqual({
+      expect(client.getBoolAssignment(flagKey, 'subject-identifer', {}, true)).toBe(true);
+      expect(client.getBoolAssignment(flagKey, 'subject-identifer', {}, false)).toBe(false);
+      expect(client.getNumericAssignment(flagKey, 'subject-identifer', {}, 1)).toBe(1);
+      expect(client.getNumericAssignment(flagKey, 'subject-identifer', {}, 0)).toBe(0);
+      expect(client.getJSONAssignment(flagKey, 'subject-identifer', {}, {})).toEqual({});
+      expect(
+        client.getJSONAssignment(flagKey, 'subject-identifer', {}, { hello: 'world' }),
+      ).toEqual({
         hello: 'world',
       });
-      expect(client.getStringAssignment('subject-identifer', flagKey, 'default')).toBe('default');
+      expect(client.getStringAssignment(flagKey, 'subject-identifer', {}, 'default')).toBe(
+        'default',
+      );
     });
 
     it('throws error when graceful failure is false', async () => {
       client.setIsGracefulFailureMode(false);
 
       expect(() => {
-        client.getBoolAssignment('subject-identifer', flagKey, true);
+        client.getBoolAssignment(flagKey, 'subject-identifer', {}, true);
       }).toThrow();
 
       expect(() => {
-        client.getJSONAssignment('subject-identifer', flagKey, {});
+        client.getJSONAssignment(flagKey, 'subject-identifer', {}, {});
       }).toThrow();
 
       expect(() => {
-        client.getNumericAssignment('subject-identifer', flagKey, 1);
+        client.getNumericAssignment(flagKey, 'subject-identifer', {}, 1);
       }).toThrow();
 
       expect(() => {
-        client.getStringAssignment('subject-identifer', flagKey, 'default');
+        client.getStringAssignment(flagKey, 'subject-identifer', {}, 'default');
       }).toThrow();
     });
   });
@@ -179,7 +183,7 @@ describe('EppoClient E2E test', () => {
       const mockLogger = td.object<IAssignmentLogger>();
 
       const client = new EppoClient(storage);
-      client.getStringAssignment('subject-to-be-logged', flagKey, 'default-value');
+      client.getStringAssignment(flagKey, 'subject-to-be-logged', {}, 'default-value');
       client.setLogger(mockLogger);
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
@@ -193,7 +197,7 @@ describe('EppoClient E2E test', () => {
 
       const client = new EppoClient(storage);
 
-      client.getStringAssignment('subject-to-be-logged', flagKey, 'default-value');
+      client.getStringAssignment(flagKey, 'subject-to-be-logged', {}, 'default-value');
       client.setLogger(mockLogger);
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
 
@@ -207,7 +211,7 @@ describe('EppoClient E2E test', () => {
       const client = new EppoClient(storage);
 
       for (let i = 0; i < MAX_EVENT_QUEUE_SIZE + 100; i++) {
-        client.getStringAssignment(`subject-to-be-logged-${i}`, flagKey, 'default-value');
+        client.getStringAssignment(flagKey, `subject-to-be-logged-${i}`, {}, 'default-value');
       }
       client.setLogger(mockLogger);
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(MAX_EVENT_QUEUE_SIZE);
@@ -296,7 +300,7 @@ describe('EppoClient E2E test', () => {
 
   it('returns null if getStringAssignment was called for the subject before any UFC was loaded', () => {
     const localClient = new EppoClient(new TestConfigurationStore());
-    expect(localClient.getStringAssignment('subject-1', flagKey, 'hello world')).toEqual(
+    expect(localClient.getStringAssignment(flagKey, 'subject-1', {}, 'hello world')).toEqual(
       'hello world',
     );
     expect(localClient.isInitialized()).toBe(false);
@@ -307,10 +311,10 @@ describe('EppoClient E2E test', () => {
 
     const nonExistentFlag = 'non-existent-flag';
 
-    expect(client.getBoolAssignment('subject-identifer', nonExistentFlag, true, {})).toBe(true);
-    expect(client.getNumericAssignment('subject-identifer', nonExistentFlag, 1, {})).toBe(1);
-    expect(client.getJSONAssignment('subject-identifer', nonExistentFlag, {}, {})).toEqual({});
-    expect(client.getStringAssignment('subject-identifer', nonExistentFlag, 'default', {})).toBe(
+    expect(client.getBoolAssignment(nonExistentFlag, 'subject-identifer', {}, true)).toBe(true);
+    expect(client.getNumericAssignment(nonExistentFlag, 'subject-identifer', {}, 1)).toBe(1);
+    expect(client.getJSONAssignment(nonExistentFlag, 'subject-identifer', {}, {})).toEqual({});
+    expect(client.getStringAssignment(nonExistentFlag, 'subject-identifer', {}, 'default')).toBe(
       'default',
     );
   });
@@ -324,10 +328,10 @@ describe('EppoClient E2E test', () => {
 
     const subjectAttributes = { foo: 3 };
     const assignment = client.getStringAssignment(
-      'subject-10',
       flagKey,
-      'default',
+      'subject-10',
       subjectAttributes,
+      'default',
     );
 
     expect(assignment).toEqual(variationA.value);
@@ -350,10 +354,10 @@ describe('EppoClient E2E test', () => {
 
     const subjectAttributes = { foo: 3 };
     const assignment = client.getStringAssignment(
-      'subject-10',
       flagKey,
-      'default',
+      'subject-10',
       subjectAttributes,
+      'default',
     );
 
     expect(assignment).toEqual('variation-a');
@@ -374,8 +378,8 @@ describe('EppoClient E2E test', () => {
     it('logs duplicate assignments without an assignment cache', () => {
       client.disableAssignmentCache();
 
-      client.getStringAssignment('subject-10', flagKey, 'default');
-      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
 
       // call count should be 2 because there is no cache.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
@@ -384,8 +388,8 @@ describe('EppoClient E2E test', () => {
     it('does not log duplicate assignments', () => {
       client.useNonExpiringInMemoryAssignmentCache();
 
-      client.getStringAssignment('subject-10', flagKey, 'default');
-      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
 
       // call count should be 1 because the second call is a cache hit and not logged.
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(1);
@@ -394,15 +398,15 @@ describe('EppoClient E2E test', () => {
     it('logs assignment again after the lru cache is full', () => {
       client.useLRUInMemoryAssignmentCache(2);
 
-      client.getStringAssignment('subject-10', flagKey, 'default'); // logged
-      client.getStringAssignment('subject-10', flagKey, 'default'); // cached
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // logged
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // cached
 
-      client.getStringAssignment('subject-11', flagKey, 'default'); // logged
-      client.getStringAssignment('subject-11', flagKey, 'default'); // cached
+      client.getStringAssignment(flagKey, 'subject-11', {}, 'default'); // logged
+      client.getStringAssignment(flagKey, 'subject-11', {}, 'default'); // cached
 
-      client.getStringAssignment('subject-12', flagKey, 'default'); // cache evicted subject-10, logged
-      client.getStringAssignment('subject-10', flagKey, 'default'); // previously evicted, logged
-      client.getStringAssignment('subject-12', flagKey, 'default'); // cached
+      client.getStringAssignment(flagKey, 'subject-12', {}, 'default'); // cache evicted subject-10, logged
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // previously evicted, logged
+      client.getStringAssignment(flagKey, 'subject-12', {}, 'default'); // cached
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(4);
     });
@@ -414,8 +418,8 @@ describe('EppoClient E2E test', () => {
 
       client.setLogger(mockLogger);
 
-      client.getStringAssignment('subject-10', flagKey, 'default');
-      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
 
       // call count should be 2 because the first call had an exception
       // therefore we are not sure the logger was successful and try again.
@@ -437,15 +441,15 @@ describe('EppoClient E2E test', () => {
 
       client.useNonExpiringInMemoryAssignmentCache();
 
-      client.getStringAssignment('subject-10', flagKey, 'default');
-      client.getStringAssignment('subject-10', flagKey, 'default');
-      client.getStringAssignment('subject-10', 'flag-2', 'default');
-      client.getStringAssignment('subject-10', 'flag-2', 'default');
-      client.getStringAssignment('subject-10', 'flag-3', 'default');
-      client.getStringAssignment('subject-10', 'flag-3', 'default');
-      client.getStringAssignment('subject-10', flagKey, 'default');
-      client.getStringAssignment('subject-10', 'flag-2', 'default');
-      client.getStringAssignment('subject-10', 'flag-3', 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
+      client.getStringAssignment('flag-2', 'subject-10', {}, 'default');
+      client.getStringAssignment('flag-2', 'subject-10', {}, 'default');
+      client.getStringAssignment('flag-3', 'subject-10', {}, 'default');
+      client.getStringAssignment('flag-3', 'subject-10', {}, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
+      client.getStringAssignment('flag-2', 'subject-10', {}, 'default');
+      client.getStringAssignment('flag-3', 'subject-10', {}, 'default');
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(3);
     });
@@ -472,7 +476,7 @@ describe('EppoClient E2E test', () => {
           ],
         },
       });
-      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
 
       storage.setEntries({
         [flagKey]: {
@@ -492,7 +496,7 @@ describe('EppoClient E2E test', () => {
           ],
         },
       });
-      client.getStringAssignment('subject-10', flagKey, 'default');
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default');
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(2);
     });
 
@@ -502,8 +506,8 @@ describe('EppoClient E2E test', () => {
       // original configuration version
       storage.setEntries({ [flagKey]: mockFlag });
 
-      client.getStringAssignment('subject-10', flagKey, 'default'); // log this assignment
-      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // log this assignment
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // cache hit, don't log
 
       // change the variation
       storage.setEntries({
@@ -525,14 +529,14 @@ describe('EppoClient E2E test', () => {
         },
       });
 
-      client.getStringAssignment('subject-10', flagKey, 'default'); // log this assignment
-      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // log this assignment
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // cache hit, don't log
 
       // change the flag again, back to the original
       storage.setEntries({ [flagKey]: mockFlag });
 
-      client.getStringAssignment('subject-10', flagKey, 'default'); // important: log this assignment
-      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // important: log this assignment
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // cache hit, don't log
 
       // change the allocation
       storage.setEntries({
@@ -554,8 +558,8 @@ describe('EppoClient E2E test', () => {
         },
       });
 
-      client.getStringAssignment('subject-10', flagKey, 'default'); // log this assignment
-      client.getStringAssignment('subject-10', flagKey, 'default'); // cache hit, don't log
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // log this assignment
+      client.getStringAssignment(flagKey, 'subject-10', {}, 'default'); // cache hit, don't log
 
       expect(td.explain(mockLogger.logAssignment).callCount).toEqual(4);
     });
@@ -624,11 +628,11 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey, 123.4);
+      let variation = client.getNumericAssignment(flagKey, subject, {}, 123.4);
       expect(variation).toBe(123.4);
       // have client fetch configurations
       await client.fetchFlagConfigurations();
-      variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
       expect(variation).toBe(pi);
     });
 
@@ -637,11 +641,11 @@ describe('EppoClient E2E test', () => {
       client.setIsGracefulFailureMode(false);
       client.setConfigurationRequestParameters(requestConfiguration);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      let variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
       expect(variation).toBe(0.0);
       // have client fetch configurations
       await client.fetchFlagConfigurations();
-      variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
       expect(variation).toBe(pi);
     });
 
@@ -668,7 +672,7 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      let variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      let variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
       expect(variation).toBe(0.0);
 
       // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
@@ -680,7 +684,7 @@ describe('EppoClient E2E test', () => {
       // Await so it can finish its initialization before this test proceeds
       await fetchPromise;
 
-      variation = client.getNumericAssignment(subject, flagKey, 0.0);
+      variation = client.getNumericAssignment(flagKey, subject, {}, 0.0);
       expect(variation).toBe(pi);
       expect(callCount).toBe(2);
 
@@ -724,7 +728,7 @@ describe('EppoClient E2E test', () => {
       client = new EppoClient(storage, requestConfiguration);
       client.setIsGracefulFailureMode(false);
       // no configuration loaded
-      expect(client.getNumericAssignment(subject, flagKey, 0.0)).toBe(0.0);
+      expect(client.getNumericAssignment(flagKey, subject, {}, 0.0)).toBe(0.0);
 
       // By not awaiting (yet) only the first attempt should be fired off before test execution below resumes
       if (throwOnFailedInitialization) {
@@ -734,14 +738,14 @@ describe('EppoClient E2E test', () => {
       }
       expect(callCount).toBe(1);
       // still no configuration loaded
-      expect(client.getNumericAssignment(subject, flagKey, 10.0)).toBe(10.0);
+      expect(client.getNumericAssignment(flagKey, subject, {}, 10.0)).toBe(10.0);
 
       // Advance timers so a post-init poll can take place
       await jest.advanceTimersByTimeAsync(POLL_INTERVAL_MS * 1.5);
 
       // if pollAfterFailedInitialization = true, we will poll later and get a config, otherwise not
       expect(callCount).toBe(pollAfterFailedInitialization ? 2 : 1);
-      expect(client.getNumericAssignment(subject, flagKey, 0.0)).toBe(
+      expect(client.getNumericAssignment(flagKey, subject, {}, 0.0)).toBe(
         pollAfterFailedInitialization ? pi : 0.0,
       );
     });
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index 394fd757..ccc73cb4 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -43,38 +43,38 @@ export interface IEppoClient {
    * @public
    */
   getStringAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: string,
-    subjectAttributes?: Record<string, AttributeType>,
   ): string;
 
   getBoolAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: boolean,
-    subjectAttributes?: Record<string, AttributeType>,
   ): boolean;
 
   getIntegerAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: number,
-    subjectAttributes?: Record<string, AttributeType>,
   ): number;
 
   getNumericAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: number,
-    subjectAttributes?: Record<string, AttributeType>,
   ): number;
 
   getJSONAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: object,
-    subjectAttributes?: Record<string, AttributeType>,
   ): object;
 
   setLogger(logger: IAssignmentLogger): void;
@@ -194,101 +194,101 @@ export default class EppoClient implements IEppoClient {
   }
 
   public getStringAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: string,
-    subjectAttributes: Record<string, AttributeType> = {},
   ): string {
     return (
       this.getAssignmentVariation(
-        subjectKey,
         flagKey,
-        EppoValue.String(defaultValue),
+        subjectKey,
         subjectAttributes,
+        EppoValue.String(defaultValue),
         VariationType.STRING,
       ).stringValue ?? defaultValue
     );
   }
 
   getBoolAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: boolean,
-    subjectAttributes: Record<string, AttributeType> = {},
   ): boolean {
     return (
       this.getAssignmentVariation(
-        subjectKey,
         flagKey,
-        EppoValue.Bool(defaultValue),
+        subjectKey,
         subjectAttributes,
+        EppoValue.Bool(defaultValue),
         VariationType.BOOLEAN,
       ).boolValue ?? defaultValue
     );
   }
 
   getIntegerAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: number,
-    subjectAttributes?: Record<string, AttributeType>,
   ): number {
     return (
       this.getAssignmentVariation(
-        subjectKey,
         flagKey,
-        EppoValue.Numeric(defaultValue),
+        subjectKey,
         subjectAttributes,
+        EppoValue.Numeric(defaultValue),
         VariationType.INTEGER,
       ).numericValue ?? defaultValue
     );
   }
 
   getNumericAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: number,
-    subjectAttributes?: Record<string, AttributeType>,
   ): number {
     return (
       this.getAssignmentVariation(
-        subjectKey,
         flagKey,
-        EppoValue.Numeric(defaultValue),
+        subjectKey,
         subjectAttributes,
+        EppoValue.Numeric(defaultValue),
         VariationType.NUMERIC,
       ).numericValue ?? defaultValue
     );
   }
 
   public getJSONAssignment(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: object,
-    subjectAttributes: Record<string, AttributeType> = {},
   ): object {
     return (
       this.getAssignmentVariation(
-        subjectKey,
         flagKey,
-        EppoValue.JSON(defaultValue),
+        subjectKey,
         subjectAttributes,
+        EppoValue.JSON(defaultValue),
         VariationType.JSON,
       ).objectValue ?? defaultValue
     );
   }
 
   private getAssignmentVariation(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
+    subjectAttributes: Record<string, AttributeType>,
     defaultValue: EppoValue,
-    subjectAttributes: Record<string, AttributeType> = {},
     expectedVariationType: VariationType,
   ): EppoValue {
     try {
       const result = this.getAssignmentDetail(
-        subjectKey,
         flagKey,
+        subjectKey,
         subjectAttributes,
         expectedVariationType,
       );
@@ -325,8 +325,8 @@ export default class EppoClient implements IEppoClient {
    * @returns A detailed return of assignment for a particular subject and flag
    */
   public getAssignmentDetail(
-    subjectKey: string,
     flagKey: string,
+    subjectKey: string,
     subjectAttributes: Record<string, AttributeType> = {},
     expectedVariationType?: VariationType,
   ): FlagEvaluation {
diff --git a/test/testHelpers.ts b/test/testHelpers.ts
index 4d9bc120..d8b8b4cb 100644
--- a/test/testHelpers.ts
+++ b/test/testHelpers.ts
@@ -57,10 +57,10 @@ export function getTestAssignments(
   }[] = [];
   for (const subject of testCase.subjects) {
     const assignment = assignmentFn(
-      subject.subjectKey,
       testCase.flag,
-      testCase.defaultValue,
+      subject.subjectKey,
       subject.subjectAttributes,
+      testCase.defaultValue,
       obfuscated,
     );
     assignments.push({ subject: subject, assignment: assignment });

From 7b25f18d883c15b6cd5d22aad3a31b0afbe80749 Mon Sep 17 00:00:00 2001
From: Sven Schmit <sven@geteppo.com>
Date: Fri, 19 Apr 2024 16:50:27 -0700
Subject: [PATCH 39/39] [UFC] Update obfuscation decoding (#52)

---
 .gitignore                     |   2 +
 src/client/eppo-client.spec.ts |  10 +-
 src/client/eppo-client.ts      |  76 +++++++-------
 src/decoding.spec.ts           | 178 +++++++++++++++++++++++++++++++++
 src/decoding.ts                |  78 +++++++++++++++
 src/interfaces.ts              |  34 +++++++
 src/rules.spec.ts              |  16 +--
 src/rules.ts                   |  20 +---
 src/sharders.spec.ts           |  37 +++++++
 9 files changed, 391 insertions(+), 60 deletions(-)
 create mode 100644 src/decoding.spec.ts
 create mode 100644 src/decoding.ts
 create mode 100644 src/sharders.spec.ts

diff --git a/.gitignore b/.gitignore
index a93ba9cc..a7a205fb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,3 +10,5 @@ yarn-error.log
 
 test/data
 .vscode/settings.json
+
+.DS_Store
\ No newline at end of file
diff --git a/src/client/eppo-client.spec.ts b/src/client/eppo-client.spec.ts
index bbddfddc..4178b2c7 100644
--- a/src/client/eppo-client.spec.ts
+++ b/src/client/eppo-client.spec.ts
@@ -22,7 +22,7 @@ import FlagConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
 import { Flag, VariationType } from '../interfaces';
 
-import EppoClient, { FlagConfigurationRequestParameters } from './eppo-client';
+import EppoClient, { FlagConfigurationRequestParameters, checkTypeMatch } from './eppo-client';
 
 // eslint-disable-next-line @typescript-eslint/no-var-requires
 const packageJson = require('../../package.json');
@@ -218,11 +218,18 @@ describe('EppoClient E2E test', () => {
     });
   });
 
+  describe('check type match', () => {
+    it('returns false when types do not match', () => {
+      expect(checkTypeMatch(VariationType.JSON, VariationType.STRING)).toBe(false);
+    });
+  });
+
   describe('UFC General Test Cases', () => {
     it.each(readAssignmentTestData())(
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
         const client = new EppoClient(storage);
+        client.setIsGracefulFailureMode(false);
 
         let assignments: {
           subject: SubjectTestCase;
@@ -273,6 +280,7 @@ describe('EppoClient E2E test', () => {
       'test variation assignment splits',
       async ({ flag, variationType, defaultValue, subjects }: IAssignmentTestCase) => {
         const client = new EppoClient(storage, undefined, true);
+        client.setIsGracefulFailureMode(false);
 
         const typeAssignmentFunctions = {
           [VariationType.BOOLEAN]: client.getBoolAssignment.bind(client),
diff --git a/src/client/eppo-client.ts b/src/client/eppo-client.ts
index ccc73cb4..5b9163ab 100644
--- a/src/client/eppo-client.ts
+++ b/src/client/eppo-client.ts
@@ -16,11 +16,12 @@ import {
   MAX_EVENT_QUEUE_SIZE,
   POLL_INTERVAL_MS,
 } from '../constants';
+import { decodeFlag } from '../decoding';
 import { EppoValue } from '../eppo_value';
 import { Evaluator, FlagEvaluation, noneResult } from '../evaluator';
 import ExperimentConfigurationRequestor from '../flag-configuration-requestor';
 import HttpClient from '../http-client';
-import { Flag, VariationType } from '../interfaces';
+import { Flag, ObfuscatedFlag, VariationType } from '../interfaces';
 import { getMD5Hash } from '../obfuscation';
 import initPoller, { IPoller } from '../poller';
 import { AttributeType, ValueType } from '../types';
@@ -341,9 +342,9 @@ export default class EppoClient implements IEppoClient {
       return noneResult(flagKey, subjectKey, subjectAttributes);
     }
 
-    if (!this.checkTypeMatch(expectedVariationType, flag.variationType)) {
+    if (!checkTypeMatch(expectedVariationType, flag.variationType)) {
       throw new TypeError(
-        `Variation value does not have the correct type. Found: ${flag.variationType} != ${expectedVariationType}`,
+        `Variation value does not have the correct type. Found: ${flag.variationType} != ${expectedVariationType} for flag ${flagKey}`,
       );
     }
 
@@ -364,10 +365,7 @@ export default class EppoClient implements IEppoClient {
       result.flagKey = flagKey;
     }
 
-    if (
-      result?.variation &&
-      !this.checkValueTypeMatch(expectedVariationType, result.variation.value)
-    ) {
+    if (result?.variation && !checkValueTypeMatch(expectedVariationType, result.variation.value)) {
       return noneResult(flagKey, subjectKey, subjectAttributes);
     }
 
@@ -383,36 +381,15 @@ export default class EppoClient implements IEppoClient {
   }
 
   private getFlag(flagKey: string): Flag | null {
-    const flag: Flag = this.configurationStore.get(
-      this.isObfuscated ? getMD5Hash(flagKey) : flagKey,
-    );
-    return flag;
+    if (this.isObfuscated) {
+      return this.getObfuscatedFlag(flagKey);
+    }
+    return this.configurationStore.get(flagKey);
   }
 
-  private checkTypeMatch(expectedType?: VariationType, actualType?: VariationType): boolean {
-    return expectedType === undefined || actualType === expectedType;
-  }
-
-  private checkValueTypeMatch(expectedType: VariationType | undefined, value: ValueType): boolean {
-    if (expectedType == undefined) {
-      return true;
-    }
-
-    switch (expectedType) {
-      case VariationType.STRING:
-        return typeof value === 'string';
-      case VariationType.BOOLEAN:
-        return typeof value === 'boolean';
-      case VariationType.INTEGER:
-        return typeof value === 'number' && Number.isInteger(value);
-      case VariationType.NUMERIC:
-        return typeof value === 'number';
-      case VariationType.JSON:
-        // note: converting to object downstream
-        return typeof value === 'string';
-      default:
-        return false;
-    }
+  private getObfuscatedFlag(flagKey: string): Flag | null {
+    const flag: ObfuscatedFlag | null = this.configurationStore.get(getMD5Hash(flagKey));
+    return flag ? decodeFlag(flag) : null;
   }
 
   public getFlagKeys() {
@@ -517,3 +494,32 @@ export default class EppoClient implements IEppoClient {
     }
   }
 }
+
+export function checkTypeMatch(expectedType?: VariationType, actualType?: VariationType): boolean {
+  return expectedType === undefined || actualType === expectedType;
+}
+
+export function checkValueTypeMatch(
+  expectedType: VariationType | undefined,
+  value: ValueType,
+): boolean {
+  if (expectedType == undefined) {
+    return true;
+  }
+
+  switch (expectedType) {
+    case VariationType.STRING:
+      return typeof value === 'string';
+    case VariationType.BOOLEAN:
+      return typeof value === 'boolean';
+    case VariationType.INTEGER:
+      return typeof value === 'number' && Number.isInteger(value);
+    case VariationType.NUMERIC:
+      return typeof value === 'number';
+    case VariationType.JSON:
+      // note: converting to object downstream
+      return typeof value === 'string';
+    default:
+      return false;
+  }
+}
diff --git a/src/decoding.spec.ts b/src/decoding.spec.ts
new file mode 100644
index 00000000..83bd6661
--- /dev/null
+++ b/src/decoding.spec.ts
@@ -0,0 +1,178 @@
+import { decodeAllocation, decodeSplit, decodeValue, decodeVariations } from './decoding';
+import { VariationType, ObfuscatedVariation, Variation } from './interfaces';
+
+describe('decoding', () => {
+  describe('decodeVariations', () => {
+    it('should correctly decode variations', () => {
+      const encodedVariations: Record<string, ObfuscatedVariation> = {
+        'Y29udHJvbA==': {
+          key: 'Y29udHJvbA==',
+          value: 'MA==',
+        },
+        dHJlYXRtZW50: {
+          key: 'dHJlYXRtZW50',
+          value: 'MQ==',
+        },
+      };
+
+      const expectedVariations: Record<string, Variation> = {
+        control: {
+          key: 'control',
+          value: 0,
+        },
+        treatment: {
+          key: 'treatment',
+          value: 1,
+        },
+      };
+
+      expect(decodeVariations(encodedVariations, VariationType.INTEGER)).toEqual(
+        expectedVariations,
+      );
+    });
+  });
+
+  describe('decodeValue', () => {
+    it('should correctly decode string values', () => {
+      expect(decodeValue('Y29udHJvbA==', VariationType.STRING)).toEqual('control');
+    });
+    it('should correctly decode integer values', () => {
+      expect(decodeValue('NDI=', VariationType.INTEGER)).toEqual(42);
+    });
+    it('should correctly decode numeric values', () => {
+      expect(decodeValue('My4xNDE1OTI2NTM1OQ==', VariationType.NUMERIC)).toEqual(3.14159265359);
+    });
+    it('should correctly decode "true" boolean values', () => {
+      expect(decodeValue('dHJ1ZQ==', VariationType.BOOLEAN)).toEqual(true);
+    });
+    it('should correctly decode "false" boolean values', () => {
+      expect(decodeValue('ZmFsc2U=', VariationType.BOOLEAN)).toEqual(false);
+    });
+
+    it('should correctly decode JSON values', () => {
+      expect(
+        JSON.parse(
+          decodeValue(
+            'eyJoZWxsbyI6ICJ3b3JsZCIsICJieWUiOiAid29ybGQifQ==',
+            VariationType.JSON,
+          ) as string,
+        ),
+      ).toEqual({ hello: 'world', bye: 'world' });
+    });
+  });
+  describe('decodeSplit', () => {
+    it('should correctly decode split without extra logging', () => {
+      const obfuscatedSplit = {
+        shards: [
+          {
+            salt: 'c2FsdA==',
+            ranges: [
+              {
+                start: 0,
+                end: 100,
+              },
+            ],
+          },
+        ],
+        variationKey: 'Y29udHJvbA==',
+      };
+
+      const expectedSplit = {
+        shards: [
+          {
+            salt: 'salt',
+            ranges: [
+              {
+                start: 0,
+                end: 100,
+              },
+            ],
+          },
+        ],
+        variationKey: 'control',
+      };
+
+      expect(decodeSplit(obfuscatedSplit)).toEqual(expectedSplit);
+    });
+    it('should correctly decode split with extra logging', () => {
+      const obfuscatedSplit = {
+        shards: [
+          {
+            salt: 'c2FsdA==',
+            ranges: [
+              {
+                start: 0,
+                end: 100,
+              },
+            ],
+          },
+        ],
+        variationKey: 'Y29udHJvbA==',
+        extraLogging: { 'aGVsbG8=': 'd29ybGQ=', Ynll: 'd29ybGQ=' },
+      };
+
+      const expectedSplit = {
+        shards: [
+          {
+            salt: 'salt',
+            ranges: [
+              {
+                start: 0,
+                end: 100,
+              },
+            ],
+          },
+        ],
+        variationKey: 'control',
+        extraLogging: {
+          hello: 'world',
+          bye: 'world',
+        },
+      };
+
+      expect(decodeSplit(obfuscatedSplit)).toEqual(expectedSplit);
+    });
+  });
+
+  describe('decodeAllocation', () => {
+    it('should correctly decode allocation without startAt and endAt', () => {
+      const obfuscatedAllocation = {
+        key: 'ZXhwZXJpbWVudA==',
+        rules: [],
+        splits: [], // tested in decodeSplit
+        doLog: true,
+      };
+
+      const expectedAllocation = {
+        key: 'experiment',
+        rules: [],
+        splits: [],
+        doLog: true,
+      };
+
+      expect(decodeAllocation(obfuscatedAllocation)).toEqual(expectedAllocation);
+    });
+
+    it('should correctly decode allocation with startAt and endAt', () => {
+      const obfuscatedAllocation = {
+        key: 'ZXhwZXJpbWVudA==',
+        startAt: 'MjAyMC0wNC0wMVQxODo1ODo1NS44Mjla',
+        endAt: 'MjAyNS0wNy0yOVQwOTowMDoxMy4yMDVa',
+        rules: [],
+        splits: [], // tested in decodeSplit
+        doLog: true,
+      };
+
+      const expectedAllocation = {
+        key: 'experiment',
+        rules: [],
+        splits: [],
+        doLog: true,
+        startAt: '2020-04-01T18:58:55.829Z',
+        endAt: '2025-07-29T09:00:13.205Z',
+      };
+
+      expect(decodeAllocation(obfuscatedAllocation)).toEqual(expectedAllocation);
+    });
+  });
+});
diff --git a/src/decoding.ts b/src/decoding.ts
new file mode 100644
index 00000000..52477043
--- /dev/null
+++ b/src/decoding.ts
@@ -0,0 +1,78 @@
+import {
+  ObfuscatedFlag,
+  Flag,
+  ObfuscatedVariation,
+  VariationType,
+  Variation,
+  ObfuscatedAllocation,
+  Allocation,
+  Split,
+  Shard,
+  ObfuscatedSplit,
+} from './interfaces';
+import { decodeBase64 } from './obfuscation';
+
+export function decodeFlag(flag: ObfuscatedFlag): Flag {
+  return {
+    ...flag,
+    variations: decodeVariations(flag.variations, flag.variationType),
+    allocations: flag.allocations.map(decodeAllocation),
+  };
+}
+
+export function decodeVariations(
+  variations: Record<string, ObfuscatedVariation>,
+  variationType: VariationType,
+): Record<string, Variation> {
+  return Object.fromEntries(
+    Object.entries(variations).map(([, variation]) => {
+      const decodedKey = decodeBase64(variation.key);
+      return [decodedKey, { key: decodedKey, value: decodeValue(variation.value, variationType) }];
+    }),
+  );
+}
+
+export function decodeValue(encodedValue: string, type: VariationType): string | number | boolean {
+  switch (type) {
+    case VariationType.INTEGER:
+    case VariationType.NUMERIC:
+      return Number(decodeBase64(encodedValue));
+    case VariationType.BOOLEAN:
+      return decodeBase64(encodedValue) === 'true';
+    default:
+      return decodeBase64(encodedValue);
+  }
+}
+
+export function decodeAllocation(allocation: ObfuscatedAllocation): Allocation {
+  return {
+    ...allocation,
+    key: decodeBase64(allocation.key),
+    splits: allocation.splits.map(decodeSplit),
+    startAt: allocation.startAt
+      ? new Date(decodeBase64(allocation.startAt)).toISOString()
+      : undefined,
+    endAt: allocation.endAt ? new Date(decodeBase64(allocation.endAt)).toISOString() : undefined,
+  };
+}
+
+export function decodeSplit(split: ObfuscatedSplit): Split {
+  return {
+    extraLogging: split.extraLogging ? decodeObject(split.extraLogging) : undefined,
+    variationKey: decodeBase64(split.variationKey),
+    shards: split.shards.map(decodeShard),
+  };
+}
+
+export function decodeShard(shard: Shard): Shard {
+  return {
+    ...shard,
+    salt: decodeBase64(shard.salt),
+  };
+}
+
+export function decodeObject(obj: Record<string, string>): Record<string, string> {
+  return Object.fromEntries(
+    Object.entries(obj).map(([key, value]) => [decodeBase64(key), decodeBase64(value)]),
+  );
+}
diff --git a/src/interfaces.ts b/src/interfaces.ts
index e0f69187..9f68da4e 100644
--- a/src/interfaces.ts
+++ b/src/interfaces.ts
@@ -46,3 +46,37 @@ export interface Flag {
   allocations: Allocation[];
   totalShards: number;
 }
+
+export interface ObfuscatedFlag {
+  key: string;
+  enabled: boolean;
+  variationType: VariationType;
+  variations: Record<string, ObfuscatedVariation>;
+  allocations: ObfuscatedAllocation[];
+  totalShards: number;
+}
+
+export interface ObfuscatedVariation {
+  key: string;
+  value: string;
+}
+
+export interface ObfuscatedAllocation {
+  key: string;
+  rules?: Rule[];
+  startAt?: string; // ISO 8601
+  endAt?: string; // ISO 8601
+  splits: ObfuscatedSplit[];
+  doLog: boolean;
+}
+
+export interface ObfuscatedSplit {
+  shards: ObfuscatedShard[];
+  variationKey: string;
+  extraLogging?: Record<string, string>;
+}
+
+export interface ObfuscatedShard {
+  salt: string;
+  ranges: Range[];
+}
diff --git a/src/rules.spec.ts b/src/rules.spec.ts
index 167e7273..6c0e8b0b 100644
--- a/src/rules.spec.ts
+++ b/src/rules.spec.ts
@@ -216,10 +216,10 @@ describe('rules', () => {
             operator: ObfuscatedOperatorType.ONE_OF,
             attribute: 'e909c2d7067ea37437cf97fe11d91bd0', // getMD5Hash('country')
             value: [
-              'ada53304c5b9e4a839615b6e8f908eb6',
-              'c2aadac2ca30ca8aadfbe331ae180d28',
-              '4edfc924721abb774d5447bade86ea5d',
-            ], // ['usa', 'canada', 'mexico'].map(getMD5Hash)
+              'f75d91cdd36b85cc4a8dfeca4f24fa14',
+              '445d337b5cd5de476f99333df6b0c2a7',
+              '8dbb07a18d46f63d8b3c8994d5ccc351',
+            ], // ['USA', 'Canada', 'Mexico'].map(getMD5Hash)
           },
         ],
       };
@@ -230,10 +230,10 @@ describe('rules', () => {
             operator: ObfuscatedOperatorType.NOT_ONE_OF,
             attribute: 'e909c2d7067ea37437cf97fe11d91bd0', // getMD5Hash('country')
             value: [
-              'ada53304c5b9e4a839615b6e8f908eb6',
-              'c2aadac2ca30ca8aadfbe331ae180d28',
-              '4edfc924721abb774d5447bade86ea5d',
-            ], // ['usa', 'canada', 'mexico'].map(getMD5Hash)
+              'f75d91cdd36b85cc4a8dfeca4f24fa14',
+              '445d337b5cd5de476f99333df6b0c2a7',
+              '8dbb07a18d46f63d8b3c8994d5ccc351',
+            ], // ['USA', 'Canada', 'Mexico'].map(getMD5Hash)
           },
         ],
       };
diff --git a/src/rules.ts b/src/rules.ts
index 968c4c06..2c6ec446 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -188,15 +188,9 @@ function evaluateCondition(subjectAttributes: Record<string, any>, condition: Co
       case OperatorType.MATCHES:
         return new RegExp(condition.value as string).test(value as string);
       case OperatorType.ONE_OF:
-        return isOneOf(
-          value.toString().toLowerCase(),
-          condition.value.map((value: string) => value.toLowerCase()),
-        );
+        return isOneOf(value.toString(), condition.value);
       case OperatorType.NOT_ONE_OF:
-        return isNotOneOf(
-          value.toString().toLowerCase(),
-          condition.value.map((value: string) => value.toLowerCase()),
-        );
+        return isNotOneOf(value.toString(), condition.value);
     }
   }
   return false;
@@ -259,15 +253,9 @@ function evaluateObfuscatedCondition(
       case ObfuscatedOperatorType.NOT_MATCHES:
         return !new RegExp(decodeBase64(condition.value as string)).test(value as string);
       case ObfuscatedOperatorType.ONE_OF:
-        return isOneOf(
-          getMD5Hash(value.toString().toLowerCase()),
-          condition.value.map((value: string) => value.toLowerCase()),
-        );
+        return isOneOf(getMD5Hash(value.toString()), condition.value);
       case ObfuscatedOperatorType.NOT_ONE_OF:
-        return isNotOneOf(
-          getMD5Hash(value.toString().toLowerCase()),
-          condition.value.map((value: string) => value.toLowerCase()),
-        );
+        return isNotOneOf(getMD5Hash(value.toString()), condition.value);
     }
   }
   return false;
diff --git a/src/sharders.spec.ts b/src/sharders.spec.ts
new file mode 100644
index 00000000..f3074520
--- /dev/null
+++ b/src/sharders.spec.ts
@@ -0,0 +1,37 @@
+import { MD5Sharder, DeterministicSharder } from './sharders';
+
+describe('Sharders', () => {
+  describe('MD5Sharder', () => {
+    it('should correctly calculate shard for given input and total shards', () => {
+      const sharder = new MD5Sharder();
+      const inputs: [string, number][] = [
+        ['test-input', 5619],
+        ['alice', 3170],
+        ['bob', 7420],
+        ['charlie', 7497],
+      ];
+      const totalShards = 10000;
+      inputs.forEach(([input, expectedShard]) => {
+        expect(sharder.getShard(input, totalShards)).toEqual(expectedShard);
+      });
+    });
+  });
+
+  describe('DeterministicSharder', () => {
+    it('should return the shard from the lookup table if present', () => {
+      const lookup = { 'test-input': 5 };
+      const sharder = new DeterministicSharder(lookup);
+      const input = 'test-input';
+      const totalShards = 10; // totalShards is ignored in DeterministicSharder
+      expect(sharder.getShard(input, totalShards)).toEqual(5);
+    });
+
+    it('should return 0 if the input is not present in the lookup table', () => {
+      const lookup = { 'some-other-input': 7 };
+      const sharder = new DeterministicSharder(lookup);
+      const input = 'test-input-not-in-lookup';
+      const totalShards = 10; // totalShards is ignored in DeterministicSharder
+      expect(sharder.getShard(input, totalShards)).toEqual(0);
+    });
+  });
+});