Skip to content

feat: [FFM-10880]: Allow logger to be overridden #118

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 1 commit into from
Mar 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading