Skip to content

Commit b4fea23

Browse files
authoredMar 6, 2024··
feat: [FFM-10880]: Allow logger to be overridden (#118)
1 parent e6fe2d2 commit b4fea23

13 files changed

+198
-102
lines changed
 

Diff for: ‎README.md

+33
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface Options {
5757
streamEnabled?: boolean
5858
debug?: boolean,
5959
cache?: boolean | CacheOptions
60+
logger?: Logger
6061
}
6162
```
6263

@@ -303,6 +304,38 @@ interface Evaluation {
303304
}
304305
```
305306

307+
## Logging
308+
By default, the Javascript Client SDK will log errors and debug messages using the `console` object. In some cases, it
309+
can be useful to instead log to a service or silently fail without logging errors.
310+
311+
```typescript
312+
const myLogger = {
313+
debug: (...data) => {
314+
// do something with the logged debug message
315+
},
316+
info: (...data) => {
317+
// do something with the logged info message
318+
},
319+
error: (...data) => {
320+
// do something with the logged error message
321+
},
322+
warn: (...data) => {
323+
// do something with the logged warning message
324+
}
325+
}
326+
327+
const client = initialize(
328+
'00000000-1111-2222-3333-444444444444',
329+
{
330+
identifier: YOUR_TARGET_IDENTIFIER,
331+
name: YOUR_TARGET_NAME
332+
},
333+
{
334+
logger: myLogger // override logger
335+
}
336+
)
337+
```
338+
306339
## Import directly from unpkg
307340

308341
In case you want to import this library directly (without having to use npm or yarn):

Diff for: ‎examples/preact/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎examples/react-redux/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎examples/react/package-lock.json

+1-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: ‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@harnessio/ff-javascript-client-sdk",
3-
"version": "1.25.0",
3+
"version": "1.26.0",
44
"author": "Harness",
55
"license": "Apache-2.0",
66
"main": "dist/sdk.cjs.js",

Diff for: ‎src/__tests__/poller.test.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,19 @@ import Poller from '../poller'
22
import type { FetchFlagsResult, Options } from '../types'
33
import { getRandom } from '../utils'
44
import { Event } from '../types'
5+
import type { Emitter } from 'mitt'
56

67
jest.useFakeTimers()
78

89
jest.mock('../utils.ts', () => ({
9-
getRandom: jest.fn(),
10-
logError: jest.fn()
10+
getRandom: jest.fn()
1111
}))
1212

13-
const mockEventBus = {
14-
emit: jest.fn()
13+
const mockEventBus: Emitter = {
14+
emit: jest.fn(),
15+
on: jest.fn(),
16+
off: jest.fn(),
17+
all: new Map()
1518
}
1619

1720
interface PollerArgs {
@@ -27,6 +30,9 @@ interface TestArgs {
2730
delayMs: number
2831
}
2932

33+
const logError = jest.fn()
34+
const logDebug = jest.fn()
35+
3036
let currentPoller: Poller
3137
const getPoller = (overrides: Partial<PollerArgs> = {}): Poller => {
3238
const args: PollerArgs = {
@@ -36,7 +42,7 @@ const getPoller = (overrides: Partial<PollerArgs> = {}): Poller => {
3642
...overrides
3743
}
3844

39-
currentPoller = new Poller(args.fetchFlags, args.configurations, args.eventBus)
45+
currentPoller = new Poller(args.fetchFlags, args.configurations, args.eventBus, logDebug, logError)
4046

4147
return currentPoller
4248
}
@@ -47,7 +53,7 @@ const getTestArgs = (overrides: Partial<TestArgs> = {}): TestArgs => {
4753
return {
4854
delayMs,
4955
delayFunction: (getRandom as jest.Mock).mockReturnValue(delayMs),
50-
logSpy: jest.spyOn(console, 'debug').mockImplementation(() => {}),
56+
logSpy: logDebug,
5157
mockError: new Error('Fetch Error'),
5258
...overrides
5359
}
@@ -58,6 +64,7 @@ describe('Poller', () => {
5864
currentPoller.stop()
5965
jest.clearAllMocks()
6066
})
67+
6168
it('should not start polling if it is already polling', () => {
6269
getPoller({ configurations: { debug: true } })
6370
const testArgs = getTestArgs()

Diff for: ‎src/__tests__/utils.test.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
import { getConfiguration, MIN_EVENTS_SYNC_INTERVAL, MIN_POLLING_INTERVAL } from '../utils'
2+
import type { Logger } from '../types'
23

34
describe('utils', () => {
45
describe('getConfiguration', () => {
56
test('it should set defaults', async () => {
6-
expect(getConfiguration({})).toEqual({
7-
debug: false,
8-
baseUrl: 'https://config.ff.harness.io/api/1.0',
9-
eventUrl: 'https://events.ff.harness.io/api/1.0',
10-
eventsSyncInterval: MIN_EVENTS_SYNC_INTERVAL,
11-
pollingInterval: MIN_POLLING_INTERVAL,
12-
streamEnabled: true,
13-
pollingEnabled: true,
14-
cache: false
15-
})
7+
expect(getConfiguration({})).toEqual(
8+
expect.objectContaining({
9+
debug: false,
10+
baseUrl: 'https://config.ff.harness.io/api/1.0',
11+
eventUrl: 'https://events.ff.harness.io/api/1.0',
12+
eventsSyncInterval: MIN_EVENTS_SYNC_INTERVAL,
13+
pollingInterval: MIN_POLLING_INTERVAL,
14+
streamEnabled: true,
15+
pollingEnabled: true,
16+
cache: false
17+
})
18+
)
1619
})
1720

1821
test('it should enable polling when streaming is enabled', async () => {
@@ -54,5 +57,24 @@ describe('utils', () => {
5457
test('it should allow pollingInterval to be set above 60s', async () => {
5558
expect(getConfiguration({ pollingInterval: 100000 })).toHaveProperty('pollingInterval', 100000)
5659
})
60+
61+
test('it should use console as a logger by default', async () => {
62+
expect(getConfiguration({})).toHaveProperty('logger', console)
63+
})
64+
65+
test('it should allow the default logger to be overridden', async () => {
66+
const logger: Logger = {
67+
debug: jest.fn(),
68+
error: jest.fn(),
69+
info: jest.fn(),
70+
warn: jest.fn()
71+
}
72+
73+
const result = getConfiguration({ logger })
74+
expect(result).toHaveProperty('logger', logger)
75+
76+
result.logger.debug('hello')
77+
expect(logger.debug).toHaveBeenCalledWith('hello')
78+
})
5779
})
5880
})

Diff for: ‎src/index.ts

+47-33
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {
1515
VariationValue
1616
} from './types'
1717
import { Event } from './types'
18-
import { defer, getConfiguration, logError } from './utils'
18+
import { defer, getConfiguration } from './utils'
1919
import { addMiddlewareToFetch } from './request'
2020
import { Streamer } from './stream'
2121
import { getVariation } from './variation'
@@ -30,29 +30,6 @@ const fetch = globalThis.fetch
3030
// Flag to detect is Proxy is supported (not under IE 11)
3131
const hasProxy = !!globalThis.Proxy
3232

33-
const convertValue = (evaluation: Evaluation) => {
34-
let { value } = evaluation
35-
36-
try {
37-
switch (evaluation.kind.toLowerCase()) {
38-
case 'int':
39-
case 'number':
40-
value = Number(value)
41-
break
42-
case 'boolean':
43-
value = value.toString().toLowerCase() === 'true'
44-
break
45-
case 'json':
46-
value = JSON.parse(value as string)
47-
break
48-
}
49-
} catch (error) {
50-
logError(error)
51-
}
52-
53-
return value
54-
}
55-
5633
const initialize = (apiKey: string, target: Target, options?: Options): Result => {
5734
let closed = false
5835
let environment: string
@@ -79,10 +56,37 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
7956

8057
const logDebug = (message: string, ...args: any[]) => {
8158
if (configurations.debug) {
82-
// tslint:disable-next-line:no-console
83-
console.debug(`[FF-SDK] ${message}`, ...args)
59+
configurations.logger.debug(`[FF-SDK] ${message}`, ...args)
60+
}
61+
}
62+
63+
const logError = (message: string, ...args: any[]) => {
64+
configurations.logger.error(`[FF-SDK] ${message}`, ...args)
65+
}
66+
67+
const convertValue = (evaluation: Evaluation) => {
68+
let { value } = evaluation
69+
70+
try {
71+
switch (evaluation.kind.toLowerCase()) {
72+
case 'int':
73+
case 'number':
74+
value = Number(value)
75+
break
76+
case 'boolean':
77+
value = value.toString().toLowerCase() === 'true'
78+
break
79+
case 'json':
80+
value = JSON.parse(value as string)
81+
break
82+
}
83+
} catch (error) {
84+
logError(error)
8485
}
86+
87+
return value
8588
}
89+
8690
const updateMetrics = (metricsInfo: MetricsInfo) => {
8791
if (metricsCollectorEnabled) {
8892
const now = Date.now()
@@ -460,7 +464,7 @@ const initialize = (apiKey: string, target: Target, options?: Options): Result =
460464
}
461465

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

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

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

529-
eventSource = new Streamer(eventBus, configurations, url, apiKey, standardHeaders, poller, event => {
530-
if (event.domain === 'flag') {
531-
handleFlagEvent(event)
532-
} else if (event.domain === 'target-segment') {
533-
handleSegmentEvent(event)
533+
eventSource = new Streamer(
534+
eventBus,
535+
configurations,
536+
url,
537+
apiKey,
538+
standardHeaders,
539+
poller,
540+
logDebug,
541+
logError,
542+
event => {
543+
if (event.domain === 'flag') {
544+
handleFlagEvent(event)
545+
} else if (event.domain === 'target-segment') {
546+
handleSegmentEvent(event)
547+
}
534548
}
535-
})
549+
)
536550
eventSource.start()
537551
}
538552

Diff for: ‎src/poller.ts

+21-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Options } from './types'
2-
import { getRandom, logError } from './utils'
2+
import { getRandom } from './utils'
33
import { Event, FetchFlagsResult } from './types'
4+
import type { Emitter } from 'mitt'
45

56
export default class Poller {
67
private timeoutId: any
@@ -10,20 +11,21 @@ export default class Poller {
1011
constructor(
1112
private fetchFlagsFn: () => Promise<FetchFlagsResult>,
1213
private configurations: Options,
13-
private eventBus: any
14-
) // Used to emit the updates retrieved in polling intervals
15-
{}
14+
private eventBus: Emitter, // Used to emit the updates retrieved in polling intervals
15+
private logDebug: (...data: any[]) => void,
16+
private logError: (...data: any[]) => void
17+
) {}
1618

1719
public start(): void {
1820
if (this.isPolling()) {
19-
this.logDebug('Already polling.')
21+
this.logDebugMessage('Already polling.')
2022
return
2123
}
2224

2325
this.isRunning = true
2426
this.eventBus.emit(Event.POLLING)
2527

26-
this.logDebug(`Starting poller, first poll will be in ${this.configurations.pollingInterval}ms`)
28+
this.logDebugMessage(`Starting poller, first poll will be in ${this.configurations.pollingInterval}ms`)
2729

2830
// Don't start polling immediately as we have already fetched flags on client initialization
2931
this.timeoutId = setTimeout(() => this.poll(), this.configurations.pollingInterval)
@@ -40,21 +42,23 @@ export default class Poller {
4042
const result = await this.fetchFlagsFn()
4143

4244
if (result.type === 'success') {
43-
this.logDebug(`Successfully polled for flag updates, next poll in ${this.configurations.pollingInterval}ms. `)
45+
this.logDebugMessage(
46+
`Successfully polled for flag updates, next poll in ${this.configurations.pollingInterval}ms. `
47+
)
4448
return
4549
}
4650

47-
logError('Error when polling for flag updates', result.error)
51+
this.logErrorMessage('Error when polling for flag updates', result.error)
4852

4953
// Retry fetching flags
5054
if (attempt >= this.maxAttempts) {
51-
this.logDebug(
55+
this.logDebugMessage(
5256
`Maximum attempts reached for polling for flags. Next poll in ${this.configurations.pollingInterval}ms.`
5357
)
5458
return
5559
}
5660

57-
this.logDebug(
61+
this.logDebugMessage(
5862
`Polling for flags attempt #${attempt} failed. Remaining attempts: ${this.maxAttempts - attempt}`,
5963
result.error
6064
)
@@ -70,17 +74,21 @@ export default class Poller {
7074
this.timeoutId = undefined
7175
this.isRunning = false
7276
this.eventBus.emit(Event.POLLING_STOPPED)
73-
this.logDebug('Polling stopped')
77+
this.logDebugMessage('Polling stopped')
7478
}
7579
}
7680

7781
public isPolling(): boolean {
7882
return this.isRunning
7983
}
8084

81-
private logDebug(message: string, ...args: unknown[]): void {
85+
private logDebugMessage(message: string, ...args: unknown[]): void {
8286
if (this.configurations.debug) {
83-
console.debug(`[FF-SDK] Poller: ${message}`, ...args)
87+
this.logDebug(`Poller: ${message}`, ...args)
8488
}
8589
}
90+
91+
private logErrorMessage(message: string, ...args: unknown[]): void {
92+
this.logError(`Poller: ${message}`, ...args)
93+
}
8694
}

Diff for: ‎src/stream.ts

+30-30
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,28 @@
1-
import { Event, StreamEvent } from './types'
2-
import { getRandom, logError } from './utils'
1+
import { Event, type Options, StreamEvent } from './types'
2+
import { getRandom } from './utils'
33
import type Poller from './poller'
4+
import type { Emitter } from 'mitt'
45

56
const SSE_TIMEOUT_MS = 30000
67

78
export class Streamer {
8-
private readonly eventBus: any
9-
private readonly configurations: any
10-
private readonly url: string
11-
private readonly standardHeaders: any
12-
private readonly apiKey: string
13-
private readonly eventCallback: any
149
private xhr: XMLHttpRequest
1510
private closed: boolean = false
1611
private readTimeoutCheckerId: any
17-
private fallbackPoller: Poller
1812
private connectionOpened = false
1913
private disconnectEventEmitted = false
2014

21-
constructor(eventBus, configurations, url, apiKey, standardHeaders, fallbackPoller, eventCallback) {
22-
this.eventBus = eventBus
23-
this.configurations = configurations
24-
this.url = url
25-
this.apiKey = apiKey
26-
this.standardHeaders = standardHeaders
27-
this.eventCallback = eventCallback
28-
this.fallbackPoller = fallbackPoller
29-
}
15+
constructor(
16+
private eventBus: Emitter,
17+
private configurations: Options,
18+
private url: string,
19+
private apiKey: string,
20+
private standardHeaders: Record<string, string>,
21+
private fallbackPoller: Poller,
22+
private logDebug: (...data: any[]) => void,
23+
private logError: (...data: any[]) => void,
24+
private eventCallback: (e: StreamEvent) => void
25+
) {}
3026

3127
start() {
3228
const processData = (data: any): void => {
@@ -36,20 +32,20 @@ export class Streamer {
3632
const processLine = (line: string): void => {
3733
if (line.startsWith('data:')) {
3834
const event: StreamEvent = JSON.parse(line.substring(5))
39-
this.logDebug('Received event from stream: ', event)
35+
this.logDebugMessage('Received event from stream: ', event)
4036
this.eventCallback(event)
4137
}
4238
}
4339

4440
const onConnected = () => {
45-
this.logDebug('Stream connected')
41+
this.logDebugMessage('Stream connected')
4642
this.eventBus.emit(Event.CONNECTED)
4743
}
4844

4945
const onDisconnect = () => {
5046
clearInterval(this.readTimeoutCheckerId)
5147
const reconnectDelayMs = getRandom(1000, 10000)
52-
this.logDebug('Stream disconnected, will reconnect in ' + reconnectDelayMs + 'ms')
48+
this.logDebugMessage('Stream disconnected, will reconnect in ' + reconnectDelayMs + 'ms')
5349
if (!this.disconnectEventEmitted) {
5450
this.eventBus.emit(Event.DISCONNECTED)
5551
this.disconnectEventEmitted = true
@@ -59,7 +55,7 @@ export class Streamer {
5955

6056
const onFailed = (msg: string) => {
6157
if (!!msg) {
62-
logError('Stream has issue', msg)
58+
this.logErrorMessage('Stream has issue', msg)
6359
}
6460

6561
// Fallback to polling while we have a stream failure
@@ -77,7 +73,7 @@ export class Streamer {
7773
...this.standardHeaders
7874
}
7975

80-
this.logDebug('SSE HTTP start request', this.url)
76+
this.logDebugMessage('SSE HTTP start request', this.url)
8177

8278
this.xhr = new XMLHttpRequest()
8379
this.xhr.open('GET', this.url)
@@ -91,7 +87,7 @@ export class Streamer {
9187
}
9288
this.xhr.onabort = () => {
9389
this.connectionOpened = false
94-
this.logDebug('SSE aborted')
90+
this.logDebugMessage('SSE aborted')
9591
if (!this.closed) {
9692
onFailed(null)
9793
}
@@ -132,14 +128,14 @@ export class Streamer {
132128
lastActivity = Date.now()
133129
const data = this.xhr.responseText.slice(offset)
134130
offset += data.length
135-
this.logDebug('SSE GOT: ' + data)
131+
this.logDebugMessage('SSE GOT: ' + data)
136132
processData(data)
137133
}
138134

139135
this.readTimeoutCheckerId = setInterval(() => {
140136
// this task will kill and restart the SSE connection if no data or heartbeat has arrived in a while
141137
if (lastActivity < Date.now() - SSE_TIMEOUT_MS) {
142-
logError('SSE read timeout')
138+
this.logErrorMessage('SSE read timeout')
143139
this.xhr.abort()
144140
}
145141
}, SSE_TIMEOUT_MS)
@@ -163,21 +159,25 @@ export class Streamer {
163159

164160
private fallBackToPolling() {
165161
if (!this.fallbackPoller.isPolling() && this.configurations.pollingEnabled) {
166-
this.logDebug('Falling back to polling mode while stream recovers')
162+
this.logDebugMessage('Falling back to polling mode while stream recovers')
167163
this.fallbackPoller.start()
168164
}
169165
}
170166

171167
private stopFallBackPolling() {
172168
if (this.fallbackPoller.isPolling()) {
173-
this.logDebug('Stopping fallback polling mode')
169+
this.logDebugMessage('Stopping fallback polling mode')
174170
this.fallbackPoller.stop()
175171
}
176172
}
177173

178-
private logDebug(message: string, ...args: unknown[]): void {
174+
private logDebugMessage(message: string, ...args: unknown[]): void {
179175
if (this.configurations.debug) {
180-
console.debug(`[FF-SDK] Streaming: ${message}`, ...args)
176+
this.logDebug(`Streaming: ${message}`, ...args)
181177
}
182178
}
179+
180+
private logErrorMessage(message: string, ...args: unknown[]): void {
181+
this.logError(`Streaming: ${message}`, ...args)
182+
}
183183
}

Diff for: ‎src/types.ts

+12
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ export interface Options {
139139
* @default false
140140
*/
141141
cache?: boolean | CacheOptions
142+
/**
143+
* Logger to use instead of the default console.log, console.error and console.info functions
144+
* @default console
145+
*/
146+
logger?: Logger
142147
}
143148

144149
export interface MetricsInfo {
@@ -173,3 +178,10 @@ export interface CacheOptions {
173178
*/
174179
storage?: AsyncStorage | SyncStorage
175180
}
181+
182+
export interface Logger {
183+
debug: (...data: any[]) => void
184+
error: (...data: any[]) => void
185+
warn: (...data: any[]) => void
186+
info: (...data: any[]) => void
187+
}

Diff for: ‎src/utils.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,13 @@ export const getConfiguration = (options: Options): Options => {
2828
config.pollingInterval = MIN_POLLING_INTERVAL
2929
}
3030

31+
if (!config.logger || !config.logger.debug || !config.logger.error || !config.logger.info || !config.logger.warn) {
32+
config.logger = console
33+
}
34+
3135
return config
3236
}
3337

34-
// tslint:disable-next-line:no-console
35-
export const logError = (message: string, ...args: any[]) => console.error(`[FF-SDK] ${message}`, ...args)
36-
3738
export const defer = (fn: Function, doDefer = true): void => {
3839
if (doDefer) {
3940
setTimeout(fn, 0)

0 commit comments

Comments
 (0)
Please sign in to comment.