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/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/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, }); }