diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 077a1bf..c27b26f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ jobs: - name: Checkout 🛎 uses: actions/checkout@v4 - - uses: pnpm/action-setup@v2 + - uses: pnpm/action-setup@v4 - name: Setup node env 🏗 uses: actions/setup-node@v4 diff --git a/e2e/BKTClient.spec.ts b/e2e/BKTClient.spec.ts index baa14cd..4e6ef9a 100644 --- a/e2e/BKTClient.spec.ts +++ b/e2e/BKTClient.spec.ts @@ -219,12 +219,30 @@ suite('e2e/BKTClientTest', () => { const javascript = client.evaluationDetails('feature-js-e2e-string') expect(javascript).not.toBeNull() + const javascriptEvaluationDetails = client.stringVariationDetails( + 'feature-js-e2e-string', + '', + ) + expect(javascriptEvaluationDetails.variationValue).not.toEqual('') + // can retrieve evaluations for other featureTag const android = client.evaluationDetails('feature-android-e2e-string') expect(android).not.toBeNull() + const androidEvaluationDetails = client.stringVariationDetails( + 'feature-android-e2e-string', + '', + ) + expect(androidEvaluationDetails.variationValue).not.toStrictEqual('') + const golang = client.evaluationDetails('feature-go-server-e2e-1') expect(golang).not.toBeNull() + + const golangEvaluationDetails = client.stringVariationDetails( + 'feature-go-server-e2e-1', + '', + ) + expect(golangEvaluationDetails.variationValue).not.toStrictEqual('') }) }) }) diff --git a/e2e/evaluations.spec.ts b/e2e/evaluations.spec.ts index 711487a..93527af 100644 --- a/e2e/evaluations.spec.ts +++ b/e2e/evaluations.spec.ts @@ -66,6 +66,20 @@ suite('e2e/evaluations', () => { variationValue: 'value-1', reason: 'DEFAULT', }) + + const evaluationDetails = client.stringVariationDetails( + FEATURE_ID_STRING, + 'default', + ) + expect(evaluationDetails).toStrictEqual({ + featureId: FEATURE_ID_STRING, + featureVersion: 5, + userId: USER_ID, + variationId: '87e0a1ef-a0cb-49da-8460-289948f117ba', + variationName: 'variation 1', + variationValue: 'value-1', + reason: 'DEFAULT', + }) }) }) @@ -95,6 +109,20 @@ suite('e2e/evaluations', () => { variationValue: '10', reason: 'DEFAULT', }) + + const evaluationDetails = client.numberVariationDetails( + FEATURE_ID_INT, + 0, + ) + expect(evaluationDetails).toStrictEqual({ + featureId: FEATURE_ID_INT, + featureVersion: 3, + userId: USER_ID, + variationId: '6079c503-c281-4561-b870-c2c59a75e6a6', + variationName: 'variation 10', + variationValue: 10, + reason: 'DEFAULT', + }) }) }) @@ -123,6 +151,20 @@ suite('e2e/evaluations', () => { variationValue: '2.1', reason: 'DEFAULT', }) + + const evaluationDetails = client.numberVariationDetails( + FEATURE_ID_DOUBLE, + 0, + ) + expect(evaluationDetails).toStrictEqual({ + featureId: FEATURE_ID_DOUBLE, + featureVersion: 3, + userId: USER_ID, + variationId: '2d4a213c-1721-434b-8484-1b72826ece98', + variationName: 'variation 2.1', + variationValue: 2.1, + reason: 'DEFAULT', + }) }) }) }) @@ -152,6 +194,61 @@ suite('e2e/evaluations', () => { variationValue: 'true', reason: 'DEFAULT', }) + + const evaluationDetails = client.booleanVariationDetails( + FEATURE_ID_BOOLEAN, + false, + ) + expect(evaluationDetails).toStrictEqual({ + featureId: FEATURE_ID_BOOLEAN, + featureVersion: 3, + userId: USER_ID, + variationId: '4fab39c8-bf62-4a78-8a10-1b8bc3dd3806', + variationName: 'variation true', + variationValue: true, + reason: 'DEFAULT', + }) + }) + }) + + suite('objectVariation', () => { + test('value', () => { + const client = getBKTClient() + + assert(client != null) + + expect(client.objectVariation(FEATURE_ID_JSON, '')).toStrictEqual({ + key: 'value-1', + }) + }) + + test('detail', () => { + const client = getBKTClient() + + assert(client != null) + + const detail = client.evaluationDetails(FEATURE_ID_JSON) + expect(detail).toBeEvaluation({ + id: 'feature-js-e2e-json:3:bucketeer-js-user-id-1', + featureId: FEATURE_ID_JSON, + featureVersion: 3, + userId: USER_ID, + variationId: '8b53a27b-2658-4f8c-925e-fb277808ed30', + variationName: 'variation 1', + variationValue: `{ "key": "value-1" }`, + reason: 'DEFAULT', + }) + + const evaluationDetails = client.objectVariationDetails(FEATURE_ID_JSON, {}) + expect(evaluationDetails).toStrictEqual({ + featureId: FEATURE_ID_JSON, + featureVersion: 3, + userId: USER_ID, + variationId: '8b53a27b-2658-4f8c-925e-fb277808ed30', + variationName: 'variation 1', + variationValue: { key: 'value-1' }, + reason: 'DEFAULT', + }) }) }) diff --git a/e2e/events.spec.ts b/e2e/events.spec.ts index 2158f1a..bd94ded 100644 --- a/e2e/events.spec.ts +++ b/e2e/events.spec.ts @@ -87,12 +87,15 @@ suite('e2e/events', () => { expect(client.jsonVariation(FEATURE_ID_JSON, '')).toStrictEqual({ key: 'value-1', }) + expect(client.objectVariation(FEATURE_ID_JSON, '')).toStrictEqual({ + key: 'value-1', + }) const component = getDefaultComponent(client) const events = component.dataModule.eventStorage().getAll() // It includes the Latency and ResponseSize metrics - expect(events).toHaveLength(7) + expect(events).toHaveLength(8) expect( events.some( (e) => @@ -125,10 +128,15 @@ suite('e2e/events', () => { ).toStrictEqual({ key: 'value-default', }) + expect( + client.objectVariation(FEATURE_ID_JSON, { key: 'value-default' }), + ).toStrictEqual({ + key: 'value-default', + }) const events = component.dataModule.eventStorage().getAll() // It includes the Latency and ResponseSize metrics - expect(events).toHaveLength(7) + expect(events).toHaveLength(8) expect( events.some( (e) => diff --git a/src/BKTClient.ts b/src/BKTClient.ts index a2570ea..6d8ad77 100644 --- a/src/BKTClient.ts +++ b/src/BKTClient.ts @@ -1,22 +1,70 @@ -import { BKTEvaluation } from './BKTEvaluation' +import { + BKTEvaluation, +} from './BKTEvaluation' import { BKTUser } from './BKTUser' import { Component } from './internal/di/Component' import { clearInstance, getInstance, setInstance } from './internal/instance' import { ApiId } from './internal/model/MetricsEventData' import { TaskScheduler } from './internal/scheduler/TaskScheduler' import { toBKTUser } from './internal/UserHolder' +import { BKTValue } from './BKTValue' +import { BKTEvaluationDetails } from './BKTEvaluationDetails' + export interface BKTClient { + booleanVariation: (featureId: string, defaultValue: boolean) => boolean + + booleanVariationDetails: ( + featureId: string, + defaultValue: boolean, + ) => BKTEvaluationDetails + stringVariation: (featureId: string, defaultValue: string) => string + + stringVariationDetails: ( + featureId: string, + defaultValue: string, + ) => BKTEvaluationDetails + numberVariation: (featureId: string, defaultValue: number) => number - booleanVariation: (featureId: string, defaultValue: boolean) => boolean + + numberVariationDetails: ( + featureId: string, + defaultValue: number, + ) => BKTEvaluationDetails + + objectVariation: (featureId: string, defaultValue: BKTValue) => BKTValue + + /** + * Retrieves the evaluation details for a given feature based on its ID. + * + * @param featureId - The unique identifier for the feature. + * @param defaultValue - The default value to return if no result is found. This value should be of type `BKTValue`. + * + * @returns An object of type `BKTEvaluationDetail` containing the evaluation details. + * + * Note: The returned value will be either a BKTJsonObject or a BKTJsonArray. If no result is found, it will return the provided `defaultValue`, which can be of any type within `BKTValue`. + */ + objectVariationDetails: ( + featureId: string, + defaultValue: BKTValue, + ) => BKTEvaluationDetails + + /** + * @deprecated use objectVariation(featureId: string, defaultValue: BKTValue) instead. + */ jsonVariation: (featureId: string, defaultValue: T) => T + track: (goalId: string, value: number) => void currentUser: () => BKTUser updateUserAttributes: (attributes: Record) => void fetchEvaluations: (timeoutMillis?: number) => Promise flush: () => Promise + /** + * @deprecated use stringVariationDetails(featureId: string, defaultValue: string) instead. + */ evaluationDetails: (featureId: string) => BKTEvaluation | null + addEvaluationUpdateListener: (listener: () => void) => string removeEvaluationUpdateListener: (listenerId: string) => void clearEvaluationUpdateListeners: () => void @@ -32,36 +80,68 @@ export class BKTClientImpl implements BKTClient { return this.fetchEvaluations(timeoutMillis) } - stringVariation(featureId: string, defaultValue: string): string { - const value = this.getVariationValue(featureId) - if (value === null) { - return defaultValue - } - return value + booleanVariation(featureId: string, defaultValue: boolean): boolean { + return this.booleanVariationDetails(featureId, defaultValue).variationValue + } + + booleanVariationDetails( + featureId: string, + defaultValue: boolean, + ): BKTEvaluationDetails { + return this.getVariationDetails( + featureId, + defaultValue, + stringToBoolConverter, + ) } numberVariation(featureId: string, defaultValue: number): number { - const value = this.getVariationValue(featureId) - if (value === null) { - return defaultValue - } - const result = Number(value) - if (Number.isNaN(result)) { - return defaultValue - } - return result + return this.numberVariationDetails(featureId, defaultValue).variationValue } - booleanVariation(featureId: string, defaultValue: boolean): boolean { - const value = this.getVariationValue(featureId) - const result = value?.toLowerCase() - if (result === 'true') { - return true - } else if (result === 'false') { - return false - } else { - return defaultValue - } + numberVariationDetails( + featureId: string, + defaultValue: number, + ): BKTEvaluationDetails { + return this.getVariationDetails( + featureId, + defaultValue, + stringToNumberConverter, + ) + } + + stringVariation(featureId: string, defaultValue: string): string { + return this.stringVariationDetails(featureId, defaultValue).variationValue + } + + stringVariationDetails( + featureId: string, + defaultValue: string, + ): BKTEvaluationDetails { + return this.getVariationDetails( + featureId, + defaultValue, + defaultStringToTypeConverter, + ) + } + + objectVariation(featureId: string, defaultValue: BKTValue): BKTValue { + const value = this.objectVariationDetails( + featureId, + defaultValue, + ).variationValue + return value + } + + objectVariationDetails( + featureId: string, + defaultValue: BKTValue, + ): BKTEvaluationDetails { + return this.getVariationDetails( + featureId, + defaultValue, + stringToObjectConverter, + ) } jsonVariation(featureId: string, defaultValue: T): T { @@ -139,6 +219,50 @@ export class BKTClientImpl implements BKTClient { this.component.evaluationInteractor().clearUpdateListeners() } + private getVariationDetails( + featureId: string, + defaultValue: T, + typeConverter: StringToTypeConverter, + ): BKTEvaluationDetails { + const raw = this.component.evaluationInteractor().getLatest(featureId) + const user = this.component.userHolder().get() + const featureTag = this.component.config().featureTag + + const variationValue = raw?.variationValue + + // Handle conversion based on the type of T + let result: T | null = null + + if (variationValue !== undefined && variationValue !== null) { + try { + result = typeConverter(variationValue) + } catch { + result = null + } + } + + if (raw !== null && result !== null) { + this.component + .eventInteractor() + .trackEvaluationEvent(featureTag, user, raw) + return { + featureId: raw.featureId, + featureVersion: raw.featureVersion, + userId: raw.userId, + variationId: raw.variationId, + variationName: raw.variationName, + variationValue: result, + reason: raw.reason.type, + } satisfies BKTEvaluationDetails + } else { + this.component + .eventInteractor() + .trackDefaultEvaluationEvent(featureTag, user, featureId) + + return newDefaultBKTEvaluationDetails(user.id, featureId, defaultValue) + } + } + private getVariationValue(featureId: string): string | null { const raw = this.component.evaluationInteractor().getLatest(featureId) @@ -225,3 +349,72 @@ export const destroyBKTClient = (): void => { } clearInstance() } + +function assetNonBlankString(input: string) { + if (input.trim().length == 0) { + throw new Error('Input string must be non-blank') + } +} + +function parseJsonObjectOrArray(input: string) { + const primitiveTypes = ['number', 'string', 'boolean', 'null'] + const parsed = JSON.parse(input) + + if (primitiveTypes.includes(typeof parsed) || parsed === null) { + throw new Error('Only JSON objects or array are allowed') + } + + return parsed +} + +export const newDefaultBKTEvaluationDetails = ( + userId: string, + featureId: string, + defaultValue: T, +): BKTEvaluationDetails => { + return { + featureId: featureId, + featureVersion: 0, + userId: userId, + variationId: '', + variationName: '', + variationValue: defaultValue, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails +} + +export type StringToTypeConverter = (input: string) => T | null + +export const defaultStringToTypeConverter: StringToTypeConverter = ( + input: string, +) => input + +export const stringToBoolConverter: StringToTypeConverter = ( + input: string, +) => { + assetNonBlankString(input) + + const lowcaseValue = input.toLowerCase() + if (lowcaseValue === 'true') { + return true + } else if (lowcaseValue === 'false') { + return false + } else { + return null + } +} + +export const stringToNumberConverter: StringToTypeConverter = ( + input: string, +) => { + assetNonBlankString(input) + const parsedNumber = Number(input) + return isNaN(parsedNumber) ? null : parsedNumber +} + +export const stringToObjectConverter: StringToTypeConverter = ( + input: string, +) => { + assetNonBlankString(input) + return parseJsonObjectOrArray(input) +} diff --git a/src/BKTEvaluation.ts b/src/BKTEvaluation.ts index 920f595..188c8d8 100644 --- a/src/BKTEvaluation.ts +++ b/src/BKTEvaluation.ts @@ -1,3 +1,6 @@ +/** + * @deprecated use BKTEvaluationDetails instead. + */ export interface BKTEvaluation { readonly id: string readonly featureId: string diff --git a/src/BKTEvaluationDetails.ts b/src/BKTEvaluationDetails.ts new file mode 100644 index 0000000..554d535 --- /dev/null +++ b/src/BKTEvaluationDetails.ts @@ -0,0 +1,17 @@ +import { BKTValue } from './BKTValue' + +export interface BKTEvaluationDetails { + readonly featureId: string + readonly featureVersion: number + readonly userId: string + readonly variationId: string + readonly variationName: string + readonly variationValue: T + readonly reason: + | 'TARGET' + | 'RULE' + | 'DEFAULT' + | 'CLIENT' + | 'OFF_VARIATION' + | 'PREREQUISITE' +} diff --git a/src/BKTValue.ts b/src/BKTValue.ts new file mode 100644 index 0000000..4cee94c --- /dev/null +++ b/src/BKTValue.ts @@ -0,0 +1,9 @@ +export type BKTJsonPrimitive = null | boolean | string | number +export type BKTJsonObject = { + [key: string]: BKTValue +} +export type BKTJsonArray = BKTValue[] +/** + * Represents a JSON node value. + */ +export type BKTValue = BKTJsonPrimitive | BKTJsonObject | BKTJsonArray diff --git a/src/main.browser.ts b/src/main.browser.ts index 9d7870b..11822e2 100644 --- a/src/main.browser.ts +++ b/src/main.browser.ts @@ -19,6 +19,13 @@ export type { BrowserLocalStorage, InMemoryStorage, } from './BKTStorage' +export type { + BKTValue, + BKTJsonArray, + BKTJsonObject, + BKTJsonPrimitive, +} from './BKTValue' +export type { BKTEvaluationDetails } from './BKTEvaluationDetails' const createBrowserComponent = (config: BKTConfig, user: User): Component => { return new DefaultComponent( diff --git a/src/main.ts b/src/main.ts index 42b107c..58ebd49 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,6 +19,13 @@ export type { BrowserLocalStorage, InMemoryStorage, } from './BKTStorage' +export type { + BKTValue, + BKTJsonArray, + BKTJsonObject, + BKTJsonPrimitive, +} from './BKTValue' +export type { BKTEvaluationDetails } from './BKTEvaluationDetails' const createNodeComponent = (config: BKTConfig, user: User): Component => { return new DefaultComponent( diff --git a/test/BKTClient.spec.ts b/test/BKTClient.spec.ts index 6e6318f..ff27fa8 100644 --- a/test/BKTClient.spec.ts +++ b/test/BKTClient.spec.ts @@ -11,10 +11,16 @@ import { afterAll, } from 'vitest' import { + defaultStringToTypeConverter, destroyBKTClient, getBKTClient, initializeBKTClientInternal, + newDefaultBKTEvaluationDetails, + stringToBoolConverter, + stringToNumberConverter, + stringToObjectConverter, } from '../src/BKTClient' +import { BKTValue } from '../src/BKTValue' import { BKTConfig, defineBKTConfig } from '../src/BKTConfig' import { GetEvaluationsRequest } from '../src/internal/model/request/GetEvaluationsRequest' import { GetEvaluationsResponse } from '../src/internal/model/response/GetEvaluationsResponse' @@ -33,13 +39,16 @@ import { } from '../src/BKTExceptions' import { Evaluation } from '../src/internal/model/Evaluation' import { EventType } from '../src/internal/model/Event' -import { BKTEvaluation } from '../src/BKTEvaluation' +import { + BKTEvaluation, +} from '../src/BKTEvaluation' import { ErrorResponse } from '../src/internal/model/response/ErrorResponse' import { RegisterEventsRequest } from '../src/internal/model/request/RegisterEventsRequest' import { RegisterEventsResponse } from '../src/internal/model/response/RegisterEventsResponse' import { DefaultComponent } from '../src/internal/di/Component' import { DataModule } from '../src/internal/di/DataModule' import { InteractorModule } from '../src/internal/di/InteractorModule' +import { BKTEvaluationDetails } from '../src/BKTEvaluationDetails' suite('BKTClient', () => { let server: SetupServer @@ -361,6 +370,19 @@ suite('BKTClient', () => { assert(client !== null) expect(client.numberVariation('feature_id_value', 99)).toBe(99) + expect(client.booleanVariation('feature_id_value', true)).toBe(true) + expect(client.stringVariation('feature_id_value', '99')).toBe('99') + expect(client.objectVariation('feature_id_value', 1)).toStrictEqual(1) + expect(client.objectVariation('feature_id_value', true)).toStrictEqual(true) + expect( + client.objectVariation('feature_id_value', 'default_text'), + ).toStrictEqual('default_text') + expect( + client.objectVariation('feature_id_value', { k: 'v' }), + ).toStrictEqual({ k: 'v' }) + expect( + client.objectVariation('feature_id_value', [{ k: 'v' }]), + ).toStrictEqual([{ k: 'v' }]) }) }) @@ -420,6 +442,72 @@ suite('BKTClient', () => { // cases for defaultValue is covered in the above test }) + suite('objectVariation', () => { + const JSON_VALUE = '{"key": "value"}' + + test.each([ + [JSON_VALUE, {}, JSON.parse(JSON_VALUE)], + ['true', JSON.parse(JSON_VALUE), JSON.parse(JSON_VALUE)], + ['true', {}, {}], + ['not bool', {}, {}], + ['1', {}, {}], + ['{}', {}, {}], + ['[{"key": "value"}]', {}, [{ key: 'value' }]], + ['', {}, {}], + ['', 'default', 'default'], + [' ', 1, 1], + ['', true, true], + ['true', {}, {}], + ['1', 'default', 'default'], + ['false', 1, 1], + ['2', true, true], + ])( + 'value=%s, default=%s, actual=%s', + async (value, defaultValue, actual) => { + server.use( + http.post< + Record, + GetEvaluationsRequest, + GetEvaluationsResponse + >( + `${config.apiEndpoint}/get_evaluations`, + () => { + return HttpResponse.json({ + evaluations: { + ...user1Evaluations, + evaluations: [buildEvaluation(value)], + }, + userEvaluationsId: 'user_evaluation_id_value', + }) + }, + { once: true }, + ), + http.post< + Record, + RegisterEventsRequest, + RegisterEventsResponse + >(`${config.apiEndpoint}/register_events`, () => { + return HttpResponse.json({}) + }), + ) + + await initializeBKTClientInternal(component, 1000) + + const client = getBKTClient() + + assert(client !== null) + + expect( + JSON.stringify( + client.objectVariation('feature_id_value', defaultValue), + ), + ).toBe(JSON.stringify(actual)) + }, + ) + + // cases for defaultValue is covered in the above test + }) + suite('jsonVariation', () => { const JSON_VALUE = '{"key": "value"}' @@ -898,12 +986,669 @@ suite('BKTClient', () => { expect(client.evaluationDetails('non_existent_feature_id')).toBeNull() }) }) + + suite('newDefaultBKTEvaluationDetails', () => { + test.each([ + ['default true', true], + ['default false', false], + ])('value=%s, default=%s', (_value: string, defaultValue: boolean) => { + const userId = '1' + const featureId = 'featureId' + const actualEvaluationDetails = newDefaultBKTEvaluationDetails( + userId, + featureId, + defaultValue, + ) + expect(actualEvaluationDetails).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: userId, + variationId: '', + variationName: '', + variationValue: defaultValue, + reason: 'CLIENT', + }) + }) + + test.each([ + ['default 1', 1], + ['default 2.0', 2.0], + ])('value=%s, default=%s', (_value: string, defaultValue: number) => { + const userId = '1' + const featureId = 'featureId' + const actualEvaluationDetails = newDefaultBKTEvaluationDetails( + userId, + featureId, + defaultValue, + ) + expect(actualEvaluationDetails.variationValue).toBe(defaultValue) + expect(actualEvaluationDetails).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: userId, + variationId: '', + variationName: '', + variationValue: defaultValue, + reason: 'CLIENT', + }) + }) + + test.each([ + ['default 1', '1'], + ['default 2.0', '2.0'], + ])('value=%s, default=%s', (_value: string, defaultValue: string) => { + const userId = '1' + const featureId = 'featureId' + const actualEvaluationDetails = newDefaultBKTEvaluationDetails( + userId, + featureId, + defaultValue, + ) + expect(actualEvaluationDetails.variationValue).toBe(defaultValue) + expect(actualEvaluationDetails).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: userId, + variationId: '', + variationName: '', + variationValue: defaultValue, + reason: 'CLIENT', + }) + }) + + test.each([ + ['default 1', '{"key": "value"}'], + ['default 2.0', '{}'], + ])('value=%s, default=%s', (_value: string, defaultValueString: string) => { + const defaultValue = JSON.parse(defaultValueString) + const userId = '1' + const featureId = 'featureId' + const actualEvaluationDetails = newDefaultBKTEvaluationDetails( + userId, + featureId, + defaultValue, + ) + expect(JSON.stringify(actualEvaluationDetails.variationValue)).toBe( + JSON.stringify(defaultValue), + ) + expect(actualEvaluationDetails).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: userId, + variationId: '', + variationName: '', + variationValue: defaultValue, + reason: 'CLIENT', + }) + }) + }) + + suite('RawValueTransformer', () => { + test.each([ + ['default true', 'default'], + ['default false', 'default'], + ['', 'default'], + [' ', 'default'], + ['1', 'default'], + ['12', 'default'], + ['[]', 'default'], + ])( + 'convertRawValueToType value=%s, testValue=%s', + (variationValue: string, _testValue: string) => { + let result: string | null = null + const transformer = defaultStringToTypeConverter + result = transformer(variationValue) + expect(result).toStrictEqual(variationValue) + }, + ) + + test.each([ + ['default true', null, true], + ['default false', null, true], + ['', null, true], + [' ', null, true], + ['1', null, true], + ['12', null, true], + ['1.0', null, true], + ['12.0', null, true], + ['true', true, true], + ['false', false, true], + ['[]', null, true], + ['{}', null, true], + ['{"key1": "value1"}', null, true], + ])( + 'convertRawValueToType value=%s, expected=%s, testValue=%s', + ( + variationValue: string, + expected: boolean | null, + _testValue: boolean, + ) => { + let result: boolean | null = null + try { + const transformer = stringToBoolConverter + result = transformer(variationValue) + expect(result).toStrictEqual(expected) + } catch { + expect(expected).toStrictEqual(null) + } + }, + ) + + test.each([ + ['default true', null, 1], + ['default false', null, 1], + ['', null, 1], + [' ', null, 2], + ['1', 1, 1], + ['12', 12, 1], + ['1.0', 1, 1], + ['12.0', 12, 1], + ['true', null, 1], + ['false', null, 1], + ['{}', null, 1], + ['[]', null, 1], + ['{"key1": "value1"}', null, 1], + ])( + 'convertRawValueToType value=%s, expected=%s, testValue=%s', + (variationValue: string, expected: number | null, _testValue: number) => { + let result: number | null = null + try { + const transformer = stringToNumberConverter + result = transformer(variationValue) + expect(result).toStrictEqual(expected) + } catch { + expect(expected).toStrictEqual(null) + } + }, + ) + + test.each([ + ['default true', null, { key1: 'value1' }], + ['default false', null, { key2: 'value1' }], + ['', null, { key1: 'value12' }], + [' ', null, { key1: 'value1222' }], + ['1', null, {}], + ['12', null, {}], + ['1.0', null, {}], + ['12.0', null, {}], + [ + 'true', + null, + { key222: 'value1', key122: 'value1333', key121: 'value13333' }, + ], + ['false', null, {}], + ['[]', [], { key133: 'value1' }], + ['{}', {}, { key122: 'value1333', key121: 'value13333' }], + [ + '{"key1": "value1"}', + { key1: 'value1' }, + { key1: 'value1', key2: 'value1', key3: 'value1' }, + ], + [ + JSON.stringify({ key1: 'value1', key2: 'value1', key3: 'value1' }), + { key1: 'value1', key2: 'value1', key3: 'value1' }, + { key1: 'value1' }, + ], + ])( + 'convertRawValueToType value=%s, expected=%s, testValue=%s', + (variationValue: string, expected: object | null, _testValue: object) => { + let result: BKTValue | null = null + try { + const transformer = stringToObjectConverter + result = transformer(variationValue) + expect(result).toStrictEqual(expected) + } catch { + expect(expected).toStrictEqual(null) + } + }, + ) + }) + + suite('BKTEvaluationDetails', () => { + test('BKTEvaluationDetailsDefaultValue', async () => { + server.use( + http.post< + Record, + GetEvaluationsRequest, + GetEvaluationsResponse + >(`${config.apiEndpoint}/get_evaluations`, () => { + return HttpResponse.json( + { + evaluations: { + ...user1Evaluations, + evaluations: [], + }, + userEvaluationsId: 'user_evaluation_id_value', + } + ) + }), + http.post< + Record, + RegisterEventsRequest, + RegisterEventsResponse + >(`${config.apiEndpoint}/register_events`, () => { + return HttpResponse.json({}) + }), + ) + + await initializeBKTClientInternal(component, 1000) + + const client = getBKTClient() + const userId = 'user_id_1' + const featureId = 'feature_id_value' + assert(client !== null) + + expect( + client.stringVariationDetails(featureId, 'default1'), + ).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, 'default1'), + ) + + expect(client.numberVariationDetails(featureId, 22)).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, 22.0), + ) + + expect(client.booleanVariationDetails(featureId, true)).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, true), + ) + + expect(client.booleanVariationDetails(featureId, false)).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, false), + ) + + expect(client.objectVariationDetails(featureId, true)).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, true), + ) + + expect(client.objectVariationDetails(featureId, 1)).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, 1), + ) + + expect(client.objectVariationDetails(featureId, 'true')).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, 'true'), + ) + + expect( + client.objectVariationDetails(featureId, { key: 'value22' }), + ).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, { key: 'value22' }), + ) + + expect( + client.objectVariationDetails(featureId, { key: 'value' }), + ).not.toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, { key: 'value22' }), + ) + + expect(client.objectVariationDetails(featureId, [])).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, []), + ) + + expect( + client.objectVariationDetails(featureId, [{ key: 'value' }]), + ).toStrictEqual( + newDefaultBKTEvaluationDetails(userId, featureId, [{ key: 'value' }]), + ) + }) + + test('stringVariationDetails', async () => { + const featureId = 'stringVariationDetails' + const mockStringEvaluation = buildEvaluation('default', featureId) + + server.use( + http.post< + Record, + GetEvaluationsRequest, + GetEvaluationsResponse + >(`${config.apiEndpoint}/get_evaluations`, () => { + return HttpResponse.json({ + evaluations: { + ...user1Evaluations, + evaluations: [mockStringEvaluation], + }, + userEvaluationsId: 'user_evaluation_id_value', + }) + }), + http.post< + Record, + RegisterEventsRequest, + RegisterEventsResponse + >(`${config.apiEndpoint}/register_events`, () => { + return HttpResponse.json({}) + }), + ) + + await initializeBKTClientInternal(component, 1000) + + const client = getBKTClient() + assert(client !== null) + expect(client.stringVariationDetails(featureId, '')).toStrictEqual({ + featureId: mockStringEvaluation.featureId, + featureVersion: mockStringEvaluation.featureVersion, + userId: mockStringEvaluation.userId, + variationId: mockStringEvaluation.variationId, + variationName: mockStringEvaluation.variationName, + variationValue: mockStringEvaluation.variationValue, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.numberVariationDetails(featureId, 1)).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: 1, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.booleanVariationDetails(featureId, true)).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: true, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect( + client.objectVariationDetails(featureId, { key: 'value11' }), + ).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: { key: 'value11' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + }) + + test('numVariationDetails', async () => { + const featureId = 'numVariationDetails' + const mockStringEvaluation = buildEvaluation('1', featureId) + + server.use( + http.post< + Record, + GetEvaluationsRequest, + GetEvaluationsResponse + >(`${config.apiEndpoint}/get_evaluations`, () => { + return HttpResponse.json({ + evaluations: { + ...user1Evaluations, + evaluations: [mockStringEvaluation], + }, + userEvaluationsId: 'user_evaluation_id_value', + }) + }), + http.post< + Record, + RegisterEventsRequest, + RegisterEventsResponse + >(`${config.apiEndpoint}/register_events`, () => { + return HttpResponse.json({}) + }), + ) + + await initializeBKTClientInternal(component, 1000) + + const client = getBKTClient() + assert(client !== null) + expect(client.numberVariationDetails(featureId, 2)).toStrictEqual({ + featureId: mockStringEvaluation.featureId, + featureVersion: mockStringEvaluation.featureVersion, + userId: mockStringEvaluation.userId, + variationId: mockStringEvaluation.variationId, + variationName: mockStringEvaluation.variationName, + variationValue: 1, + reason: mockStringEvaluation.reason.type, + } satisfies BKTEvaluationDetails) + + expect(client.stringVariationDetails(featureId, '')).toStrictEqual({ + featureId: featureId, + featureVersion: mockStringEvaluation.featureVersion, + userId: mockStringEvaluation.userId, + variationId: mockStringEvaluation.variationId, + variationName: mockStringEvaluation.variationName, + variationValue: '1', + reason: mockStringEvaluation.reason.type, + } satisfies BKTEvaluationDetails) + + expect(client.booleanVariationDetails(featureId, true)).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: true, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect( + client.objectVariationDetails(featureId, { key: 'value11' }), + ).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: { key: 'value11' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + }) + + test('booleanVariationDetails', async () => { + const featureId = 'booleanVariationDetails' + const mockStringEvaluation = buildEvaluation('true', featureId) + + server.use( + http.post< + Record, + GetEvaluationsRequest, + GetEvaluationsResponse + >(`${config.apiEndpoint}/get_evaluations`, () => { + return HttpResponse.json({ + evaluations: { + ...user1Evaluations, + evaluations: [mockStringEvaluation], + }, + userEvaluationsId: 'user_evaluation_id_value', + }) + }), + http.post< + Record, + RegisterEventsRequest, + RegisterEventsResponse + >(`${config.apiEndpoint}/register_events`, () => { + return HttpResponse.json({}) + }), + ) + + await initializeBKTClientInternal(component, 1000) + + const client = getBKTClient() + assert(client !== null) + expect(client.booleanVariationDetails(featureId, false)).toStrictEqual({ + featureId: featureId, + featureVersion: mockStringEvaluation.featureVersion, + userId: mockStringEvaluation.userId, + variationId: mockStringEvaluation.variationId, + variationName: mockStringEvaluation.variationName, + variationValue: true, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.stringVariationDetails(featureId, '')).toStrictEqual({ + featureId: featureId, + featureVersion: mockStringEvaluation.featureVersion, + userId: mockStringEvaluation.userId, + variationId: mockStringEvaluation.variationId, + variationName: mockStringEvaluation.variationName, + variationValue: 'true', + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.numberVariationDetails(featureId, 1)).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: 1, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect( + client.objectVariationDetails(featureId, { key: 'value11' }), + ).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockStringEvaluation.userId, + variationId: '', + variationName: '', + variationValue: { key: 'value11' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + }) + + test('objectVariationDetails', async () => { + const featureId = 'objectVariationDetails' + const mockJsonObjectEvaluation = buildEvaluation( + '{"key1": "value1"}', + featureId, + ) + + const featureIdForJsonArray = 'objectVariationDetailsArray' + const mockJsonArrayEvaluation = buildEvaluation( + '[{"key1": "value1"}]', + featureIdForJsonArray, + ) + + server.use( + http.post< + Record, + GetEvaluationsRequest, + GetEvaluationsResponse + >(`${config.apiEndpoint}/get_evaluations`, () => { + return HttpResponse.json({ + evaluations: { + ...user1Evaluations, + evaluations: [ + mockJsonObjectEvaluation, + mockJsonArrayEvaluation, + ], + }, + userEvaluationsId: 'user_evaluation_id_value', + }) + }), + http.post< + Record, + RegisterEventsRequest, + RegisterEventsResponse + >(`${config.apiEndpoint}/register_events`, () => { + return HttpResponse.json({}) + }), + ) + + await initializeBKTClientInternal(component, 1000) + + const client = getBKTClient() + assert(client !== null) + + expect( + client.objectVariationDetails(featureIdForJsonArray, {}), + ).toStrictEqual({ + featureId: featureIdForJsonArray, + featureVersion: mockJsonArrayEvaluation.featureVersion, + userId: mockJsonArrayEvaluation.userId, + variationId: mockJsonArrayEvaluation.variationId, + variationName: mockJsonArrayEvaluation.variationName, + variationValue: [{ key1: 'value1' }], + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.objectVariationDetails(featureId, {})).toStrictEqual({ + featureId: mockJsonObjectEvaluation.featureId, + featureVersion: mockJsonObjectEvaluation.featureVersion, + userId: mockJsonObjectEvaluation.userId, + variationId: mockJsonObjectEvaluation.variationId, + variationName: mockJsonObjectEvaluation.variationName, + variationValue: { key1: 'value1' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.objectVariationDetails(featureId, '')).toStrictEqual({ + featureId: mockJsonObjectEvaluation.featureId, + featureVersion: mockJsonObjectEvaluation.featureVersion, + userId: mockJsonObjectEvaluation.userId, + variationId: mockJsonObjectEvaluation.variationId, + variationName: mockJsonObjectEvaluation.variationName, + variationValue: { key1: 'value1' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.objectVariationDetails(featureId, true)).toStrictEqual({ + featureId: mockJsonObjectEvaluation.featureId, + featureVersion: mockJsonObjectEvaluation.featureVersion, + userId: mockJsonObjectEvaluation.userId, + variationId: mockJsonObjectEvaluation.variationId, + variationName: mockJsonObjectEvaluation.variationName, + variationValue: { key1: 'value1' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.objectVariationDetails(featureId, 1)).toStrictEqual({ + featureId: mockJsonObjectEvaluation.featureId, + featureVersion: mockJsonObjectEvaluation.featureVersion, + userId: mockJsonObjectEvaluation.userId, + variationId: mockJsonObjectEvaluation.variationId, + variationName: mockJsonObjectEvaluation.variationName, + variationValue: { key1: 'value1' }, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.stringVariationDetails(featureId, '')).toStrictEqual({ + featureId: featureId, + featureVersion: mockJsonObjectEvaluation.featureVersion, + userId: mockJsonObjectEvaluation.userId, + variationId: mockJsonObjectEvaluation.variationId, + variationName: mockJsonObjectEvaluation.variationName, + variationValue: '{"key1": "value1"}', + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.numberVariationDetails(featureId, 1)).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockJsonObjectEvaluation.userId, + variationId: '', + variationName: '', + variationValue: 1, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + + expect(client.booleanVariationDetails(featureId, true)).toStrictEqual({ + featureId: featureId, + featureVersion: 0, + userId: mockJsonObjectEvaluation.userId, + variationId: '', + variationName: '', + variationValue: true, + reason: 'CLIENT', + } satisfies BKTEvaluationDetails) + }) + }) }) -function buildEvaluation(value: string): Evaluation { +function buildEvaluation( + value: string, + featureId: string = 'feature_id_value', +): Evaluation { return { id: 'evaluation_id_value', - featureId: 'feature_id_value', + featureId: featureId, featureVersion: 1, userId: user1.id, variationId: 'variation_id_value',