Skip to content

Commit 5cee559

Browse files
authored
Warn and log on default value fallback dynamicconfig (#229)
* Warn and log on default value fallback dynamicconfig * cleanup
1 parent f68d834 commit 5cee559

13 files changed

+241
-25
lines changed

jest.config.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
module.exports = {
22
roots: ['./'],
3-
setupFilesAfterEnv: ['<rootDir>/src/__tests__/jest.setup.js'],
3+
setupFilesAfterEnv: ['<rootDir>/src/__tests__/jest.setup.ts'],
44
testMatch: ['**/__tests__/**/*.test.(j|t)s', '**/?(*.)+test.(j|t)s'],
55
testPathIgnorePatterns: [
66
'<rootDir>/node_modules/',
77
'<rootDir>/dist/',
8-
'<rootDir>/src/__tests__/jest.setup.js',
8+
'<rootDir>/src/__tests__/jest.setup.ts',
99
],
1010
transform: {
1111
'^.+\\.ts$': 'ts-jest',

src/DynamicConfig.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { EvaluationDetails } from './StatsigStore';
22

3+
export type OnDefaultValueFallback = (
4+
config: DynamicConfig,
5+
parameter: string,
6+
defaultValueType: string,
7+
valueType: string,
8+
) => void;
9+
310
export default class DynamicConfig {
411
private name: string;
512
public value: Record<string, any>;
613
private ruleID: string;
714
private secondaryExposures: Record<string, string>[];
815
private allocatedExperimentName: string;
916
private evaluationDetails: EvaluationDetails;
17+
private onDefaultValueFallback: OnDefaultValueFallback | null = null;
1018

1119
public constructor(
1220
configName: string,
@@ -15,13 +23,15 @@ export default class DynamicConfig {
1523
evaluationDetails: EvaluationDetails,
1624
secondaryExposures: Record<string, string>[] = [],
1725
allocatedExperimentName: string = '',
26+
onDefaultValueFallback: OnDefaultValueFallback | null = null,
1827
) {
1928
this.name = configName;
2029
this.value = JSON.parse(JSON.stringify(configValue ?? {}));
2130
this.ruleID = ruleID ?? '';
2231
this.secondaryExposures = secondaryExposures;
2332
this.allocatedExperimentName = allocatedExperimentName;
2433
this.evaluationDetails = evaluationDetails;
34+
this.onDefaultValueFallback = onDefaultValueFallback;
2535
}
2636

2737
public get<T>(
@@ -50,6 +60,14 @@ export default class DynamicConfig {
5060
return val as unknown as T;
5161
}
5262

63+
if (this.onDefaultValueFallback != null) {
64+
this.onDefaultValueFallback(
65+
this,
66+
key,
67+
Array.isArray(defaultValue) ? 'array' : typeof defaultValue,
68+
Array.isArray(val) ? 'array' : typeof val,
69+
);
70+
}
5371
return defaultValue;
5472
}
5573

src/StatsigClient.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import Diagnostics, {
3131
DiagnosticsEvent,
3232
DiagnosticsKey,
3333
} from './utils/Diagnostics';
34+
import ConsoleLogger from './utils/ConsoleLogger';
3435

3536
const MAX_VALUE_SIZE = 64;
3637
const MAX_OBJ_SIZE = 2048;
@@ -100,6 +101,7 @@ export interface IHasStatsigInternal {
100101
getErrorBoundary(): ErrorBoundary;
101102
getSDKType(): string;
102103
getSDKVersion(): string;
104+
getConsoleLogger(): ConsoleLogger;
103105
}
104106

105107
export type StatsigOverrides = {
@@ -175,6 +177,11 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
175177
return this.identity.getSDKVersion();
176178
}
177179

180+
private consoleLogger: ConsoleLogger;
181+
public getConsoleLogger(): ConsoleLogger {
182+
return this.consoleLogger;
183+
}
184+
178185
public constructor(
179186
sdkKey: string,
180187
user?: StatsigUser | null,
@@ -189,6 +196,7 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
189196
this.ready = false;
190197
this.sdkKey = sdkKey;
191198
this.options = new StatsigSDKOptions(options);
199+
this.consoleLogger = new ConsoleLogger(this.options.getLogLevel());
192200
StatsigLocalStorage.disabled = this.options.getDisableLocalStorage();
193201
this.initializeDiagnostics = new Diagnostics('initialize');
194202
this.identity = new StatsigIdentity(
@@ -500,11 +508,13 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
500508
);
501509
}
502510
if (typeof eventName !== 'string' || eventName.length === 0) {
503-
console.error('Event not logged. No valid eventName passed.');
511+
this.consoleLogger.error(
512+
'Event not logged. No valid eventName passed.',
513+
);
504514
return;
505515
}
506516
if (this.shouldTrimParam(eventName, MAX_VALUE_SIZE)) {
507-
console.warn(
517+
this.consoleLogger.info(
508518
'eventName is too long, trimming to ' +
509519
MAX_VALUE_SIZE +
510520
' characters.',
@@ -515,11 +525,13 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
515525
typeof value === 'string' &&
516526
this.shouldTrimParam(value, MAX_VALUE_SIZE)
517527
) {
518-
console.warn('value is too long, trimming to ' + MAX_VALUE_SIZE + '.');
528+
this.consoleLogger.info(
529+
'value is too long, trimming to ' + MAX_VALUE_SIZE + '.',
530+
);
519531
value = value.substring(0, MAX_VALUE_SIZE);
520532
}
521533
if (this.shouldTrimParam(metadata, MAX_OBJ_SIZE)) {
522-
console.warn('metadata is too big. Dropping the metadata.');
534+
this.consoleLogger.info('metadata is too big. Dropping the metadata.');
523535
metadata = { error: 'not logged due to size too large' };
524536
}
525537
const event = new LogEvent(eventName);
@@ -884,18 +896,22 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
884896
return {};
885897
}
886898
if (this.shouldTrimParam(user.userID ?? null, MAX_VALUE_SIZE)) {
887-
console.warn(
899+
this.consoleLogger.info(
888900
'User ID is too large, trimming to ' + MAX_VALUE_SIZE + 'characters',
889901
);
890902
user.userID = user.userID?.toString().substring(0, MAX_VALUE_SIZE);
891903
}
892904
if (this.shouldTrimParam(user, MAX_OBJ_SIZE)) {
893905
user.custom = {};
894906
if (this.shouldTrimParam(user, MAX_OBJ_SIZE)) {
895-
console.warn('User object is too large, only keeping the user ID.');
907+
this.consoleLogger.info(
908+
'User object is too large, only keeping the user ID.',
909+
);
896910
user = { userID: user.userID };
897911
} else {
898-
console.warn('User object is too large, dropping the custom property.');
912+
this.consoleLogger.info(
913+
'User object is too large, dropping the custom property.',
914+
);
899915
}
900916
}
901917
return user;
@@ -925,7 +941,7 @@ export default class StatsigClient implements IHasStatsigInternal, IStatsig {
925941
diagnostics?: Diagnostics,
926942
): Promise<void> {
927943
if (prefetchUsers.length > 5) {
928-
console.warn('Cannot prefetch more than 5 users.');
944+
this.consoleLogger.info('Cannot prefetch more than 5 users.');
929945
}
930946

931947
const keyedPrefetchUsers = prefetchUsers.slice(0, 5).reduce((acc, curr) => {

src/StatsigLogger.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ const APP_METRICS_PAGE_LOAD_EVENT =
1919
const APP_METRICS_DOM_INTERACTIVE_EVENT =
2020
INTERNAL_EVENT_PREFIX + 'app_metrics::dom_interactive_time';
2121
const DIAGNOSTICS_EVENT = INTERNAL_EVENT_PREFIX + 'diagnostics';
22+
const DEFAULT_VALUE_WARNING = INTERNAL_EVENT_PREFIX + 'default_value';
2223

2324
type FailedLogEventBody = {
2425
events: object[];
@@ -248,6 +249,20 @@ export default class StatsigLogger {
248249
this.log(configExposure);
249250
}
250251

252+
public logConfigDefaultValueFallback(
253+
user: StatsigUser | null,
254+
message: string,
255+
metadata: object,
256+
): void {
257+
const defaultValueEvent = new LogEvent(DEFAULT_VALUE_WARNING);
258+
defaultValueEvent.setUser(user);
259+
defaultValueEvent.setValue(message);
260+
defaultValueEvent.setMetadata(metadata);
261+
this.log(defaultValueEvent);
262+
this.loggedErrors.add(message);
263+
this.sdkInternal.getConsoleLogger().error(message);
264+
}
265+
251266
public logAppError(
252267
user: StatsigUser | null,
253268
message: string,

src/StatsigSDKOptions.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import LogEvent from './LogEvent';
12
import { StatsigUser } from './StatsigUser';
23

34
const DEFAULT_FEATURE_GATE_API = 'https://featuregates.org/v1/';
@@ -32,8 +33,15 @@ export type StatsigOptions = {
3233
disableLocalStorage?: boolean;
3334
initCompletionCallback?: InitCompletionCallback | null;
3435
disableDiagnosticsLogging?: boolean;
36+
logLevel?: LogLevel | null;
3537
};
3638

39+
export enum LogLevel {
40+
'NONE',
41+
'INFO',
42+
'DEBUG',
43+
}
44+
3745
type BoundedNumberInput = {
3846
default: number;
3947
min: number;
@@ -58,6 +66,7 @@ export default class StatsigSDKOptions {
5866
private disableLocalStorage: boolean;
5967
private initCompletionCallback: InitCompletionCallback | null;
6068
private disableDiagnosticsLogging: boolean;
69+
private logLevel: LogLevel;
6170

6271
constructor(options?: StatsigOptions | null) {
6372
if (options == null) {
@@ -103,6 +112,7 @@ export default class StatsigSDKOptions {
103112
this.disableLocalStorage = options.disableLocalStorage ?? false;
104113
this.initCompletionCallback = options.initCompletionCallback ?? null;
105114
this.disableDiagnosticsLogging = options.disableDiagnosticsLogging ?? false;
115+
this.logLevel = options?.logLevel ?? LogLevel.NONE;
106116
}
107117

108118
getApi(): string {
@@ -169,6 +179,10 @@ export default class StatsigSDKOptions {
169179
return this.disableDiagnosticsLogging;
170180
}
171181

182+
getLogLevel(): LogLevel {
183+
return this.logLevel;
184+
}
185+
172186
private normalizeNumberInput(
173187
input: number | undefined,
174188
bounds: BoundedNumberInput,

src/StatsigStore.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,9 @@ export default class StatsigStore {
340340
this.overrides.configs[configName],
341341
'override',
342342
details,
343+
[],
344+
'',
345+
this.onConfigDefaultValueFallback,
343346
);
344347
} else if (this.userValues?.dynamic_configs[configNameHash] != null) {
345348
const rawConfigValue = this.userValues?.dynamic_configs[configNameHash];
@@ -572,6 +575,7 @@ export default class StatsigStore {
572575
details,
573576
apiConfig?.secondary_exposures,
574577
apiConfig?.allocated_experiment_name ?? '',
578+
this.onConfigDefaultValueFallback,
575579
);
576580
}
577581

@@ -689,4 +693,27 @@ export default class StatsigStore {
689693
StatsigLocalStorage.setItem(key, value);
690694
}
691695
}
696+
697+
private onConfigDefaultValueFallback(
698+
config: DynamicConfig,
699+
parameter: string,
700+
defaultValueType: string,
701+
valueType: string,
702+
): void {
703+
if (!this.isLoaded()) {
704+
return;
705+
}
706+
this.sdkInternal.getLogger().logConfigDefaultValueFallback(
707+
this.sdkInternal.getCurrentUser(),
708+
`Parameter ${parameter} is a value of type ${valueType}.
709+
Returning requested defaultValue type ${defaultValueType}`,
710+
{
711+
name: config.getName(),
712+
ruleID: config.getRuleID(),
713+
parameter,
714+
defaultValueType,
715+
valueType,
716+
},
717+
);
718+
}
692719
}

src/__tests__/DynamicConfigTyped.test.ts

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,45 @@ import DynamicConfig from '../DynamicConfig';
22
import { EvaluationReason } from '../StatsigStore';
33

44
describe('Verify behavior of DynamicConfig', () => {
5+
let fallback: {
6+
config: DynamicConfig;
7+
parameter: string;
8+
defaultValueType: string;
9+
valueType: string;
10+
} | null = null;
11+
12+
const onFallback = (
13+
config: DynamicConfig,
14+
parameter: string,
15+
defaultValueType: string,
16+
valueType: string,
17+
) => {
18+
fallback = {
19+
config,
20+
parameter,
21+
defaultValueType,
22+
valueType,
23+
};
24+
};
25+
26+
const expectFallback = function (
27+
config: DynamicConfig,
28+
parameter: string,
29+
defaultValue: any,
30+
valueType: string,
31+
) {
32+
expect(config.get(parameter, defaultValue)).toStrictEqual(defaultValue);
33+
const defaultValueType = Array.isArray(defaultValue)
34+
? 'array'
35+
: typeof defaultValue;
36+
expect(fallback).toStrictEqual({
37+
config,
38+
parameter,
39+
defaultValueType: defaultValueType,
40+
valueType: valueType,
41+
});
42+
fallback = null;
43+
};
544
const testConfig = new DynamicConfig(
645
'test_config',
746
{
@@ -23,6 +62,9 @@ describe('Verify behavior of DynamicConfig', () => {
2362
reason: EvaluationReason.Network,
2463
time: Date.now(),
2564
},
65+
[],
66+
'',
67+
onFallback,
2668
);
2769

2870
type TestObject = {
@@ -46,21 +88,29 @@ describe('Verify behavior of DynamicConfig', () => {
4688
};
4789

4890
beforeEach(() => {
91+
fallback = null;
4992
expect.hasAssertions();
5093
});
5194

5295
test('Test typed get', () => {
5396
expect(testConfig.get('bool', 3)).toStrictEqual(3);
97+
expectFallback(testConfig, 'bool', 3, 'boolean');
5498
expect(testConfig.getValue('111', 222)).toStrictEqual(222);
99+
// not called when default value is applied because the field is missing
100+
expect(fallback).toBeNull();
55101
expect(testConfig.get('numberStr2', 'test')).toStrictEqual('3.3');
102+
expect(fallback).toBeNull();
56103
expect(testConfig.get('boolStr1', 'test')).toStrictEqual('true');
57-
expect(testConfig.get('numberStr2', 17)).toStrictEqual(17);
104+
expect(fallback).toBeNull();
105+
expectFallback(testConfig, 'numberStr2', 17, 'string');
58106
expect(testConfig.get('arr', ['test'])).toStrictEqual([1, 2, 'three']);
59-
expect(testConfig.get('object', ['test'])).toStrictEqual(['test']);
107+
expect(fallback).toBeNull();
108+
expectFallback(testConfig, 'object', ['test'], 'object');
60109
expect(testConfig.get('object', {})).toStrictEqual({
61110
key: 'value',
62111
key2: 123,
63112
});
113+
expect(fallback).toBeNull();
64114
});
65115

66116
test('Test optional type guard when runtime check succeeds', () => {

0 commit comments

Comments
 (0)