From 0d5bdfb3de18f847864b6baa2eb4400e23be0976 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Thu, 24 Sep 2026 21:11:30 +0200 Subject: [PATCH 1/4] fix(flag-evaluation): return structured value diagnostics --- .changeset/structured-evaluation-failures.md | 5 + packages/flag-evaluation/src/index.ts | 179 ++++++++++++++++--- packages/flag-evaluation/test/index.test.ts | 156 ++++++++++++++++ 3 files changed, 312 insertions(+), 28 deletions(-) create mode 100644 .changeset/structured-evaluation-failures.md diff --git a/.changeset/structured-evaluation-failures.md b/.changeset/structured-evaluation-failures.md new file mode 100644 index 000000000..bd6117126 --- /dev/null +++ b/.changeset/structured-evaluation-failures.md @@ -0,0 +1,5 @@ +--- +"@reflag/flag-evaluation": patch +--- + +Return structured diagnostics for invalid numeric and date operands and unknown targeting operators instead of writing evaluation failures directly to the console. diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index 33c69e891..09aafc1c9 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -74,23 +74,26 @@ export type FilterTree = * - "IS_TRUE": Checks if a boolean value is true. * - "IS_FALSE": Checks if a boolean value is false. */ -export type ContextFilterOperator = - | "IS" - | "IS_NOT" - | "ANY_OF" - | "NOT_ANY_OF" - | "CONTAINS" - | "NOT_CONTAINS" - | "GT" - | "LT" - | "AFTER" - | "BEFORE" - | "DATE_AFTER" - | "DATE_BEFORE" - | "SET" - | "NOT_SET" - | "IS_TRUE" - | "IS_FALSE"; +const CONTEXT_FILTER_OPERATORS = [ + "IS", + "IS_NOT", + "ANY_OF", + "NOT_ANY_OF", + "CONTAINS", + "NOT_CONTAINS", + "GT", + "LT", + "AFTER", + "BEFORE", + "DATE_AFTER", + "DATE_BEFORE", + "SET", + "NOT_SET", + "IS_TRUE", + "IS_FALSE", +] as const; + +export type ContextFilterOperator = (typeof CONTEXT_FILTER_OPERATORS)[number]; /** * Represents a filter configuration used to filter data based on specific context. @@ -211,6 +214,24 @@ export type EvaluationError = field: string; operator: ContextFilterOperator | "rolloutPercentage"; message: string; + } + | { + code: "INVALID_CONTEXT_VALUE"; + field: string; + operator: ContextFilterOperator; + message: string; + } + | { + code: "INVALID_TARGETING_VALUE"; + field: string; + operator: ContextFilterOperator; + message: string; + } + | { + code: "UNKNOWN_OPERATOR"; + field: string; + operator: string; + message: string; }; function normalizeArrayElement(value: unknown): string | undefined { @@ -373,6 +394,8 @@ export function hashInt(hashInput: string): number { return Math.floor((value / 0xfffff) * 100000); } +const CONTEXT_FILTER_OPERATOR_SET = new Set(CONTEXT_FILTER_OPERATORS); + const ARRAY_OPERATORS = new Set([ "IS", "IS_NOT", @@ -451,18 +474,11 @@ export function evaluate( ); case "GT": if (isNaN(Number(normalizedFieldValue)) || isNaN(Number(value))) { - // TODO: return error instead? used logger previously - console.error( - `GT operator requires numeric values: ${normalizedFieldValue}, ${value}`, - ); return false; } return Number(normalizedFieldValue) > Number(value); case "LT": if (isNaN(Number(normalizedFieldValue)) || isNaN(Number(value))) { - console.error( - `LT operator requires numeric values: ${normalizedFieldValue}, ${value}`, - ); return false; } return Number(normalizedFieldValue) < Number(value); @@ -482,9 +498,6 @@ export function evaluate( const fieldValueDate = new Date(normalizedFieldValue).getTime(); const valueDate = new Date(value).getTime(); if (isNaN(fieldValueDate) || isNaN(valueDate)) { - console.error( - `${operator} operator requires valid date values: ${normalizedFieldValue}, ${value}`, - ); return false; } return operator === "DATE_AFTER" @@ -512,7 +525,6 @@ export function evaluate( case "IS_FALSE": return normalizedFieldValue == "false"; default: - console.error(`unknown operator: ${operator}`); return false; } } @@ -545,6 +557,104 @@ function addMissingContextFieldError( }); } +type ExpectedValue = "numeric" | "a valid date" | "a numeric day offset"; + +function isExpectedValue(value: string | undefined, expected: ExpectedValue) { + return expected === "a valid date" + ? !isNaN(new Date(value ?? "").getTime()) + : Number.isFinite(Number(value)); +} + +function addInvalidContextValueError( + errors: Map, + field: string, + operator: ContextFilterOperator, + expected: ExpectedValue, +): void { + errors.set(`invalid-context:${field}:${operator}`, { + code: "INVALID_CONTEXT_VALUE", + field, + operator, + message: `Context field "${field}" must be ${expected} for operator "${operator}".`, + }); +} + +function addInvalidTargetingValueError( + errors: Map, + field: string, + operator: ContextFilterOperator, + expected: ExpectedValue, +): void { + errors.set(`invalid-targeting:${field}:${operator}`, { + code: "INVALID_TARGETING_VALUE", + field, + operator, + message: `Targeting value for operator "${operator}" and context field "${field}" must be ${expected}.`, + }); +} + +function addUnknownOperatorError( + errors: Map, + field: string, + operator: string, +): void { + errors.set(`unknown-operator:${field}:${operator}`, { + code: "UNKNOWN_OPERATOR", + field, + operator, + message: `Unknown targeting operator "${operator}" for context field "${field}".`, + }); +} + +function hasValidOperatorValues( + filter: ContextFilter, + normalizedFieldValue: string, + errors: Map, +): boolean { + let contextExpected: ExpectedValue | undefined; + let targetingExpected: ExpectedValue | undefined; + + switch (filter.operator) { + case "GT": + case "LT": + contextExpected = targetingExpected = "numeric"; + break; + case "AFTER": + case "BEFORE": + contextExpected = "a valid date"; + targetingExpected = "a numeric day offset"; + break; + case "DATE_AFTER": + case "DATE_BEFORE": + contextExpected = targetingExpected = "a valid date"; + break; + default: + return true; + } + + const contextValid = isExpectedValue(normalizedFieldValue, contextExpected); + const targetingValid = isExpectedValue(filter.values?.[0], targetingExpected); + + if (!contextValid) { + addInvalidContextValueError( + errors, + filter.field, + filter.operator, + contextExpected, + ); + } + if (!targetingValid) { + addInvalidTargetingValueError( + errors, + filter.field, + filter.operator, + targetingExpected, + ); + } + + return contextValid && targetingValid; +} + function evaluateRecursively( filter: RuleFilter, context: FlattenedContext, @@ -554,6 +664,12 @@ function evaluateRecursively( case "constant": return filter.value; case "context": { + const operator = String(filter.operator); + if (!CONTEXT_FILTER_OPERATOR_SET.has(operator)) { + addUnknownOperatorError(errors, filter.field, operator); + return false; + } + if ( !(filter.field in context) && filter.operator !== "SET" && @@ -572,6 +688,13 @@ function evaluateRecursively( return false; } + if ( + !Array.isArray(normalizedFieldValue) && + !hasValidOperatorValues(filter, normalizedFieldValue, errors) + ) { + return false; + } + return evaluate( normalizedFieldValue, filter.operator, diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 56fc48174..354e0f31a 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -1,6 +1,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { + ContextFilterOperator, evaluate, evaluateFlagRules, EvaluationParams, @@ -887,6 +888,161 @@ describe("evaluate flag targeting integration ", () => { }); }); + describe("invalid scalar operator values", () => { + it("returns diagnostics for invalid numeric context and targeting values", () => { + const rules: Rule[] = [ + { + value: true, + filter: { + type: "context", + field: "user.age", + operator: "GT", + values: ["not-numeric"], + }, + }, + ]; + const context = { user: { age: "also-not-numeric" } }; + + for (const result of [ + evaluateFlagRules({ flagKey: "numeric", rules, context }), + newEvaluator(rules)(context, "numeric"), + ]) { + expect(result.value).toBeUndefined(); + expect(result.ruleEvaluationResults).toEqual([false]); + expect(result.errors).toEqual([ + { + code: "INVALID_CONTEXT_VALUE", + field: "user.age", + operator: "GT", + message: + 'Context field "user.age" must be numeric for operator "GT".', + }, + { + code: "INVALID_TARGETING_VALUE", + field: "user.age", + operator: "GT", + message: + 'Targeting value for operator "GT" and context field "user.age" must be numeric.', + }, + ]); + } + }); + + it("returns diagnostics for invalid date context and targeting values", () => { + const rules: Rule[] = [ + { + value: true, + filter: { + type: "context", + field: "user.createdAt", + operator: "DATE_AFTER", + values: ["not-a-date"], + }, + }, + ]; + const context = { user: { createdAt: "also-not-a-date" } }; + + for (const result of [ + evaluateFlagRules({ flagKey: "date", rules, context }), + newEvaluator(rules)(context, "date"), + ]) { + expect(result.errors).toEqual([ + { + code: "INVALID_CONTEXT_VALUE", + field: "user.createdAt", + operator: "DATE_AFTER", + message: + 'Context field "user.createdAt" must be a valid date for operator "DATE_AFTER".', + }, + { + code: "INVALID_TARGETING_VALUE", + field: "user.createdAt", + operator: "DATE_AFTER", + message: + 'Targeting value for operator "DATE_AFTER" and context field "user.createdAt" must be a valid date.', + }, + ]); + } + }); + + it("returns a diagnostic for an invalid relative-date offset", () => { + const rules: Rule[] = [ + { + value: true, + filter: { + type: "context", + field: "user.createdAt", + operator: "AFTER", + values: ["not-a-day-offset"], + }, + }, + ]; + const result = evaluateFlagRules({ + flagKey: "relative-date", + rules, + context: { user: { createdAt: "2024-01-10" } }, + }); + + expect(result.errors).toEqual([ + { + code: "INVALID_TARGETING_VALUE", + field: "user.createdAt", + operator: "AFTER", + message: + 'Targeting value for operator "AFTER" and context field "user.createdAt" must be a numeric day offset.', + }, + ]); + }); + + it("returns a diagnostic for an unknown operator", () => { + const rules: Rule[] = [ + { + value: true, + filter: { + type: "context", + field: "user.role", + operator: "UNKNOWN" as ContextFilterOperator, + values: ["admin"], + }, + }, + ]; + const result = evaluateFlagRules({ + flagKey: "unknown-operator", + rules, + context: { user: { role: "admin" } }, + }); + + expect(result.errors).toEqual([ + { + code: "UNKNOWN_OPERATOR", + field: "user.role", + operator: "UNKNOWN", + message: + 'Unknown targeting operator "UNKNOWN" for context field "user.role".', + }, + ]); + }); + + it("does not write invalid evaluations directly to the console", () => { + const consoleError = vi + .spyOn(console, "error") + .mockImplementation(() => undefined); + + try { + expect(evaluate("not-numeric", "GT", ["also-not-numeric"])).toBe(false); + expect(evaluate("not-a-date", "DATE_AFTER", ["also-not-a-date"])).toBe( + false, + ); + expect( + evaluate("value", "UNKNOWN" as ContextFilterOperator, ["target"]), + ).toBe(false); + expect(consoleError).not.toHaveBeenCalled(); + } finally { + consoleError.mockRestore(); + } + }); + }); + describe("DATE_AFTER and DATE_BEFORE in flag rules", () => { it("should evaluate DATE_AFTER operator in flag rules", () => { const res = evaluateFlagRules({ From b76a0b61440d87411f86e0a8f89f1840c6bf7b79 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Fri, 25 Sep 2026 12:11:29 +0200 Subject: [PATCH 2/4] refactor(flag-evaluation): remove redundant value guards --- packages/flag-evaluation/src/index.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index 09aafc1c9..fa8935578 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -473,14 +473,8 @@ export function evaluate( !normalizedFieldValue.toLowerCase().includes(value.toLowerCase()) ); case "GT": - if (isNaN(Number(normalizedFieldValue)) || isNaN(Number(value))) { - return false; - } return Number(normalizedFieldValue) > Number(value); case "LT": - if (isNaN(Number(normalizedFieldValue)) || isNaN(Number(value))) { - return false; - } return Number(normalizedFieldValue) < Number(value); case "AFTER": case "BEFORE": { @@ -497,9 +491,6 @@ export function evaluate( case "DATE_BEFORE": { const fieldValueDate = new Date(normalizedFieldValue).getTime(); const valueDate = new Date(value).getTime(); - if (isNaN(fieldValueDate) || isNaN(valueDate)) { - return false; - } return operator === "DATE_AFTER" ? fieldValueDate >= valueDate : fieldValueDate <= valueDate; From 9a5c67d9eee8c20b158d03057515d051e99aed46 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Fri, 25 Sep 2026 12:14:17 +0200 Subject: [PATCH 3/4] fix(flag-evaluation): preserve infinity comparisons --- packages/flag-evaluation/src/index.ts | 2 +- packages/flag-evaluation/test/index.test.ts | 30 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index fa8935578..e0c7d4745 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -553,7 +553,7 @@ type ExpectedValue = "numeric" | "a valid date" | "a numeric day offset"; function isExpectedValue(value: string | undefined, expected: ExpectedValue) { return expected === "a valid date" ? !isNaN(new Date(value ?? "").getTime()) - : Number.isFinite(Number(value)); + : !isNaN(Number(value)); } function addInvalidContextValueError( diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 354e0f31a..69a4a1aa3 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -928,6 +928,36 @@ describe("evaluate flag targeting integration ", () => { } }); + it.each([ + ["Infinity", "GT", "1"], + ["1", "LT", "Infinity"], + ] as const)( + "preserves numeric evaluation semantics for %s %s %s", + (contextValue, operator, targetingValue) => { + const rules: Rule[] = [ + { + value: true, + filter: { + type: "context", + field: "value", + operator, + values: [targetingValue], + }, + }, + ]; + const context = { value: contextValue }; + + expect(evaluate(contextValue, operator, [targetingValue])).toBe(true); + for (const result of [ + evaluateFlagRules({ flagKey: "numeric", rules, context }), + newEvaluator(rules)(context, "numeric"), + ]) { + expect(result.value).toBe(true); + expect(result.errors).toBeUndefined(); + } + }, + ); + it("returns diagnostics for invalid date context and targeting values", () => { const rules: Rule[] = [ { From 1942e3c3ef35f6c08237e93cf8fa1282df689107 Mon Sep 17 00:00:00 2001 From: Ron Cohen Date: Thu, 24 Sep 2026 21:38:46 +0200 Subject: [PATCH 4/4] refactor: align evaluation error terminology --- .changeset/consistent-evaluation-errors.md | 7 +++++++ packages/browser-sdk/src/client.ts | 8 ++++---- packages/browser-sdk/src/flag/flags.ts | 6 +++--- packages/browser-sdk/test/usage.test.ts | 8 ++++---- packages/flag-evaluation/src/index.ts | 2 +- packages/flag-evaluation/test/index.test.ts | 12 ++++++------ packages/node-sdk/src/client.ts | 20 ++++++++++---------- packages/node-sdk/src/types.ts | 6 +++--- packages/node-sdk/test/client.test.ts | 6 +++--- 9 files changed, 41 insertions(+), 34 deletions(-) create mode 100644 .changeset/consistent-evaluation-errors.md diff --git a/.changeset/consistent-evaluation-errors.md b/.changeset/consistent-evaluation-errors.md new file mode 100644 index 000000000..605944686 --- /dev/null +++ b/.changeset/consistent-evaluation-errors.md @@ -0,0 +1,7 @@ +--- +"@reflag/flag-evaluation": patch +"@reflag/browser-sdk": patch +"@reflag/node-sdk": patch +--- + +Use consistent evaluation error terminology internally and clarify the error emitted when flags are evaluated before initial flag state is available. diff --git a/packages/browser-sdk/src/client.ts b/packages/browser-sdk/src/client.ts index d4f0f80fe..6dd683ee9 100644 --- a/packages/browser-sdk/src/client.ts +++ b/packages/browser-sdk/src/client.ts @@ -406,10 +406,10 @@ const CLIENT_NOT_INITIALIZED_EVALUATION_ERROR = { code: "CLIENT_NOT_INITIALIZED", field: "", message: - "ReflagClient was not initialized before this flag was evaluated. Call initialize() before evaluating flags.", + "Flag was evaluated before the initial flag state was available. Await initialize() or wait for SDK loading to complete before evaluating flags.", } as const; -function withClientInitializationDiagnostic( +function withClientInitializationError( errors: CheckEvent["evaluationErrors"], evaluatedBeforeInitialization: boolean, ): CheckEvent["evaluationErrors"] { @@ -1445,7 +1445,7 @@ export class ReflagClient { version: f?.targetingVersion, ruleEvaluationResults: f?.ruleEvaluationResults, missingContextFields: f?.missingContextFields, - evaluationErrors: withClientInitializationDiagnostic( + evaluationErrors: withClientInitializationError( f?.evaluationErrors, evaluatedBeforeInitialization, ), @@ -1464,7 +1464,7 @@ export class ReflagClient { version: f?.config?.version, ruleEvaluationResults: f?.config?.ruleEvaluationResults, missingContextFields: f?.config?.missingContextFields, - evaluationErrors: withClientInitializationDiagnostic( + evaluationErrors: withClientInitializationError( f?.config?.evaluationErrors, evaluatedBeforeInitialization, ), diff --git a/packages/browser-sdk/src/flag/flags.ts b/packages/browser-sdk/src/flag/flags.ts index 8ac9b0448..8326cea84 100644 --- a/packages/browser-sdk/src/flag/flags.ts +++ b/packages/browser-sdk/src/flag/flags.ts @@ -92,7 +92,7 @@ export type RawFlag = { missingContextFields?: string[]; /** - * Non-fatal diagnostics produced while evaluating targeting rules. + * Non-fatal errors produced while evaluating targeting rules. */ evaluationErrors?: Array<{ code: string; @@ -142,7 +142,7 @@ export type RawFlag = { missingContextFields?: string[]; /** - * Non-fatal diagnostics produced while evaluating targeting rules. + * Non-fatal errors produced while evaluating targeting rules. */ evaluationErrors?: RawFlag["evaluationErrors"]; }; @@ -265,7 +265,7 @@ export interface CheckEvent { missingContextFields?: string[]; /** - * Non-fatal diagnostics produced while evaluating the flag. + * Non-fatal errors produced while evaluating the flag. */ evaluationErrors?: RawFlag["evaluationErrors"]; } diff --git a/packages/browser-sdk/test/usage.test.ts b/packages/browser-sdk/test/usage.test.ts index 44e001f70..9a839c7db 100644 --- a/packages/browser-sdk/test/usage.test.ts +++ b/packages/browser-sdk/test/usage.test.ts @@ -33,7 +33,7 @@ const clientNotInitializedError = { code: "CLIENT_NOT_INITIALIZED", field: "", message: - "ReflagClient was not initialized before this flag was evaluated. Call initialize() before evaluating flags.", + "Flag was evaluated before the initial flag state was available. Await initialize() or wait for SDK loading to complete before evaluating flags.", }; vi.mock("../src/sse"); @@ -456,7 +456,7 @@ describe(`sends "check" events `, () => { }); }); - it("adds diagnostics when flags are evaluated before initialization", () => { + it("adds errors when flags are evaluated before initialization", () => { const sendCheckEventSpy = vi.spyOn( FlagsClient.prototype, "sendCheckEvent", @@ -485,7 +485,7 @@ describe(`sends "check" events `, () => { ); }); - it("does not add initialization diagnostics to bootstrapped evaluations", () => { + it("does not add initialization errors to bootstrapped evaluations", () => { const sendCheckEventSpy = vi.spyOn( FlagsClient.prototype, "sendCheckEvent", @@ -508,7 +508,7 @@ describe(`sends "check" events `, () => { ); }); - it(`does not send check events or add initialization diagnostics when offline`, () => { + it(`does not send check events or add initialization errors when offline`, () => { const sendCheckEventSpy = vi.spyOn( FlagsClient.prototype, "sendCheckEvent", diff --git a/packages/flag-evaluation/src/index.ts b/packages/flag-evaluation/src/index.ts index e0c7d4745..3e63f9152 100644 --- a/packages/flag-evaluation/src/index.ts +++ b/packages/flag-evaluation/src/index.ts @@ -752,7 +752,7 @@ export interface EvaluationParams { * @property {boolean[]} ruleEvaluationResults - Array indicating the success or failure of each rule evaluated. * @property {string} [reason] - Optional field providing additional explanation regarding the evaluation result. * @property {string[]} [missingContextFields] - Legacy array of context fields that were required but not provided during evaluation. - * @property {EvaluationError[]} [errors] - Non-fatal diagnostics for rules that could not be evaluated. + * @property {EvaluationError[]} [errors] - Non-fatal errors for rules that could not be evaluated. */ export interface EvaluationResult { flagKey: string; diff --git a/packages/flag-evaluation/test/index.test.ts b/packages/flag-evaluation/test/index.test.ts index 69a4a1aa3..87019e1e9 100644 --- a/packages/flag-evaluation/test/index.test.ts +++ b/packages/flag-evaluation/test/index.test.ts @@ -555,7 +555,7 @@ describe("evaluate flag targeting integration ", () => { "%s normalization", (operator) => { it.each(["admin", "2", "true", "", '{"level":3}', "[false]"])( - "matches normalized element %j without diagnostics", + "matches normalized element %j without errors", (value) => { const rules: Rule[] = [ { @@ -726,7 +726,7 @@ describe("evaluate flag targeting integration ", () => { ]); }); - it("returns a non-fatal diagnostic for scalar-only operators", () => { + it("returns a non-fatal error for scalar-only operators", () => { const res = evaluateFlagRules({ flagKey: "role-based-flag", rules: [ @@ -889,7 +889,7 @@ describe("evaluate flag targeting integration ", () => { }); describe("invalid scalar operator values", () => { - it("returns diagnostics for invalid numeric context and targeting values", () => { + it("returns errors for invalid numeric context and targeting values", () => { const rules: Rule[] = [ { value: true, @@ -958,7 +958,7 @@ describe("evaluate flag targeting integration ", () => { }, ); - it("returns diagnostics for invalid date context and targeting values", () => { + it("returns errors for invalid date context and targeting values", () => { const rules: Rule[] = [ { value: true, @@ -995,7 +995,7 @@ describe("evaluate flag targeting integration ", () => { } }); - it("returns a diagnostic for an invalid relative-date offset", () => { + it("returns an error for an invalid relative-date offset", () => { const rules: Rule[] = [ { value: true, @@ -1024,7 +1024,7 @@ describe("evaluate flag targeting integration ", () => { ]); }); - it("returns a diagnostic for an unknown operator", () => { + it("returns an error for an unknown operator", () => { const rules: Rule[] = [ { value: true, diff --git a/packages/node-sdk/src/client.ts b/packages/node-sdk/src/client.ts index 5791e4d7e..a5002c8cf 100644 --- a/packages/node-sdk/src/client.ts +++ b/packages/node-sdk/src/client.ts @@ -90,10 +90,10 @@ const CLIENT_NOT_INITIALIZED_EVALUATION_ERROR = { code: "CLIENT_NOT_INITIALIZED", field: "", message: - "ReflagClient was not initialized before this flag was evaluated. Call initialize() before evaluating flags.", + "Flag was evaluated before the initial flag state was available. Await initialize() or wait for SDK loading to complete before evaluating flags.", } as const; -function withClientInitializationDiagnostic( +function withClientInitializationError( errors: FlagEvent["evalErrors"], evaluatedBeforeInitialization: boolean, ): FlagEvent["evalErrors"] { @@ -1257,7 +1257,7 @@ export class ReflagClient { * @param event.evalContext - The evaluation context of the flag to send. * @param event.evalRuleResults - The evaluation rule results of the flag to send. * @param event.evalMissingFields - The evaluation missing fields of the flag to send. - * @param event.evalErrors - The non-fatal evaluation diagnostics of the flag to send. + * @param event.evalErrors - The non-fatal evaluation errors of the flag to send. * * @throws An error if the event is invalid. * @@ -1377,11 +1377,11 @@ export class ReflagClient { } /** - * Warns if a flag or config evaluation produced diagnostics. + * Warns if a flag or config evaluation produced errors. * * @param flag - The flag to check. */ - private _warnFlagEvaluationDiagnostics(flag: { + private _warnFlagEvaluationErrors(flag: { key: string; missingContextFields?: string[]; evaluationErrors?: EvaluationError[]; @@ -1474,7 +1474,7 @@ export class ReflagClient { checkContextWithTracking(options); if (!this.initializationFinished && !this._config.offline) { - this.logger.error("getFlag(s): ReflagClient is not initialized yet."); + this.logger.error(CLIENT_NOT_INITIALIZED_EVALUATION_ERROR.message); } void this.syncContext(options); @@ -1589,11 +1589,11 @@ export class ReflagClient { : { key: undefined, payload: undefined }; const evaluatedBeforeInitialization = !this.initializationFinished && !this._config.offline; - const flagEvaluationErrors = withClientInitializationDiagnostic( + const flagEvaluationErrors = withClientInitializationError( flag.evaluationErrors, evaluatedBeforeInitialization, ); - const configEvaluationErrors = withClientInitializationDiagnostic( + const configEvaluationErrors = withClientInitializationError( config?.evaluationErrors, evaluatedBeforeInitialization, ); @@ -1601,7 +1601,7 @@ export class ReflagClient { return { get isEnabled() { if (enableTracking && enableChecks) { - client._warnFlagEvaluationDiagnostics(flag); + client._warnFlagEvaluationErrors(flag); void client .sendFlagEvent({ @@ -1625,7 +1625,7 @@ export class ReflagClient { }, get config() { if (enableTracking && enableChecks) { - client._warnFlagEvaluationDiagnostics({ ...flag, config }); + client._warnFlagEvaluationErrors({ ...flag, config }); void client .sendFlagEvent({ diff --git a/packages/node-sdk/src/types.ts b/packages/node-sdk/src/types.ts index 006c110c7..fee47c44e 100644 --- a/packages/node-sdk/src/types.ts +++ b/packages/node-sdk/src/types.ts @@ -65,7 +65,7 @@ export type FlagEvent = { evalMissingFields?: string[]; /** - * Non-fatal diagnostics produced while evaluating the flag (optional). + * Non-fatal errors produced while evaluating the flag (optional). **/ evalErrors?: Array<{ code: string; @@ -106,7 +106,7 @@ export type RawFlagRemoteConfig = { missingContextFields?: string[]; /** - * Non-fatal diagnostics produced while evaluating targeting rules. + * Non-fatal errors produced while evaluating targeting rules. */ evaluationErrors?: EvaluationError[]; }; @@ -147,7 +147,7 @@ export interface RawFlag { missingContextFields?: string[]; /** - * Non-fatal diagnostics produced while evaluating targeting rules. + * Non-fatal errors produced while evaluating targeting rules. */ evaluationErrors?: EvaluationError[]; } diff --git a/packages/node-sdk/test/client.test.ts b/packages/node-sdk/test/client.test.ts index 459465c5d..7c262f196 100644 --- a/packages/node-sdk/test/client.test.ts +++ b/packages/node-sdk/test/client.test.ts @@ -47,7 +47,7 @@ const clientNotInitializedError = { code: "CLIENT_NOT_INITIALIZED", field: "", message: - "ReflagClient was not initialized before this flag was evaluated. Call initialize() before evaluating flags.", + "Flag was evaluated before the initial flag state was available. Await initialize() or wait for SDK loading to complete before evaluating flags.", }; vi.mock("../src/rate-limiter", async (importOriginal) => { @@ -1581,7 +1581,7 @@ describe("ReflagClient", () => { }); }); - it("sends diagnostics when flags are evaluated before initialization", async () => { + it("sends errors when flags are evaluated before initialization", async () => { const context = { company, user, @@ -1609,7 +1609,7 @@ describe("ReflagClient", () => { ]); }); - it("does not add initialization diagnostics when offline", () => { + it("does not add initialization errors when offline", () => { const offlineClient = new ReflagClient({ ...validOptions, offline: true,