diff --git a/README.md b/README.md index 2e431a1..09e56cd 100644 --- a/README.md +++ b/README.md @@ -213,7 +213,7 @@ client.on(Event.ERROR_STREAM, error => { ### Getting value for a particular feature flag -If you would like to know that the default variation was returned when getting the value, for example, if the provided flag identifier wasn't found then pass true for the third argument withDebug: +If you would like to know that the default variation was returned when getting the value, for example, if the provided flag wasn't found in the cache then pass true for the third argument withDebug: ```typescript const result = client.variation('Dark_Theme', false, true); ``` @@ -248,6 +248,23 @@ For the example above: - If the flag identifier 'Dark_Theme' exists in storage, variationValue would be the stored value for that identifier. - If the flag identifier 'Dark_Theme' does not exist, variationValue would be the default value provided, in this case, false + +* Note the reasons for the default variation being returned can be + 1. SDK Not Initialized Yet + 2. Typo in Flag Identifier + 3. Wrong project API key being used + +#### Listening for the `ERROR_DEFAULT_VARIATION_RETURNED` event +You can also listen for the `ERROR_DEFAULT_VARIATION_RETURNED` event, which is emitted whenever a default variation is returned because the flag has not been found in the cache. This is useful for logging or taking other action when a flag is not found. + +Example of listening for the event: + +```typescript +client.on(Event.ERROR_DEFAULT_VARIATION_RETURNED, ({ flag, defaultVariation }) => { + console.warn(`Default variation returned for flag: ${flag}, value: ${defaultVariation}`) +}) +``` + ### Cleaning up Remove a listener of an event by `client.off`. diff --git a/package-lock.json b/package-lock.json index d8b55ae..009e7f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@harnessio/ff-javascript-client-sdk", - "version": "1.28.0", + "version": "1.29.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@harnessio/ff-javascript-client-sdk", - "version": "1.28.0", + "version": "1.29.0", "license": "Apache-2.0", "dependencies": { "jwt-decode": "^3.1.2", diff --git a/package.json b/package.json index 9ebfd1f..c358033 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@harnessio/ff-javascript-client-sdk", - "version": "1.28.0", + "version": "1.29.0", "author": "Harness", "license": "Apache-2.0", "main": "dist/sdk.cjs.js", diff --git a/src/__tests__/variation.test.ts b/src/__tests__/variation.test.ts index 9d4c406..ef7c12b 100644 --- a/src/__tests__/variation.test.ts +++ b/src/__tests__/variation.test.ts @@ -1,25 +1,45 @@ import { getVariation } from '../variation' +import type { Emitter } from 'mitt' +import { type DefaultVariationEventPayload, Event } from '../types' describe('getVariation', () => { describe('without debug', () => { it('should return the stored value when it exists', () => { const storage = { testFlag: true, otherFlag: true, anotherFlag: false } const mockMetricsHandler = jest.fn() + const mockEventBus: Emitter = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + all: new Map() + } - const result = getVariation('testFlag', false, storage, mockMetricsHandler) + const result = getVariation('testFlag', false, storage, mockMetricsHandler, mockEventBus) expect(result).toBe(true) expect(mockMetricsHandler).toHaveBeenCalledWith('testFlag', true) + expect(mockEventBus.emit).not.toHaveBeenCalled() }) - it('should return the default value when stored value is undefined', () => { + it('should return the default value and emit event when it is missing', () => { const storage = {} const mockMetricsHandler = jest.fn() + const mockEventBus: Emitter = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + all: new Map() + } - const result = getVariation('testFlag', false, storage, mockMetricsHandler) + const defaultValue = false + const result = getVariation('testFlag', defaultValue, storage, mockMetricsHandler, mockEventBus) - expect(result).toBe(false) + expect(result).toBe(defaultValue) expect(mockMetricsHandler).not.toHaveBeenCalled() + + const expectedEvent: DefaultVariationEventPayload = { flag: 'testFlag', defaultVariation: defaultValue } + + expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, expectedEvent) }) }) @@ -29,21 +49,39 @@ describe('getVariation', () => { it('should return debug type with stored value', () => { const storage = { testFlag: true, otherFlag: true, anotherFlag: false } const mockMetricsHandler = jest.fn() + const mockEventBus: Emitter = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + all: new Map() + } - const result = getVariation('testFlag', false, storage, mockMetricsHandler, true) + const result = getVariation('testFlag', false, storage, mockMetricsHandler, mockEventBus, true) expect(result).toEqual({ value: true, isDefaultValue: false }) expect(mockMetricsHandler).toHaveBeenCalledWith(flagIdentifier, true) + expect(mockEventBus.emit).not.toHaveBeenCalled() }) it('should return debug type with default value when flag is missing', () => { const storage = { otherFlag: true } const mockMetricsHandler = jest.fn() + const mockEventBus: Emitter = { + emit: jest.fn(), + on: jest.fn(), + off: jest.fn(), + all: new Map() + } - const result = getVariation('testFlag', false, storage, mockMetricsHandler, true) + const defaultValue = false + const result = getVariation('testFlag', defaultValue, storage, mockMetricsHandler, mockEventBus, true) - expect(result).toEqual({ value: false, isDefaultValue: true }) + expect(result).toEqual({ value: defaultValue, isDefaultValue: true }) expect(mockMetricsHandler).not.toHaveBeenCalled() + + const expectedEvent: DefaultVariationEventPayload = { flag: 'testFlag', defaultVariation: defaultValue } + + expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, expectedEvent) }) }) }) diff --git a/src/index.ts b/src/index.ts index ccaced9..7b7225d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,8 @@ import type { StreamEvent, Target, VariationFn, - VariationValue + VariationValue, + DefaultVariationEventPayload } from './types' import { Event } from './types' import { defer, encodeTarget, getConfiguration } from './utils' @@ -678,7 +679,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result = } const variation = (identifier: string, defaultValue: any, withDebug = false) => { - return getVariation(identifier, defaultValue, storage, handleMetrics, withDebug) + return getVariation(identifier, defaultValue, storage, handleMetrics, eventBus, withDebug) } return { @@ -702,5 +703,6 @@ export { EventOffBinding, Result, Evaluation, - VariationValue + VariationValue, + DefaultVariationEventPayload } diff --git a/src/types.ts b/src/types.ts index 8ad658e..bdc6c7f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -29,7 +29,8 @@ export enum Event { ERROR_AUTH = 'auth error', ERROR_FETCH_FLAGS = 'fetch flags error', ERROR_FETCH_FLAG = 'fetch flag error', - ERROR_STREAM = 'stream error' + ERROR_STREAM = 'stream error', + ERROR_DEFAULT_VARIATION_RETURNED = 'default variation returned' } export type VariationValue = boolean | string | number | object | undefined @@ -41,6 +42,11 @@ export interface VariationValueWithDebug { isDefaultValue: boolean } +export interface DefaultVariationEventPayload { + flag: string + defaultVariation: VariationValue +} + export interface Evaluation { flag: string // Feature flag identifier identifier: string // variation identifier @@ -67,6 +73,7 @@ export interface EventCallbackMapping { [Event.ERROR_FETCH_FLAG]: (error: unknown) => void [Event.ERROR_STREAM]: (error: unknown) => void [Event.ERROR_METRICS]: (error: unknown) => void + [Event.ERROR_DEFAULT_VARIATION_RETURNED]: (payload: DefaultVariationEventPayload) => void } export type EventOnBinding = (event: K, callback: EventCallbackMapping[K]) => void diff --git a/src/variation.ts b/src/variation.ts index de7c04b..2012f7e 100644 --- a/src/variation.ts +++ b/src/variation.ts @@ -1,10 +1,12 @@ -import type { VariationValue, VariationValueWithDebug } from './types' +import { type DefaultVariationEventPayload, Event, type VariationValue, type VariationValueWithDebug } from './types' +import type { Emitter } from 'mitt' export function getVariation( identifier: string, defaultValue: any, storage: Record, metricsHandler: (flag: string, value: any) => void, + eventBus: Emitter, withDebug?: boolean ): VariationValue | VariationValueWithDebug { const identifierExists = identifier in storage @@ -12,6 +14,9 @@ export function getVariation( if (identifierExists) { metricsHandler(identifier, value) + } else { + const payload: DefaultVariationEventPayload = { flag: identifier, defaultVariation: defaultValue } + eventBus.emit(Event.ERROR_DEFAULT_VARIATION_RETURNED, payload) } return !withDebug ? value : { value, isDefaultValue: !identifierExists }