diff --git a/index.ts b/index.ts index 62ec58e..01cd041 100644 --- a/index.ts +++ b/index.ts @@ -1,5 +1,6 @@ export { AnalyticsProcessor, + AnalyticsProcessorOptions, FlagsmithAPIError, FlagsmithClientError, EnvironmentDataPollingManager, diff --git a/package-lock.json b/package-lock.json index a0fa8e9..e0eef26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "flagsmith-nodejs", - "version": "5.0.1", + "version": "5.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "flagsmith-nodejs", - "version": "5.0.1", + "version": "5.1.0", "license": "MIT", "dependencies": { "pino": "^8.8.0", diff --git a/package.json b/package.json index 7f3b134..1274cb8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flagsmith-nodejs", - "version": "5.0.1", + "version": "5.1.0", "description": "Flagsmith lets you manage features flags and remote config across web, mobile and server side applications. Deliver true Continuous Integration. Get builds out faster. Control who has access to new features.", "main": "./build/cjs/index.js", "type": "module", diff --git a/sdk/analytics.ts b/sdk/analytics.ts index 1235b92..518bd2a 100644 --- a/sdk/analytics.ts +++ b/sdk/analytics.ts @@ -1,32 +1,52 @@ import { pino, Logger } from 'pino'; import { Fetch } from "./types.js"; +import { Flags } from "./models.js"; -const ANALYTICS_ENDPOINT = 'analytics/flags/'; +export const ANALYTICS_ENDPOINT = './analytics/flags/'; -// Used to control how often we send data(in seconds) +/** Duration in seconds to wait before trying to flush collected data after {@link trackFeature} is called. **/ const ANALYTICS_TIMER = 10; +const DEFAULT_REQUEST_TIMEOUT_MS = 3000 + +export interface AnalyticsProcessorOptions { + /** URL of the Flagsmith analytics events API endpoint + * @example https://flagsmith.example.com/api/v1/analytics + */ + analyticsUrl?: string; + /** Client-side key of the environment that analytics will be recorded for. **/ + environmentKey: string; + /** Duration in milliseconds to wait for API requests to complete before timing out. Defaults to {@link DEFAULT_REQUEST_TIMEOUT_MS}. **/ + requestTimeoutMs?: number; + logger?: Logger; + /** Custom {@link fetch} implementation to use for API requests. **/ + fetch?: Fetch + + /** @deprecated Use {@link analyticsUrl} instead. **/ + baseApiUrl?: string; +} + +/** + * Tracks how often individual features are evaluated whenever {@link trackFeature} is called. + * + * Analytics data is posted after {@link trackFeature} is called and at least {@link ANALYTICS_TIMER} seconds have + * passed since the previous analytics API request was made (if any), or by calling {@link flush}. + * + * Data will stay in memory indefinitely until it can be successfully posted to the API. + * @see https://docs.flagsmith.com/advanced-use/flag-analytics. + */ export class AnalyticsProcessor { - private analyticsEndpoint: string; + private analyticsUrl: string; private environmentKey: string; private lastFlushed: number; analyticsData: { [key: string]: any }; - private requestTimeoutMs: number = 3000; + private requestTimeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS; private logger: Logger; private currentFlush: ReturnType | undefined; private customFetch: Fetch; - /** - * AnalyticsProcessor is used to track how often individual Flags are evaluated within - * the Flagsmith SDK. Docs: https://docs.flagsmith.com/advanced-use/flag-analytics. - * - * @param data.environmentKey environment key obtained from the Flagsmith UI - * @param data.baseApiUrl base api url to override when using self hosted version - * @param data.requestTimeoutMs used to tell requests to stop waiting for a response after a - given number of milliseconds - */ - constructor(data: { environmentKey: string; baseApiUrl: string; requestTimeoutMs?: number, logger?: Logger, fetch?: Fetch }) { - this.analyticsEndpoint = data.baseApiUrl + ANALYTICS_ENDPOINT; + constructor(data: AnalyticsProcessorOptions) { + this.analyticsUrl = data.analyticsUrl || data.baseApiUrl + ANALYTICS_ENDPOINT; this.environmentKey = data.environmentKey; this.lastFlushed = Date.now(); this.analyticsData = {}; @@ -35,7 +55,7 @@ export class AnalyticsProcessor { this.customFetch = data.fetch ?? fetch; } /** - * Sends all the collected data to the api asynchronously and resets the timer + * Try to flush pending collected data to the Flagsmith analytics API. */ async flush() { if (this.currentFlush || !Object.keys(this.analyticsData).length) { @@ -43,7 +63,7 @@ export class AnalyticsProcessor { } try { - this.currentFlush = this.customFetch(this.analyticsEndpoint, { + this.currentFlush = this.customFetch(this.analyticsUrl, { method: 'POST', body: JSON.stringify(this.analyticsData), signal: AbortSignal.timeout(this.requestTimeoutMs), @@ -66,6 +86,11 @@ export class AnalyticsProcessor { this.lastFlushed = Date.now(); } + /** + * Track a single evaluation event for a feature. + * + * This method is called whenever {@link Flags.isFeatureEnabled}, {@link Flags.getFeatureValue} or {@link Flags.getFlag} are called. + */ trackFeature(featureName: string) { this.analyticsData[featureName] = (this.analyticsData[featureName] || 0) + 1; if (Date.now() - this.lastFlushed > ANALYTICS_TIMER * 1000) { diff --git a/sdk/index.ts b/sdk/index.ts index c73ed81..60269d2 100644 --- a/sdk/index.ts +++ b/sdk/index.ts @@ -5,7 +5,7 @@ import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js' import { IdentityModel } from '../flagsmith-engine/index.js'; import { TraitModel } from '../flagsmith-engine/index.js'; -import { AnalyticsProcessor } from './analytics.js'; +import {ANALYTICS_ENDPOINT, AnalyticsProcessor} from './analytics.js'; import { BaseOfflineHandler } from './offline_handlers.js'; import { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; @@ -17,7 +17,7 @@ import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js' import { Fetch, FlagsmithCache, FlagsmithConfig, FlagsmithTraitValue, ITraitConfig } from './types.js'; import { pino, Logger } from 'pino'; -export { AnalyticsProcessor } from './analytics.js'; +export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js'; export { FlagsmithAPIError, FlagsmithClientError } from './errors.js'; export { DefaultFlag, Flags } from './models.js'; @@ -30,6 +30,7 @@ const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10; export class Flagsmith { environmentKey?: string = undefined; apiUrl?: string = undefined; + analyticsUrl?: string = undefined; customHeaders?: { [key: string]: any }; agent?: Dispatcher; requestTimeoutMs?: number; @@ -138,6 +139,7 @@ export class Flagsmith { const apiUrl = data.apiUrl || DEFAULT_API_URL; this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`; + this.analyticsUrl = this.analyticsUrl || new URL(ANALYTICS_ENDPOINT, new Request(this.apiUrl).url).href this.environmentFlagsUrl = `${this.apiUrl}flags/`; this.identitiesUrl = `${this.apiUrl}identities/`; this.environmentUrl = `${this.apiUrl}environment-document/`; @@ -156,14 +158,14 @@ export class Flagsmith { this.updateEnvironment(); } - this.analyticsProcessor = data.enableAnalytics - ? new AnalyticsProcessor({ - environmentKey: this.environmentKey, - baseApiUrl: this.apiUrl, - requestTimeoutMs: this.requestTimeoutMs, - logger: this.logger - }) - : undefined; + if (data.enableAnalytics) { + this.analyticsProcessor = new AnalyticsProcessor({ + environmentKey: this.environmentKey, + analyticsUrl: this.analyticsUrl, + requestTimeoutMs: this.requestTimeoutMs, + logger: this.logger, + }) + } } } /** diff --git a/tests/sdk/analytics.test.ts b/tests/sdk/analytics.test.ts index 95e72aa..fa687dc 100644 --- a/tests/sdk/analytics.test.ts +++ b/tests/sdk/analytics.test.ts @@ -26,7 +26,7 @@ test('test_analytics_processor_flush_post_request_data_match_ananlytics_data', a aP.trackFeature("myFeature2"); await aP.flush(); expect(fetch).toHaveBeenCalledTimes(1); - expect(fetch).toHaveBeenCalledWith('http://testUrlanalytics/flags/', expect.objectContaining({ + expect(fetch).toHaveBeenCalledWith('http://testUrl/analytics/flags/', expect.objectContaining({ body: '{"myFeature1":1,"myFeature2":1}', headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'test-key' }, method: 'POST', diff --git a/tests/sdk/utils.ts b/tests/sdk/utils.ts index da62508..b02d38b 100644 --- a/tests/sdk/utils.ts +++ b/tests/sdk/utils.ts @@ -24,7 +24,7 @@ export const fetch = vi.fn(global.fetch) export function analyticsProcessor() { return new AnalyticsProcessor({ environmentKey: 'test-key', - baseApiUrl: 'http://testUrl', + analyticsUrl: 'http://testUrl/analytics/flags/', fetch, }); }