Skip to content

Commit b374566

Browse files
authored
FFM-11022 Applies standard Metrics Enhancements (#112)
1 parent 62fb5e8 commit b374566

File tree

3 files changed

+97
-58
lines changed

3 files changed

+97
-58
lines changed

Diff for: src/__tests__/sdk_codes.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ describe('SDK Codes', () => {
2222
['debugStreamEventReceived', [logger]],
2323
['infoStreamStopped', [logger]],
2424
['infoMetricsSuccess', [logger]],
25+
['warnTargetMetricsExceeded', [logger]],
26+
['warnEvaluationMetricsExceeded', [logger]],
2527
['infoMetricsThreadExited', [logger]],
2628
[
2729
'debugEvalSuccess',

Diff for: src/metrics.ts

+85-58
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import {
2525
import { Options, Target } from './types';
2626
import { VERSION } from './version';
2727
import {
28+
warnEvaluationMetricsExceeded,
2829
infoMetricsSuccess,
2930
infoMetricsThreadExited,
31+
warnTargetMetricsExceeded,
3032
warnPostMetricsFailed,
3133
} from './sdk_codes';
3234
import { Logger } from './log';
@@ -55,7 +57,18 @@ export interface MetricsProcessorInterface {
5557
}
5658

5759
export class MetricsProcessor implements MetricsProcessorInterface {
58-
private data: Map<string, AnalyticsEvent> = new Map();
60+
private evaluationAnalytics: Map<string, AnalyticsEvent> = new Map();
61+
private targetAnalytics: Map<string, Target> = new Map();
62+
63+
// Only store and send targets that haven't been sent before in the life of the client instance
64+
private seenTargets: Set<string> = new Set();
65+
66+
// Maximum sizes for caches
67+
private MAX_EVALUATION_ANALYTICS_SIZE = 10000;
68+
private MAX_TARGET_ANALYTICS_SIZE = 100000;
69+
private evaluationAnalyticsExceeded = false;
70+
private targetAnalyticsExceeded = false;
71+
5972
private syncInterval?: NodeJS.Timeout;
6073
private api: MetricsApi;
6174
private readonly log: Logger;
@@ -116,13 +129,48 @@ export class MetricsProcessor implements MetricsProcessorInterface {
116129
variation,
117130
count: 0,
118131
};
132+
this.storeEvaluationAnalytic(event);
133+
this.storeTargetAnalytic(target);
134+
}
135+
136+
private storeTargetAnalytic(target: Target): void {
137+
if (this.targetAnalytics.size >= this.MAX_TARGET_ANALYTICS_SIZE) {
138+
if (!this.targetAnalyticsExceeded) {
139+
this.targetAnalyticsExceeded = true;
140+
warnTargetMetricsExceeded(this.log);
141+
}
142+
143+
return;
144+
}
145+
146+
if (target && !target.anonymous) {
147+
// If target has been seen then ignore it
148+
if (this.seenTargets.has(target.identifier)) {
149+
return;
150+
}
151+
152+
this.seenTargets.add(target.identifier);
153+
this.targetAnalytics.set(target.identifier, target);
154+
}
155+
}
156+
157+
private storeEvaluationAnalytic(event: AnalyticsEvent): void {
158+
if (this.evaluationAnalytics.size >= this.MAX_EVALUATION_ANALYTICS_SIZE) {
159+
if (!this.evaluationAnalyticsExceeded) {
160+
this.evaluationAnalyticsExceeded = true;
161+
warnEvaluationMetricsExceeded(this.log);
162+
}
163+
164+
return;
165+
}
166+
119167
const key = this._formatKey(event);
120-
const found = this.data.get(key);
168+
const found = this.evaluationAnalytics.get(key);
121169
if (found) {
122170
found.count++;
123171
} else {
124172
event.count = 1;
125-
this.data.set(key, event);
173+
this.evaluationAnalytics.set(key, event);
126174
}
127175
}
128176

@@ -138,79 +186,58 @@ export class MetricsProcessor implements MetricsProcessorInterface {
138186
const metricsData: MetricsData[] = [];
139187

140188
// clone map and clear data
141-
const clonedData = new Map(this.data);
142-
this.data.clear();
143-
144-
for (const event of clonedData.values()) {
145-
if (event.target && !event.target.anonymous) {
146-
let targetAttributes: KeyValue[] = [];
147-
if (event.target.attributes) {
148-
targetAttributes = Object.entries(event.target.attributes).map(
149-
([key, value]) => {
150-
const stringValue =
151-
value === null || value === undefined
152-
? ''
153-
: this.valueToString(value);
154-
return { key, value: stringValue };
155-
},
156-
);
157-
}
158-
159-
let targetName = event.target.identifier;
160-
if (event.target.name) {
161-
targetName = event.target.name;
162-
}
163-
164-
const td: TargetData = {
165-
identifier: event.target.identifier,
166-
name: targetName,
167-
attributes: targetAttributes,
168-
};
169-
targetData.push(td);
170-
}
189+
const clonedEvaluationAnalytics = new Map(this.evaluationAnalytics);
190+
const clonedTargetAnalytics = new Map(this.targetAnalytics);
191+
this.evaluationAnalytics.clear();
192+
this.targetAnalytics.clear();
193+
this.evaluationAnalyticsExceeded = false;
194+
this.targetAnalyticsExceeded = false;
171195

196+
clonedEvaluationAnalytics.forEach((event) => {
172197
const metricsAttributes: KeyValue[] = [
173198
{
174199
key: FEATURE_IDENTIFIER_ATTRIBUTE,
175200
value: event.featureConfig.feature,
176201
},
177-
{
178-
key: FEATURE_NAME_ATTRIBUTE,
179-
value: event.featureConfig.feature,
180-
},
181202
{
182203
key: VARIATION_IDENTIFIER_ATTRIBUTE,
183204
value: event.variation.identifier,
184205
},
185-
{
186-
key: SDK_TYPE_ATTRIBUTE,
187-
value: SDK_TYPE,
188-
},
189-
{
190-
key: SDK_LANGUAGE_ATTRIBUTE,
191-
value: SDK_LANGUAGE,
192-
},
193-
{
194-
key: SDK_VERSION_ATTRIBUTE,
195-
value: VERSION,
196-
},
197-
{
198-
key: TARGET_ATTRIBUTE,
199-
value: event?.target?.identifier ?? null,
200-
},
206+
{ key: FEATURE_NAME_ATTRIBUTE, value: event.featureConfig.feature },
207+
{ key: SDK_TYPE_ATTRIBUTE, value: SDK_TYPE },
208+
{ key: SDK_LANGUAGE_ATTRIBUTE, value: SDK_LANGUAGE },
209+
{ key: SDK_VERSION_ATTRIBUTE, value: VERSION },
210+
{ key: TARGET_ATTRIBUTE, value: event?.target?.identifier ?? null },
201211
];
202212

203-
// private target attributes
204-
// need more info
205-
206213
const md: MetricsData = {
207214
timestamp: Date.now(),
208215
count: event.count,
209216
metricsType: MetricsDataMetricsTypeEnum.Ffmetrics,
210217
attributes: metricsAttributes,
211218
};
212219
metricsData.push(md);
213-
}
220+
});
221+
222+
clonedTargetAnalytics.forEach((target) => {
223+
let targetAttributes: KeyValue[] = [];
224+
if (target.attributes) {
225+
targetAttributes = Object.entries(target.attributes).map(
226+
([key, value]) => {
227+
return { key, value: this.valueToString(value) };
228+
},
229+
);
230+
}
231+
232+
const targetName = target.name || target.identifier;
233+
234+
targetData.push({
235+
identifier: target.identifier,
236+
name: targetName,
237+
attributes: targetAttributes,
238+
});
239+
});
240+
214241
return {
215242
targetData: targetData,
216243
metricsData: metricsData,
@@ -223,7 +250,7 @@ export class MetricsProcessor implements MetricsProcessorInterface {
223250
return;
224251
}
225252

226-
if (this.data.size === 0) {
253+
if (!this.evaluationAnalytics.size) {
227254
this.log.debug('No metrics to send in this interval');
228255
return;
229256
}

Diff for: src/sdk_codes.ts

+10
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ const sdkCodes: Record<number, string> = {
3333
7001: 'Metrics stopped',
3434
7002: 'Posting metrics failed, reason:',
3535
7003: 'Metrics posted successfully',
36+
7004: 'Target metrics exceeded max size, remaining targets for this analytics interval will not be sent',
37+
7007: 'Evaluation metrics exceeded max size, remaining evaluations for this analytics interval will not be sent'
3638
};
3739

3840
function getSDKCodeMessage(key: number): string {
@@ -107,6 +109,14 @@ export function infoMetricsThreadExited(logger: Logger): void {
107109
logger.info(getSdkErrMsg(7001));
108110
}
109111

112+
export function warnTargetMetricsExceeded(logger: Logger): void {
113+
logger.warn(getSdkErrMsg(7004));
114+
}
115+
116+
export function warnEvaluationMetricsExceeded(logger: Logger): void {
117+
logger.warn(getSdkErrMsg(7007));
118+
}
119+
110120
export function debugEvalSuccess(
111121
result: string,
112122
flagIdentifier: string,

0 commit comments

Comments
 (0)