Skip to content

Commit

Permalink
FFM-12129 Add new event for default variation being returned (#145)
Browse files Browse the repository at this point in the history
  • Loading branch information
erdirowlands authored Oct 16, 2024
1 parent 6b8ded4 commit 06ddd83
Show file tree
Hide file tree
Showing 7 changed files with 85 additions and 16 deletions.
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

0 comments on commit 06ddd83

Please sign in to comment.