Skip to content

Commit 0ec2cff

Browse files
[FFM-10041] - Fall back to identifier if bucketBy attr is not found (#99)
* [FFM-10041] - Fall back to identifier if bucketBy attribute is not found What Update BucketBy logic to fall back to the target identifier if given BucketBy attribute is not found. Why Keep the SDK consistent with the behaviour of the other server SDKs. Testing Testgrid + new unit test --------- Co-authored-by: Kevin Nagurski <[email protected]>
1 parent 04abf0e commit 0ec2cff

File tree

8 files changed

+116
-12
lines changed

8 files changed

+116
-12
lines changed

examples/getting_started/package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package-lock.json

+2-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@harnessio/ff-nodejs-server-sdk",
3-
"version": "1.3.7",
3+
"version": "1.4.0",
44
"description": "Feature flags SDK for NodeJS environments",
55
"main": "dist/cjs/index.js",
66
"module": "dist/esm/index.mjs",

src/__tests__/evaluator.test.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { Evaluator } from '../evaluator';
2+
import { Logger } from '../log';
3+
4+
describe('Evaluator', () => {
5+
it('should fallback to identifier if bucketby attribute does not exist', async () => {
6+
const logger: Logger = {
7+
trace: jest.fn(),
8+
debug: jest.fn(),
9+
info: jest.fn(),
10+
warn: jest.fn(),
11+
error: jest.fn(),
12+
};
13+
14+
const mock_query = {
15+
getFlag: jest.fn(),
16+
getSegment: jest.fn(),
17+
findFlagsBySegment: jest.fn(),
18+
};
19+
20+
const feature_config = {
21+
feature: 'flag',
22+
kind: 'string',
23+
state: 'on',
24+
variationToTargetMap: [],
25+
variations: [
26+
{ identifier: 'variation1', value: 'default_on' },
27+
{ identifier: 'variation2', value: 'wanted_value' },
28+
{ identifier: 'variation3', value: 'default_off' },
29+
],
30+
defaultServe: {
31+
distribution: null,
32+
variation: 'default_on',
33+
},
34+
offVariation: 'variation3',
35+
rules: [
36+
{
37+
ruleId: 'rule1',
38+
clauses: [
39+
{
40+
op: 'equal',
41+
attribute: 'identifier',
42+
values: ['test'],
43+
},
44+
],
45+
serve: {
46+
distribution: {
47+
bucketBy: 'i_do_not_exist',
48+
variations: [
49+
{ variation: 'variation1', weight: 56 },
50+
{ variation: 'variation2', weight: 1 }, // bucket 57
51+
{ variation: 'variation3', weight: 43 },
52+
],
53+
},
54+
},
55+
},
56+
],
57+
};
58+
59+
mock_query.getFlag.mockReturnValue(feature_config);
60+
61+
const evaluator = new Evaluator(mock_query, logger);
62+
63+
const target = {
64+
identifier: 'test', // Test will fall back to bucketing on this (bucket 57)
65+
name: 'test name',
66+
attributes: {
67+
location: 'emea',
68+
},
69+
};
70+
71+
const actual_variation = await evaluator.stringVariation(
72+
'flag',
73+
target,
74+
'fallback_value',
75+
null,
76+
);
77+
78+
expect(actual_variation).toEqual('wanted_value');
79+
});
80+
});

src/__tests__/sdk_codes.test.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('SDK Codes', () => {
4444
'warnDefaultVariationServed',
4545
['flag', { name: 'dummy', identifier: 'dummy' }, 'default value', logger],
4646
],
47+
['warnBucketByAttributeNotFound', ['dummy1', 'dummy2', logger]],
4748
])('it should not throw when %s is called', (fn, args) => {
4849
expect(() => sdkCodes[fn](...args)).not.toThrow();
4950
});

src/evaluator.ts

+19-7
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ import {
2323
} from './openapi';
2424
import murmurhash from 'murmurhash';
2525
import { ConsoleLog } from './log';
26-
import { debugEvalSuccess, warnDefaultVariationServed } from './sdk_codes';
26+
import {
27+
debugEvalSuccess,
28+
warnBucketByAttributeNotFound,
29+
warnDefaultVariationServed,
30+
} from './sdk_codes';
2731

2832
type Callback = (
2933
fc: FeatureConfig,
@@ -54,19 +58,19 @@ export class Evaluator {
5458
}
5559

5660
private getNormalizedNumberWithNormalizer(
57-
property: Type,
5861
bucketBy: string,
62+
property: Type,
5963
normalizer: number,
6064
): number {
6165
const value = [bucketBy, property].join(':');
6266
const hash = parseInt(murmurhash(value).toString());
6367
return (hash % normalizer) + 1;
6468
}
6569

66-
private getNormalizedNumber(property: Type, bucketBy: string): number {
70+
private getNormalizedNumber(bucketBy: string, property: Type): number {
6771
return this.getNormalizedNumberWithNormalizer(
68-
property,
6972
bucketBy,
73+
property,
7074
ONE_HUNDRED,
7175
);
7276
}
@@ -76,11 +80,19 @@ export class Evaluator {
7680
bucketBy: string,
7781
percentage: number,
7882
): boolean {
79-
const property = this.getAttrValue(target, bucketBy);
83+
let bb = bucketBy;
84+
let property = this.getAttrValue(target, bb);
8085
if (!property) {
81-
return false;
86+
bb = 'identifier';
87+
property = this.getAttrValue(target, bb);
88+
89+
if (!property) {
90+
return false;
91+
}
92+
93+
warnBucketByAttributeNotFound(bucketBy, property?.toString(), this.log);
8294
}
83-
const bucketId = this.getNormalizedNumber(property, bucketBy);
95+
const bucketId = this.getNormalizedNumber(bb, property);
8496
return percentage > 0 && bucketId <= percentage;
8597
}
8698

src/sdk_codes.ts

+11
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const sdkCodes: Record<number, string> = {
2626
// SDK_EVAL_6xxx -
2727
6000: 'Evaluation successful: ',
2828
6001: 'Evaluation Failed, returning default variation: ',
29+
6002: "BucketBy attribute not found in target attributes, falling back to 'identifier':",
2930
// SDK_METRICS_7xxx
3031
7000: 'Metrics thread started with request interval:',
3132
7001: 'Metrics stopped',
@@ -173,3 +174,13 @@ export function warnDefaultVariationServed(
173174
),
174175
);
175176
}
177+
178+
export function warnBucketByAttributeNotFound(
179+
bucketBy: string,
180+
usingValue: string,
181+
logger: Logger,
182+
): void {
183+
logger.warn(
184+
getSdkErrMsg(6002, `missing=${bucketBy}, using value=${usingValue}`),
185+
);
186+
}

src/version.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export const VERSION = '1.3.7';
1+
export const VERSION = '1.4.0';

0 commit comments

Comments
 (0)