Skip to content

Commit

Permalink
Allow configuring analytics API endpoint separate from flags API
Browse files Browse the repository at this point in the history
  • Loading branch information
rolodato committed Jan 17, 2025
1 parent d52fbac commit 0ce1b7a
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 28 deletions.
1 change: 1 addition & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export {
AnalyticsProcessor,
AnalyticsProcessorOptions,
FlagsmithAPIError,
FlagsmithClientError,
EnvironmentDataPollingManager,
Expand Down
59 changes: 42 additions & 17 deletions sdk/analytics.ts
Original file line number Diff line number Diff line change
@@ -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<typeof fetch> | 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 = {};
Expand All @@ -35,15 +55,15 @@ 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) {
return;
}

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),
Expand All @@ -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) {
Expand Down
22 changes: 12 additions & 10 deletions sdk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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';
Expand All @@ -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;
Expand Down Expand Up @@ -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/`;
Expand All @@ -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,
})
}
}
}
/**
Expand Down
2 changes: 1 addition & 1 deletion tests/sdk/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
Expand Down

0 comments on commit 0ce1b7a

Please sign in to comment.