Skip to content

Commit 2d26062

Browse files
authored
Merge pull request #168 from Flagsmith/feat/analytics-endpoint
feat: Allow configuring analytics API endpoint separate from flags API
2 parents 6212858 + 978fadc commit 2d26062

File tree

7 files changed

+60
-32
lines changed

7 files changed

+60
-32
lines changed

index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export {
22
AnalyticsProcessor,
3+
AnalyticsProcessorOptions,
34
FlagsmithAPIError,
45
FlagsmithClientError,
56
EnvironmentDataPollingManager,

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "flagsmith-nodejs",
3-
"version": "5.0.1",
3+
"version": "5.1.0",
44
"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.",
55
"main": "./build/cjs/index.js",
66
"type": "module",

sdk/analytics.ts

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,52 @@
11
import { pino, Logger } from 'pino';
22
import { Fetch } from "./types.js";
3+
import { Flags } from "./models.js";
34

4-
const ANALYTICS_ENDPOINT = 'analytics/flags/';
5+
export const ANALYTICS_ENDPOINT = './analytics/flags/';
56

6-
// Used to control how often we send data(in seconds)
7+
/** Duration in seconds to wait before trying to flush collected data after {@link trackFeature} is called. **/
78
const ANALYTICS_TIMER = 10;
89

10+
const DEFAULT_REQUEST_TIMEOUT_MS = 3000
11+
12+
export interface AnalyticsProcessorOptions {
13+
/** URL of the Flagsmith analytics events API endpoint
14+
* @example https://flagsmith.example.com/api/v1/analytics
15+
*/
16+
analyticsUrl?: string;
17+
/** Client-side key of the environment that analytics will be recorded for. **/
18+
environmentKey: string;
19+
/** Duration in milliseconds to wait for API requests to complete before timing out. Defaults to {@link DEFAULT_REQUEST_TIMEOUT_MS}. **/
20+
requestTimeoutMs?: number;
21+
logger?: Logger;
22+
/** Custom {@link fetch} implementation to use for API requests. **/
23+
fetch?: Fetch
24+
25+
/** @deprecated Use {@link analyticsUrl} instead. **/
26+
baseApiUrl?: string;
27+
}
28+
29+
/**
30+
* Tracks how often individual features are evaluated whenever {@link trackFeature} is called.
31+
*
32+
* Analytics data is posted after {@link trackFeature} is called and at least {@link ANALYTICS_TIMER} seconds have
33+
* passed since the previous analytics API request was made (if any), or by calling {@link flush}.
34+
*
35+
* Data will stay in memory indefinitely until it can be successfully posted to the API.
36+
* @see https://docs.flagsmith.com/advanced-use/flag-analytics.
37+
*/
938
export class AnalyticsProcessor {
10-
private analyticsEndpoint: string;
39+
private analyticsUrl: string;
1140
private environmentKey: string;
1241
private lastFlushed: number;
1342
analyticsData: { [key: string]: any };
14-
private requestTimeoutMs: number = 3000;
43+
private requestTimeoutMs: number = DEFAULT_REQUEST_TIMEOUT_MS;
1544
private logger: Logger;
1645
private currentFlush: ReturnType<typeof fetch> | undefined;
1746
private customFetch: Fetch;
1847

19-
/**
20-
* AnalyticsProcessor is used to track how often individual Flags are evaluated within
21-
* the Flagsmith SDK. Docs: https://docs.flagsmith.com/advanced-use/flag-analytics.
22-
*
23-
* @param data.environmentKey environment key obtained from the Flagsmith UI
24-
* @param data.baseApiUrl base api url to override when using self hosted version
25-
* @param data.requestTimeoutMs used to tell requests to stop waiting for a response after a
26-
given number of milliseconds
27-
*/
28-
constructor(data: { environmentKey: string; baseApiUrl: string; requestTimeoutMs?: number, logger?: Logger, fetch?: Fetch }) {
29-
this.analyticsEndpoint = data.baseApiUrl + ANALYTICS_ENDPOINT;
48+
constructor(data: AnalyticsProcessorOptions) {
49+
this.analyticsUrl = data.analyticsUrl || data.baseApiUrl + ANALYTICS_ENDPOINT;
3050
this.environmentKey = data.environmentKey;
3151
this.lastFlushed = Date.now();
3252
this.analyticsData = {};
@@ -35,15 +55,15 @@ export class AnalyticsProcessor {
3555
this.customFetch = data.fetch ?? fetch;
3656
}
3757
/**
38-
* Sends all the collected data to the api asynchronously and resets the timer
58+
* Try to flush pending collected data to the Flagsmith analytics API.
3959
*/
4060
async flush() {
4161
if (this.currentFlush || !Object.keys(this.analyticsData).length) {
4262
return;
4363
}
4464

4565
try {
46-
this.currentFlush = this.customFetch(this.analyticsEndpoint, {
66+
this.currentFlush = this.customFetch(this.analyticsUrl, {
4767
method: 'POST',
4868
body: JSON.stringify(this.analyticsData),
4969
signal: AbortSignal.timeout(this.requestTimeoutMs),
@@ -66,6 +86,11 @@ export class AnalyticsProcessor {
6686
this.lastFlushed = Date.now();
6787
}
6888

89+
/**
90+
* Track a single evaluation event for a feature.
91+
*
92+
* This method is called whenever {@link Flags.isFeatureEnabled}, {@link Flags.getFeatureValue} or {@link Flags.getFlag} are called.
93+
*/
6994
trackFeature(featureName: string) {
7095
this.analyticsData[featureName] = (this.analyticsData[featureName] || 0) + 1;
7196
if (Date.now() - this.lastFlushed > ANALYTICS_TIMER * 1000) {

sdk/index.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { buildEnvironmentModel } from '../flagsmith-engine/environments/util.js'
55
import { IdentityModel } from '../flagsmith-engine/index.js';
66
import { TraitModel } from '../flagsmith-engine/index.js';
77

8-
import { AnalyticsProcessor } from './analytics.js';
8+
import {ANALYTICS_ENDPOINT, AnalyticsProcessor} from './analytics.js';
99
import { BaseOfflineHandler } from './offline_handlers.js';
1010
import { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
1111

@@ -17,7 +17,7 @@ import { getIdentitySegments } from '../flagsmith-engine/segments/evaluators.js'
1717
import { Fetch, FlagsmithCache, FlagsmithConfig, FlagsmithTraitValue, ITraitConfig } from './types.js';
1818
import { pino, Logger } from 'pino';
1919

20-
export { AnalyticsProcessor } from './analytics.js';
20+
export { AnalyticsProcessor, AnalyticsProcessorOptions } from './analytics.js';
2121
export { FlagsmithAPIError, FlagsmithClientError } from './errors.js';
2222

2323
export { DefaultFlag, Flags } from './models.js';
@@ -30,6 +30,7 @@ const DEFAULT_REQUEST_TIMEOUT_SECONDS = 10;
3030
export class Flagsmith {
3131
environmentKey?: string = undefined;
3232
apiUrl?: string = undefined;
33+
analyticsUrl?: string = undefined;
3334
customHeaders?: { [key: string]: any };
3435
agent?: Dispatcher;
3536
requestTimeoutMs?: number;
@@ -138,6 +139,7 @@ export class Flagsmith {
138139

139140
const apiUrl = data.apiUrl || DEFAULT_API_URL;
140141
this.apiUrl = apiUrl.endsWith('/') ? apiUrl : `${apiUrl}/`;
142+
this.analyticsUrl = this.analyticsUrl || new URL(ANALYTICS_ENDPOINT, new Request(this.apiUrl).url).href
141143
this.environmentFlagsUrl = `${this.apiUrl}flags/`;
142144
this.identitiesUrl = `${this.apiUrl}identities/`;
143145
this.environmentUrl = `${this.apiUrl}environment-document/`;
@@ -156,14 +158,14 @@ export class Flagsmith {
156158
this.updateEnvironment();
157159
}
158160

159-
this.analyticsProcessor = data.enableAnalytics
160-
? new AnalyticsProcessor({
161-
environmentKey: this.environmentKey,
162-
baseApiUrl: this.apiUrl,
163-
requestTimeoutMs: this.requestTimeoutMs,
164-
logger: this.logger
165-
})
166-
: undefined;
161+
if (data.enableAnalytics) {
162+
this.analyticsProcessor = new AnalyticsProcessor({
163+
environmentKey: this.environmentKey,
164+
analyticsUrl: this.analyticsUrl,
165+
requestTimeoutMs: this.requestTimeoutMs,
166+
logger: this.logger,
167+
})
168+
}
167169
}
168170
}
169171
/**

tests/sdk/analytics.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ test('test_analytics_processor_flush_post_request_data_match_ananlytics_data', a
2626
aP.trackFeature("myFeature2");
2727
await aP.flush();
2828
expect(fetch).toHaveBeenCalledTimes(1);
29-
expect(fetch).toHaveBeenCalledWith('http://testUrlanalytics/flags/', expect.objectContaining({
29+
expect(fetch).toHaveBeenCalledWith('http://testUrl/analytics/flags/', expect.objectContaining({
3030
body: '{"myFeature1":1,"myFeature2":1}',
3131
headers: { 'Content-Type': 'application/json', 'X-Environment-Key': 'test-key' },
3232
method: 'POST',

tests/sdk/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export const fetch = vi.fn(global.fetch)
2424
export function analyticsProcessor() {
2525
return new AnalyticsProcessor({
2626
environmentKey: 'test-key',
27-
baseApiUrl: 'http://testUrl',
27+
analyticsUrl: 'http://testUrl/analytics/flags/',
2828
fetch,
2929
});
3030
}

0 commit comments

Comments
 (0)