Skip to content

Commit

Permalink
Add reporting
Browse files Browse the repository at this point in the history
  • Loading branch information
nick-keller committed Jul 21, 2024
1 parent 4d3c7a7 commit 0f85876
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 9 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"scripts": {
"build": "tsc",
"format": "prettier --write src/.",
"test": "jest --passWithNoTests",
"test": "jest",
"prepublishOnly": "npm test && rm -rf lib && npm run build",
"prepare": "husky install && npm run build",
"version": "npm run format && git add -A src",
Expand Down
1 change: 1 addition & 0 deletions src/TgglClient-concurency.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
jest.mock('./apiCall')
jest.mock('./TgglReporting')

class DL {
constructor(private batch: (contexts: any[]) => any) {}
Expand Down
25 changes: 22 additions & 3 deletions src/TgglClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { TgglResponse } from './TgglResponse'
import DataLoader from 'dataloader'
import { assertValidContext } from './validation'
import { apiCall } from './apiCall'
import { TgglReporting } from './TgglReporting'

export class TgglClient<
TFlags extends TgglFlags = TgglFlags,
Expand Down Expand Up @@ -35,9 +36,25 @@ export class TgglClient<
initialActiveFlags?: Partial<TFlags>
pollingInterval?: number
log?: boolean
reporting?: boolean | { app?: string; url?: string }
} = {}
) {
super(options.initialActiveFlags)
super(options.initialActiveFlags, {
reporting:
options.reporting === false || !apiKey
? null
: new TgglReporting({
apiKey,
app:
typeof options.reporting === 'object'
? `TgglClient/${options.reporting.app}`
: 'TgglClient',
url:
typeof options.reporting === 'object'
? options.reporting.url
: undefined,
}),
})

this.url = options.url ?? 'https://api.tggl.io/flags'
this.log = options.log ?? true
Expand Down Expand Up @@ -216,14 +233,16 @@ export class TgglClient<
throw response
}

return new TgglResponse(response)
return new TgglResponse(response, { reporting: this.reporting })
})
} catch (error) {
if (this.log) {
console.error(error)
}

return contexts.map(() => new TgglResponse())
return contexts.map(
() => new TgglResponse({}, { reporting: this.reporting })
)
}
}
}
1 change: 1 addition & 0 deletions src/TgglLocalClient-concurency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TgglLocalClient } from './TgglLocalClient'

jest.mock('tggl-core')
jest.mock('./apiCall')
jest.mock('./TgglReporting')
jest.useFakeTimers()

const runTimers = async (ms?: number) => {
Expand Down
40 changes: 37 additions & 3 deletions src/TgglLocalClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { TgglContext, TgglFlags, TgglFlagSlug, TgglFlagValue } from './types'
import { evalFlag, Flag } from 'tggl-core'
import { assertValidContext } from './validation'
import { apiCall } from './apiCall'
import { TgglReporting } from './TgglReporting'

export class TgglLocalClient<
TFlags extends TgglFlags = TgglFlags,
Expand All @@ -24,6 +25,7 @@ export class TgglLocalClient<
private onFetchSuccessfulCallbacks = new Map<number, () => void>()
private onFetchFailCallbacks = new Map<number, (error: Error) => void>()
private log: boolean = true
protected reporting: TgglReporting | null

constructor(
private apiKey?: string | null,
Expand All @@ -32,11 +34,26 @@ export class TgglLocalClient<
initialConfig?: Map<TgglFlagSlug<TFlags>, Flag>
pollingInterval?: number
log?: boolean
reporting?: boolean | { app?: string; url?: string }
} = {}
) {
this.url = options.url ?? 'https://api.tggl.io/config'
this.config = options.initialConfig ?? new Map()
this.log = options.log ?? true
this.reporting =
options.reporting === false || !apiKey
? null
: new TgglReporting({
apiKey,
app:
typeof options.reporting === 'object'
? `TgglLocalClient/${options.reporting.app}`
: 'TgglLocalClient',
url:
typeof options.reporting === 'object'
? options.reporting.url
: undefined,
})

this.startPolling(options.pollingInterval ?? 0)
}
Expand Down Expand Up @@ -196,7 +213,16 @@ export class TgglLocalClient<
isActive(context: Partial<TContext>, slug: TgglFlagSlug<TFlags>) {
assertValidContext(context)
const flag = this.config.get(slug)
return flag ? evalFlag(context, flag) !== undefined : false
const value = flag ? evalFlag(context, flag) : undefined
const active = value !== undefined

this.reporting?.reportFlag(String(slug), {
active,
value,
stack: Error().stack?.split('\n').slice(2).join('\n'),
})

return active
}

get<TSlug extends TgglFlagSlug<TFlags>>(
Expand All @@ -221,7 +247,15 @@ export class TgglLocalClient<
): TgglFlagValue<TSlug, TFlags> | TDefaultValue | undefined {
assertValidContext(context)
const flag = this.config.get(slug)
const result = flag ? evalFlag(context, flag) : undefined
return result === undefined ? defaultValue : result
const value = flag ? evalFlag(context, flag) : undefined

this.reporting?.reportFlag(String(slug), {
active: value !== undefined,
default: defaultValue,
value,
stack: Error().stack?.split('\n').slice(2).join('\n'),
})

return value === undefined ? defaultValue : value
}
}
112 changes: 112 additions & 0 deletions src/TgglReporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { apiCall } from './apiCall'

export const PACKAGE_VERSION = '1.15.0'

export class TgglReporting {
private app: string | null
private apiKey: string
private url: string
private flagsToReport: Record<
string,
Map<
string,
{
active: boolean
value?: any
default?: any
count: number
stack?: string
}
>
> = {}

constructor({
app,
apiKey,
url,
}: {
app?: string
apiKey: string
url?: string
}) {
this.app = app ?? null
this.apiKey = apiKey
this.url = url ?? 'https://api.tggl.io/report'

this.sendReport()
}

private async sendReport() {
const payload: Record<string, any> = {}

if (Object.keys(this.flagsToReport).length) {
const flagsToReport = { ...this.flagsToReport }
this.flagsToReport = {}

payload.clients = [
{
id: `js-client:${PACKAGE_VERSION}${this.app ? `/${this.app}` : ''}`,
flags: Object.entries(flagsToReport).reduce(
(acc, [key, value]) => {
acc[key] = [...value.values()]
return acc
},
{} as Record<
string,
{
active: boolean
value?: any
default?: any
count: number
stack?: string
}[]
>
),
},
]
}

if (Object.keys(payload).length) {
await apiCall({
url: this.url,
apiKey: this.apiKey,
method: 'post',
body: payload,
})
}

setTimeout(() => {
this.sendReport()
}, 4000)
}

reportFlag(
slug: string,
data: {
active: boolean
value?: any
default?: any
stack?: string
}
) {
const key = `${data.active ? '1' : '0'}${JSON.stringify(
data.value
)}${JSON.stringify(data.default)}${data.stack}`

this.flagsToReport[slug] ??= new Map()

const value =
this.flagsToReport[slug].get(key) ??
this.flagsToReport[slug]
.set(key, {
active: data.active,
value: data.value,
default: data.default,
count: 0,
stack: data.stack,
})
.get(key)!

value.count++
}
}
6 changes: 6 additions & 0 deletions src/TgglResponse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { PACKAGE_VERSION } from './TgglReporting'
import { version } from '../package.json'

test('Check package version', () => {
expect(PACKAGE_VERSION).toBe(version)
})
29 changes: 27 additions & 2 deletions src/TgglResponse.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { TgglFlags, TgglFlagSlug, TgglFlagValue } from './types'
import { TgglReporting } from './TgglReporting'

export class TgglResponse<TFlags extends TgglFlags = TgglFlags> {
constructor(protected flags: Partial<TFlags> = {}) {}
protected reporting: TgglReporting | null

constructor(
protected flags: Partial<TFlags> = {},
options: {
reporting?: TgglReporting | null
} = {}
) {
this.reporting = options.reporting ?? null
}

isActive(slug: TgglFlagSlug<TFlags>): boolean {
return this.flags[slug as keyof TFlags] !== undefined
const active = this.flags[slug as keyof TFlags] !== undefined

this.reporting?.reportFlag(String(slug), {
active,
value: this.flags[slug as keyof TFlags],
stack: Error().stack?.split('\n').slice(2).join('\n'),
})

return active
}

get<TSlug extends TgglFlagSlug<TFlags>>(
Expand All @@ -21,6 +39,13 @@ export class TgglResponse<TFlags extends TgglFlags = TgglFlags> {
slug: TSlug,
defaultValue?: TDefaultValue
): TgglFlagValue<TSlug, TFlags> | TDefaultValue | undefined {
this.reporting?.reportFlag(String(slug), {
active: this.flags[slug as keyof TFlags] !== undefined,
default: defaultValue,
value: this.flags[slug as keyof TFlags],
stack: Error().stack?.split('\n').slice(2).join('\n'),
})

// @ts-ignore
return this.flags[slug as keyof TFlags] === undefined
? defaultValue
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export type {
export { TgglResponse } from './TgglResponse'
export { TgglClient } from './TgglClient'
export { TgglLocalClient } from './TgglLocalClient'
export { TgglReporting } from './TgglReporting'

0 comments on commit 0f85876

Please sign in to comment.