Skip to content

FFM-12129 Add new event for default variation being returned #145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 16, 2024
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
```
Expand Down Expand Up @@ -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`.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
52 changes: 45 additions & 7 deletions src/__tests__/variation.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})

Expand All @@ -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)
})
})
})
8 changes: 5 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import type {
StreamEvent,
Target,
VariationFn,
VariationValue
VariationValue,
DefaultVariationEventPayload
} from './types'
import { Event } from './types'
import { defer, encodeTarget, getConfiguration } from './utils'
Expand Down Expand Up @@ -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 {
Expand All @@ -702,5 +703,6 @@ export {
EventOffBinding,
Result,
Evaluation,
VariationValue
VariationValue,
DefaultVariationEventPayload
}
9 changes: 8 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = <K extends keyof EventCallbackMapping>(event: K, callback: EventCallbackMapping[K]) => void
Expand Down
7 changes: 6 additions & 1 deletion src/variation.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
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<string, any>,
metricsHandler: (flag: string, value: any) => void,
eventBus: Emitter,
withDebug?: boolean
): VariationValue | VariationValueWithDebug {
const identifierExists = identifier in storage
const value = identifierExists ? storage[identifier] : defaultValue

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 }
Expand Down