diff --git a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts index 87cc857..9cebefb 100644 --- a/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts +++ b/flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts @@ -128,6 +128,26 @@ export type FeaturePriority = number; * Feature overrides for the segment. */ export type Overrides = FeatureContext[]; +/** + * Feature name. + */ +export type Name3 = string; +/** + * Indicates if the feature flag is enabled. + */ +export type Enabled1 = boolean; +/** + * Feature flag value. + */ +export type Value4 = string | number | boolean | null; +/** + * Reason for the feature flag evaluation. + */ +export type Reason = string; +/** + * A stable identifier of the multivariate variant the identity was bucketed into: the variant's key, "control" for the control bucket, or null when no multivariate split applied (a standard feature, an unkeyed variant, or evaluation without an identity). + */ +export type Variant = string | null; /** * A context object containing the necessary information to evaluate Flagsmith feature flags. @@ -140,6 +160,7 @@ export interface EvaluationContext { identity?: IdentityContext | null; segments?: Segments; features?: Features; + flags?: FlagResults; [k: string]: unknown; } /** @@ -250,3 +271,24 @@ export interface SegmentMetadata { export interface Features { [k: string]: FeatureContext; } +/** + * Results of features already evaluated for this context, mapped by feature name. Populated by the engine as evaluation progresses, so that segment conditions may reference `$.flags..enabled`, `.value` and `.variant` to express feature dependencies. + */ +export interface FlagResults { + [k: string]: FlagResult; +} +export interface FlagResult { + name: Name3; + enabled: Enabled1; + value: Value4; + reason: Reason; + variant: Variant; + metadata?: FeatureMetadata1; + [k: string]: unknown; +} +/** + * Additional metadata associated with the feature. + */ +export interface FeatureMetadata1 { + [k: string]: unknown; +} diff --git a/package.json b/package.json index e774f57..473ae90 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "deploy:beta": "npm i && npm run build && npm publish --tag beta", "prepare": "husky install", "generate-evaluation-result-types": "curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-result.json -o flagsmith-engine/evaluation/evaluationResult/evaluationResult.types.ts && rm evaluation-result.json", - "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json", + "generate-evaluation-context-types": "curl -o evaluation-context.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-context.json && curl -o evaluation-result.json https://raw.githubusercontent.com/Flagsmith/flagsmith/main/sdk/evaluation-result.json && npx json2ts -i evaluation-context.json -o flagsmith-engine/evaluation/evaluationContext/evaluationContext.types.ts && rm evaluation-context.json evaluation-result.json", "generate-engine-types": "npm run generate-evaluation-result-types && npm run generate-evaluation-context-types" }, "dependencies": { diff --git a/sdk/index.ts b/sdk/index.ts index e262278..47abf13 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -637,7 +637,11 @@ export class Flagsmith { throw new FlagsmithClientError('Unable to get flags. No environment present.'); } const evaluationResult = getEvaluationResult(context as EvaluationContextWithMetadata); - const flags = Flags.fromEvaluationResult(evaluationResult); + const flags = Flags.fromEvaluationResult( + evaluationResult, + this.defaultFlagHandler, + this.analyticsProcessor + ); if (!!this.cache) { await this.cache.set('flags', flags); diff --git a/tests/sdk/flagsmith-environment-flags.test.ts b/tests/sdk/flagsmith-environment-flags.test.ts index e424424..249b024 100644 --- a/tests/sdk/flagsmith-environment-flags.test.ts +++ b/tests/sdk/flagsmith-environment-flags.test.ts @@ -1,6 +1,6 @@ import Flagsmith from '../../sdk/index.js'; import { fetch, environmentJSON, flagsJSON } from './fetchMock.js'; -import { environmentModel, flagsmith } from './utils.js'; +import { environmentModel, flagsmith, TestCache } from './utils.js'; import { DefaultFlag } from '../../sdk/models.js'; import { getUserAgent } from '../../sdk/utils.js'; @@ -181,3 +181,56 @@ test('test_local_evaluation', async () => { expect(flag.value).not.toBe(defaultFlag.value); expect(flag.value).toBe('some-value'); }); + +test.each([false, true])( + 'environment flag analytics work with local evaluation: %s', + async enableLocalEvaluation => { + vi.spyOn(globalThis, 'fetch').mockImplementation((url, options) => + fetch(url.toString(), options) + ); + const flg = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation, + enableAnalytics: true, + cache: new TestCache() + }); + const flags = await flg.getEnvironmentFlags(); + expect(flags.isFeatureEnabled('some_feature')).toBe(true); + expect(flags.getFeatureValue('some_feature')).toBe('some-value'); + + const cachedFlags = await flg.getEnvironmentFlags(); + expect(cachedFlags).toBe(flags); + cachedFlags.getFlag('some_feature'); + await flags.analyticsProcessor?.flush(); + + const analyticsRequests = fetch.mock.calls.filter(([url]) => + String(url).includes('/analytics/flags/') + ); + expect(analyticsRequests).toHaveLength(1); + expect(JSON.parse(String(analyticsRequests[0][1]?.body))).toEqual({ + some_feature: 3 + }); + } +); + +test('local environment evaluation does not enable analytics by default', async () => { + const flg = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true + }); + const flags = await flg.getEnvironmentFlags(); + expect(flags.isFeatureEnabled('some_feature')).toBe(true); + expect(flags.analyticsProcessor).toBeUndefined(); + expect(fetch.mock.calls.some(([url]) => String(url).includes('/analytics/flags/'))).toBe(false); +}); + +test('local environment flags preserve the default flag handler', async () => { + const defaultFlag = new DefaultFlag('fallback-value', true); + const flg = flagsmith({ + environmentKey: 'ser.key', + enableLocalEvaluation: true, + defaultFlagHandler: () => defaultFlag + }); + const flags = await flg.getEnvironmentFlags(); + expect(flags.getFlag('missing_feature')).toBe(defaultFlag); +}); diff --git a/tests/setup.ts b/tests/setup.ts index 097e640..1543171 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -2,4 +2,7 @@ import { fetch as mockFetch, fetchImpl } from './sdk/fetchMock.js'; beforeEach(() => { mockFetch.mockImplementation(fetchImpl); + vi.spyOn(globalThis, 'fetch').mockImplementation((url, options) => + mockFetch(url.toString(), options) + ); });