Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Allow configuring analytics API endpoint separate from flags API #168

Merged
merged 4 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
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;
matthewelwell marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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/analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
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
Loading