From e59de307ada5ef3addc3343c159cf1e14dbdceb2 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 15:23:50 +0100 Subject: [PATCH 01/12] FFM-12129 Add new event for default variation being returbed --- README.md | 19 +++++++++++- src/__tests__/variation.test.ts | 53 ++++++++++++++++++++++++++++----- src/index.ts | 2 +- src/types.ts | 3 +- src/variation.ts | 6 +++- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 2e431a12..d1375d19 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, valueReturned }) => { + console.warn(`Default variation returned for flag: ${flag}, value: ${valueReturned}`) +}) +``` + ### Cleaning up Remove a listener of an event by `client.off`. diff --git a/src/__tests__/variation.test.ts b/src/__tests__/variation.test.ts index 9d4c4065..049307e0 100644 --- a/src/__tests__/variation.test.ts +++ b/src/__tests__/variation.test.ts @@ -1,25 +1,46 @@ import { getVariation } from '../variation' +import type { Emitter } from 'mitt' +import {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() + + expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, { + flag: 'testFlag', + valueReturned: defaultValue + }) }) }) @@ -29,21 +50,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() + expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, { + flag: 'testFlag', + valueReturned: defaultValue + }) }) }) }) diff --git a/src/index.ts b/src/index.ts index ccaced9d..38991670 100644 --- a/src/index.ts +++ b/src/index.ts @@ -678,7 +678,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 { diff --git a/src/types.ts b/src/types.ts index 8ad658eb..e36064ee 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 diff --git a/src/variation.ts b/src/variation.ts index de7c04b9..3f7e527a 100644 --- a/src/variation.ts +++ b/src/variation.ts @@ -1,10 +1,12 @@ -import type { VariationValue, VariationValueWithDebug } from './types' +import {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,8 @@ export function getVariation( if (identifierExists) { metricsHandler(identifier, value) + } else { + eventBus.emit(Event.ERROR_DEFAULT_VARIATION_RETURNED, { flag: identifier, valueReturned: defaultValue }) } return !withDebug ? value : { value, isDefaultValue: !identifierExists } From 162e4e7b70e29f851637ddb03c60faa5bdfe4bc7 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 15:39:11 +0100 Subject: [PATCH 02/12] FFM-12129 Prettier --- src/__tests__/variation.test.ts | 6 +++--- src/variation.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/__tests__/variation.test.ts b/src/__tests__/variation.test.ts index 049307e0..15c382dc 100644 --- a/src/__tests__/variation.test.ts +++ b/src/__tests__/variation.test.ts @@ -1,6 +1,6 @@ import { getVariation } from '../variation' import type { Emitter } from 'mitt' -import {Event} from "../types"; +import { Event } from '../types' describe('getVariation', () => { describe('without debug', () => { @@ -31,7 +31,7 @@ describe('getVariation', () => { all: new Map() } - const defaultValue = false; + const defaultValue = false const result = getVariation('testFlag', defaultValue, storage, mockMetricsHandler, mockEventBus) expect(result).toBe(defaultValue) @@ -74,7 +74,7 @@ describe('getVariation', () => { all: new Map() } - const defaultValue = false; + const defaultValue = false const result = getVariation('testFlag', defaultValue, storage, mockMetricsHandler, mockEventBus, true) expect(result).toEqual({ value: defaultValue, isDefaultValue: true }) diff --git a/src/variation.ts b/src/variation.ts index 3f7e527a..df9b559b 100644 --- a/src/variation.ts +++ b/src/variation.ts @@ -1,5 +1,5 @@ -import {Event, type VariationValue, type VariationValueWithDebug} from './types' -import type {Emitter} from "mitt"; +import { Event, type VariationValue, type VariationValueWithDebug } from './types' +import type { Emitter } from 'mitt' export function getVariation( identifier: string, From 399ebb679eb145e88c70bfd2698f851159011267 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 15:48:46 +0100 Subject: [PATCH 03/12] FFM-12129 Type new event --- src/types.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types.ts b/src/types.ts index e36064ee..61ddd574 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,6 +68,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: { flag: string; valueReturned: VariationValue }) => void } export type EventOnBinding = (event: K, callback: EventCallbackMapping[K]) => void From 0e5c98e70f9abafe0764fd772ec8a33cbfd37e79 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 15:53:26 +0100 Subject: [PATCH 04/12] FFM-12129 Bump version --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8b55aef..009e7f35 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 9ebfd1f5..c3580339 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", From 3757d86db989aea6942d64ecba77eb4f3ac82d7d Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 16:03:55 +0100 Subject: [PATCH 05/12] FFM-12129 Change type name --- README.md | 4 ++-- src/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d1375d19..09e56cdf 100644 --- a/README.md +++ b/README.md @@ -260,8 +260,8 @@ You can also listen for the `ERROR_DEFAULT_VARIATION_RETURNED` event, which is e Example of listening for the event: ```typescript -client.on(Event.ERROR_DEFAULT_VARIATION_RETURNED, ({ flag, valueReturned }) => { - console.warn(`Default variation returned for flag: ${flag}, value: ${valueReturned}`) +client.on(Event.ERROR_DEFAULT_VARIATION_RETURNED, ({ flag, defaultVariation }) => { + console.warn(`Default variation returned for flag: ${flag}, value: ${defaultVariation}`) }) ``` diff --git a/src/types.ts b/src/types.ts index 61ddd574..59e512f0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -68,7 +68,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: { flag: string; valueReturned: VariationValue }) => void + [Event.ERROR_DEFAULT_VARIATION_RETURNED]: (payload: { flag: string; defaultVariation: VariationValue }) => void } export type EventOnBinding = (event: K, callback: EventCallbackMapping[K]) => void From ecf2b2526320f7f5bf69720f659b886eb639fca2 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 17:17:22 +0100 Subject: [PATCH 06/12] FFM-12129 Add prepare script to build the SDK --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c3580339..5d73b844 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,8 @@ "format": "prettier --write \"src/**/*.ts\"", "type": "tsc ./src/*.ts --declaration --emitDeclarationOnly --outDir dist --lib ES2015,DOM", "clean": "rm -rf ./dist", - "test": "jest" + "test": "jest", + "prepare": "npm run build" }, "devDependencies": { "@types/jest": "^29.5.4", From 11ad602b7635f6f4bb688b5a71c661cfa96d749a Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 17:46:15 +0100 Subject: [PATCH 07/12] Revert "FFM-12129 Add prepare script to build the SDK" This reverts commit ecf2b2526320f7f5bf69720f659b886eb639fca2. --- package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/package.json b/package.json index 5d73b844..c3580339 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,7 @@ "format": "prettier --write \"src/**/*.ts\"", "type": "tsc ./src/*.ts --declaration --emitDeclarationOnly --outDir dist --lib ES2015,DOM", "clean": "rm -rf ./dist", - "test": "jest", - "prepare": "npm run build" + "test": "jest" }, "devDependencies": { "@types/jest": "^29.5.4", From c29f13e0ea89b0512f4c8fea238a28d4c6e1c07a Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 19:24:41 +0100 Subject: [PATCH 08/12] FFM-12129 Type the default return --- src/types.ts | 5 +++++ src/variation.ts | 6 +++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/types.ts b/src/types.ts index 59e512f0..16e6e390 100644 --- a/src/types.ts +++ b/src/types.ts @@ -42,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 diff --git a/src/variation.ts b/src/variation.ts index df9b559b..529d3eea 100644 --- a/src/variation.ts +++ b/src/variation.ts @@ -1,4 +1,4 @@ -import { Event, type VariationValue, type VariationValueWithDebug } from './types' +import {type DefaultVariationEventPayload, Event, type VariationValue, type VariationValueWithDebug} from './types' import type { Emitter } from 'mitt' export function getVariation( @@ -15,8 +15,8 @@ export function getVariation( if (identifierExists) { metricsHandler(identifier, value) } else { - eventBus.emit(Event.ERROR_DEFAULT_VARIATION_RETURNED, { flag: identifier, valueReturned: defaultValue }) - } + const payload: DefaultVariationEventPayload = { flag: identifier, defaultVariation: defaultValue } + eventBus.emit(Event.ERROR_DEFAULT_VARIATION_RETURNED, payload) } return !withDebug ? value : { value, isDefaultValue: !identifierExists } } From a6d3025b85e0f457a24da0f6d4b3f6c99e345f62 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 19:25:14 +0100 Subject: [PATCH 09/12] FFM-12129 Type the default return --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 16e6e390..bdc6c7fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -73,7 +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: { flag: string; defaultVariation: VariationValue }) => void + [Event.ERROR_DEFAULT_VARIATION_RETURNED]: (payload: DefaultVariationEventPayload) => void } export type EventOnBinding = (event: K, callback: EventCallbackMapping[K]) => void From bfa177c0adf6c6c4df7476c20b203afdd44a513d Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 19:27:06 +0100 Subject: [PATCH 10/12] FFM-12129 Fix tests --- src/__tests__/variation.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/variation.test.ts b/src/__tests__/variation.test.ts index 15c382dc..037133b5 100644 --- a/src/__tests__/variation.test.ts +++ b/src/__tests__/variation.test.ts @@ -39,7 +39,7 @@ describe('getVariation', () => { expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, { flag: 'testFlag', - valueReturned: defaultValue + defaultVariation: defaultValue }) }) }) @@ -81,7 +81,7 @@ describe('getVariation', () => { expect(mockMetricsHandler).not.toHaveBeenCalled() expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, { flag: 'testFlag', - valueReturned: defaultValue + defaultVariation: defaultValue }) }) }) From 83fb465264a40d23b6b4af63c36851d78c7e8614 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Tue, 15 Oct 2024 19:29:58 +0100 Subject: [PATCH 11/12] FFM-12129 Harden tests --- src/__tests__/variation.test.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/__tests__/variation.test.ts b/src/__tests__/variation.test.ts index 037133b5..2f075be8 100644 --- a/src/__tests__/variation.test.ts +++ b/src/__tests__/variation.test.ts @@ -1,6 +1,6 @@ import { getVariation } from '../variation' import type { Emitter } from 'mitt' -import { Event } from '../types' +import {type DefaultVariationEventPayload, Event} from '../types' describe('getVariation', () => { describe('without debug', () => { @@ -37,10 +37,9 @@ describe('getVariation', () => { expect(result).toBe(defaultValue) expect(mockMetricsHandler).not.toHaveBeenCalled() - expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, { - flag: 'testFlag', - defaultVariation: defaultValue - }) + const expectedEvent: DefaultVariationEventPayload = { flag: 'testFlag', defaultVariation: defaultValue } + + expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, expectedEvent) }) }) @@ -79,10 +78,10 @@ describe('getVariation', () => { expect(result).toEqual({ value: defaultValue, isDefaultValue: true }) expect(mockMetricsHandler).not.toHaveBeenCalled() - expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, { - flag: 'testFlag', - defaultVariation: defaultValue - }) + + const expectedEvent: DefaultVariationEventPayload = { flag: 'testFlag', defaultVariation: defaultValue } + + expect(mockEventBus.emit).toHaveBeenCalledWith(Event.ERROR_DEFAULT_VARIATION_RETURNED, expectedEvent) }) }) }) From d78317b9e39df9238ed10e3e8d890a1b19ad1098 Mon Sep 17 00:00:00 2001 From: Erdi Rowlands Date: Wed, 16 Oct 2024 12:33:25 +0100 Subject: [PATCH 12/12] FFM-12129 Harden types --- src/__tests__/variation.test.ts | 2 +- src/index.ts | 6 ++++-- src/variation.ts | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/__tests__/variation.test.ts b/src/__tests__/variation.test.ts index 2f075be8..ef7c12bb 100644 --- a/src/__tests__/variation.test.ts +++ b/src/__tests__/variation.test.ts @@ -1,6 +1,6 @@ import { getVariation } from '../variation' import type { Emitter } from 'mitt' -import {type DefaultVariationEventPayload, Event} from '../types' +import { type DefaultVariationEventPayload, Event } from '../types' describe('getVariation', () => { describe('without debug', () => { diff --git a/src/index.ts b/src/index.ts index 38991670..7b7225d5 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' @@ -702,5 +703,6 @@ export { EventOffBinding, Result, Evaluation, - VariationValue + VariationValue, + DefaultVariationEventPayload } diff --git a/src/variation.ts b/src/variation.ts index 529d3eea..2012f7e1 100644 --- a/src/variation.ts +++ b/src/variation.ts @@ -1,4 +1,4 @@ -import {type DefaultVariationEventPayload, Event, type VariationValue, type VariationValueWithDebug} from './types' +import { type DefaultVariationEventPayload, Event, type VariationValue, type VariationValueWithDebug } from './types' import type { Emitter } from 'mitt' export function getVariation( @@ -16,7 +16,8 @@ export function getVariation( metricsHandler(identifier, value) } else { const payload: DefaultVariationEventPayload = { flag: identifier, defaultVariation: defaultValue } - eventBus.emit(Event.ERROR_DEFAULT_VARIATION_RETURNED, payload) } + eventBus.emit(Event.ERROR_DEFAULT_VARIATION_RETURNED, payload) + } return !withDebug ? value : { value, isDefaultValue: !identifierExists } }