Skip to content

Commit

Permalink
feat: [FFM-10880]: Allow logger to be overridden (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
knagurski authored Mar 6, 2024
1 parent e6fe2d2 commit b4fea23
Show file tree
Hide file tree
Showing 13 changed files with 198 additions and 102 deletions.
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ interface Options {
streamEnabled?: boolean
debug?: boolean,
cache?: boolean | CacheOptions
logger?: Logger
}
```

Expand Down Expand Up @@ -303,6 +304,38 @@ interface Evaluation {
}
```

## Logging
By default, the Javascript Client SDK will log errors and debug messages using the `console` object. In some cases, it
can be useful to instead log to a service or silently fail without logging errors.

```typescript
const myLogger = {
debug: (...data) => {
// do something with the logged debug message
},
info: (...data) => {
// do something with the logged info message
},
error: (...data) => {
// do something with the logged error message
},
warn: (...data) => {
// do something with the logged warning message
}
}

const client = initialize(
'00000000-1111-2222-3333-444444444444',
{
identifier: YOUR_TARGET_IDENTIFIER,
name: YOUR_TARGET_NAME
},
{
logger: myLogger // override logger
}
)
```

## Import directly from unpkg

In case you want to import this library directly (without having to use npm or yarn):
Expand Down
2 changes: 1 addition & 1 deletion examples/preact/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 examples/react-redux/package-lock.json

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

3 changes: 1 addition & 2 deletions examples/react/package-lock.json

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

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.25.0",
"version": "1.26.0",
"author": "Harness",
"license": "Apache-2.0",
"main": "dist/sdk.cjs.js",
Expand Down
19 changes: 13 additions & 6 deletions src/__tests__/poller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,19 @@ import Poller from '../poller'
import type { FetchFlagsResult, Options } from '../types'
import { getRandom } from '../utils'
import { Event } from '../types'
import type { Emitter } from 'mitt'

jest.useFakeTimers()

jest.mock('../utils.ts', () => ({
getRandom: jest.fn(),
logError: jest.fn()
getRandom: jest.fn()
}))

const mockEventBus = {
emit: jest.fn()
const mockEventBus: Emitter = {
emit: jest.fn(),
on: jest.fn(),
off: jest.fn(),
all: new Map()
}

interface PollerArgs {
Expand All @@ -27,6 +30,9 @@ interface TestArgs {
delayMs: number
}

const logError = jest.fn()
const logDebug = jest.fn()

let currentPoller: Poller
const getPoller = (overrides: Partial<PollerArgs> = {}): Poller => {
const args: PollerArgs = {
Expand All @@ -36,7 +42,7 @@ const getPoller = (overrides: Partial<PollerArgs> = {}): Poller => {
...overrides
}

currentPoller = new Poller(args.fetchFlags, args.configurations, args.eventBus)
currentPoller = new Poller(args.fetchFlags, args.configurations, args.eventBus, logDebug, logError)

return currentPoller
}
Expand All @@ -47,7 +53,7 @@ const getTestArgs = (overrides: Partial<TestArgs> = {}): TestArgs => {
return {
delayMs,
delayFunction: (getRandom as jest.Mock).mockReturnValue(delayMs),
logSpy: jest.spyOn(console, 'debug').mockImplementation(() => {}),
logSpy: logDebug,
mockError: new Error('Fetch Error'),
...overrides
}
Expand All @@ -58,6 +64,7 @@ describe('Poller', () => {
currentPoller.stop()
jest.clearAllMocks()
})

it('should not start polling if it is already polling', () => {
getPoller({ configurations: { debug: true } })
const testArgs = getTestArgs()
Expand Down
42 changes: 32 additions & 10 deletions src/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { getConfiguration, MIN_EVENTS_SYNC_INTERVAL, MIN_POLLING_INTERVAL } from '../utils'
import type { Logger } from '../types'

describe('utils', () => {
describe('getConfiguration', () => {
test('it should set defaults', async () => {
expect(getConfiguration({})).toEqual({
debug: false,
baseUrl: 'https://config.ff.harness.io/api/1.0',
eventUrl: 'https://events.ff.harness.io/api/1.0',
eventsSyncInterval: MIN_EVENTS_SYNC_INTERVAL,
pollingInterval: MIN_POLLING_INTERVAL,
streamEnabled: true,
pollingEnabled: true,
cache: false
})
expect(getConfiguration({})).toEqual(
expect.objectContaining({
debug: false,
baseUrl: 'https://config.ff.harness.io/api/1.0',
eventUrl: 'https://events.ff.harness.io/api/1.0',
eventsSyncInterval: MIN_EVENTS_SYNC_INTERVAL,
pollingInterval: MIN_POLLING_INTERVAL,
streamEnabled: true,
pollingEnabled: true,
cache: false
})
)
})

test('it should enable polling when streaming is enabled', async () => {
Expand Down Expand Up @@ -54,5 +57,24 @@ describe('utils', () => {
test('it should allow pollingInterval to be set above 60s', async () => {
expect(getConfiguration({ pollingInterval: 100000 })).toHaveProperty('pollingInterval', 100000)
})

test('it should use console as a logger by default', async () => {
expect(getConfiguration({})).toHaveProperty('logger', console)
})

test('it should allow the default logger to be overridden', async () => {
const logger: Logger = {
debug: jest.fn(),
error: jest.fn(),
info: jest.fn(),
warn: jest.fn()
}

const result = getConfiguration({ logger })
expect(result).toHaveProperty('logger', logger)

result.logger.debug('hello')
expect(logger.debug).toHaveBeenCalledWith('hello')
})
})
})
80 changes: 47 additions & 33 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
VariationValue
} from './types'
import { Event } from './types'
import { defer, getConfiguration, logError } from './utils'
import { defer, getConfiguration } from './utils'
import { addMiddlewareToFetch } from './request'
import { Streamer } from './stream'
import { getVariation } from './variation'
Expand All @@ -30,29 +30,6 @@ const fetch = globalThis.fetch
// Flag to detect is Proxy is supported (not under IE 11)
const hasProxy = !!globalThis.Proxy

const convertValue = (evaluation: Evaluation) => {
let { value } = evaluation

try {
switch (evaluation.kind.toLowerCase()) {
case 'int':
case 'number':
value = Number(value)
break
case 'boolean':
value = value.toString().toLowerCase() === 'true'
break
case 'json':
value = JSON.parse(value as string)
break
}
} catch (error) {
logError(error)
}

return value
}

const initialize = (apiKey: string, target: Target, options?: Options): Result => {
let closed = false
let environment: string
Expand All @@ -79,10 +56,37 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =

const logDebug = (message: string, ...args: any[]) => {
if (configurations.debug) {
// tslint:disable-next-line:no-console
console.debug(`[FF-SDK] ${message}`, ...args)
configurations.logger.debug(`[FF-SDK] ${message}`, ...args)
}
}

const logError = (message: string, ...args: any[]) => {
configurations.logger.error(`[FF-SDK] ${message}`, ...args)
}

const convertValue = (evaluation: Evaluation) => {
let { value } = evaluation

try {
switch (evaluation.kind.toLowerCase()) {
case 'int':
case 'number':
value = Number(value)
break
case 'boolean':
value = value.toString().toLowerCase() === 'true'
break
case 'json':
value = JSON.parse(value as string)
break
}
} catch (error) {
logError(error)
}

return value
}

const updateMetrics = (metricsInfo: MetricsInfo) => {
if (metricsCollectorEnabled) {
const now = Date.now()
Expand Down Expand Up @@ -460,7 +464,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
}

// We instantiate the Poller here so it can be used as a fallback for streaming, but we don't start it yet.
poller = new Poller(fetchFlags, configurations, eventBus)
poller = new Poller(fetchFlags, configurations, eventBus, logDebug, logError)

const startStream = () => {
const handleFlagEvent = (event: StreamEvent): void => {
Expand Down Expand Up @@ -526,13 +530,23 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =

const url = `${configurations.baseUrl}/stream?cluster=${clusterIdentifier}`

eventSource = new Streamer(eventBus, configurations, url, apiKey, standardHeaders, poller, event => {
if (event.domain === 'flag') {
handleFlagEvent(event)
} else if (event.domain === 'target-segment') {
handleSegmentEvent(event)
eventSource = new Streamer(
eventBus,
configurations,
url,
apiKey,
standardHeaders,
poller,
logDebug,
logError,
event => {
if (event.domain === 'flag') {
handleFlagEvent(event)
} else if (event.domain === 'target-segment') {
handleSegmentEvent(event)
}
}
})
)
eventSource.start()
}

Expand Down
Loading

0 comments on commit b4fea23

Please sign in to comment.