Skip to content

Commit e215322

Browse files
leoromanovskyaarsilv
authored andcommitted
adding debug mode; version 2.1.1-alpha.0
1 parent b04f796 commit e215322

File tree

6 files changed

+153
-20
lines changed

6 files changed

+153
-20
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@eppo/js-client-sdk-common",
3-
"version": "2.1.1",
3+
"version": "2.1.1-alpha.1",
44
"description": "Eppo SDK for client-side JavaScript applications (base for both web and react native)",
55
"main": "dist/index.js",
66
"files": [

src/client/eppo-client.spec.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,14 @@ describe('EppoClient E2E test', () => {
164164
expect(
165165
client.getParsedJSONAssignment('subject-identifer', flagKey, {}, mockHooks),
166166
).toBeNull();
167-
expect(client.getStringAssignment('subject-identifer', flagKey, {}, mockHooks)).toBeNull();
167+
const assignmentWithReason = client._getStringAssignmentWithReason(
168+
'subject-identifer',
169+
flagKey,
170+
{},
171+
mockHooks,
172+
);
173+
expect(assignmentWithReason.reason).toContain('Error');
174+
expect(assignmentWithReason.assignment).toBeNull();
168175
});
169176

170177
it('throws error when graceful failure is false', async () => {
@@ -191,7 +198,7 @@ describe('EppoClient E2E test', () => {
191198
}).toThrow();
192199

193200
expect(() => {
194-
client.getStringAssignment('subject-identifer', flagKey, {}, mockHooks);
201+
client._getStringAssignmentWithReason('subject-identifer', flagKey, {}, mockHooks);
195202
}).toThrow();
196203
});
197204
});
@@ -856,11 +863,13 @@ describe('EppoClient E2E test', () => {
856863
return EppoValue.Numeric(na);
857864
}
858865
case ValueTestType.StringType: {
859-
const sa = globalClient.getStringAssignment(
866+
const assignmentWithReason = globalClient._getStringAssignmentWithReason(
860867
subject.subjectKey,
861868
experiment,
862869
subject.subjectAttributes,
863870
);
871+
const sa = assignmentWithReason.assignment;
872+
console.log('Assigned ' + sa + ' because ' + assignmentWithReason.reason);
864873
if (sa === null) return null;
865874
return EppoValue.String(sa);
866875
}
@@ -966,9 +975,9 @@ describe('EppoClient E2E test', () => {
966975
expect(td.explain(mockHooks.onPostAssignment).callCount).toEqual(1);
967976
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[0]).toEqual(flagKey);
968977
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[1]).toEqual(subject);
969-
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(
970-
EppoValue.String(variation ?? ''),
971-
);
978+
const eppoValue = EppoValue.String(variation ?? '');
979+
eppoValue.reason = 'Normal assignment randomization';
980+
expect(td.explain(mockHooks.onPostAssignment).calls[0].args[2]).toEqual(eppoValue);
972981
});
973982
});
974983
});
@@ -1069,13 +1078,15 @@ describe(' EppoClient getAssignment From Obfuscated RAC', () => {
10691078
return EppoValue.Numeric(na);
10701079
}
10711080
case ValueTestType.StringType: {
1072-
const sa = globalClient.getStringAssignment(
1081+
const assignmentWithReason = globalClient._getStringAssignmentWithReason(
10731082
subject.subjectKey,
10741083
experiment,
10751084
subject.subjectAttributes,
10761085
undefined,
10771086
true,
10781087
);
1088+
const sa = assignmentWithReason.assignment;
1089+
console.log('Assigned ' + sa + ' because ' + assignmentWithReason.reason);
10791090
if (sa === null) return null;
10801091
return EppoValue.String(sa);
10811092
}

src/client/eppo-client.ts

Lines changed: 94 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { EppoValue, ValueType } from '../eppo_value';
3030
import ExperimentConfigurationRequestor from '../experiment-configuration-requestor';
3131
import HttpClient from '../http-client';
3232
import { getMD5Hash } from '../obfuscation';
33-
import initPoller, { IPoller } from '../poller';
33+
import initPoller, { IPoller, _pollerStats } from '../poller';
3434
import { findMatchingRule } from '../rule_evaluator';
3535
import { getShard, isShardInRange } from '../shard';
3636
import { validateNotBlank } from '../validation';
@@ -59,6 +59,18 @@ export interface IEppoClient {
5959
assignmentHooks?: IAssignmentHooks,
6060
): string | null;
6161

62+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
63+
_pollerStats(): any;
64+
65+
_getStringAssignmentWithReason(
66+
subjectKey: string,
67+
flagKey: string,
68+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
69+
subjectAttributes?: Record<string, any>,
70+
assignmentHooks?: IAssignmentHooks,
71+
obfuscated?: boolean,
72+
): { assignment: string | null; reason: string };
73+
6274
/**
6375
* Maps a subject to a variation for a given experiment.
6476
*
@@ -162,6 +174,13 @@ export default class EppoClient implements IEppoClient {
162174
this.configurationRequestParameters = configurationRequestParameters;
163175
}
164176

177+
/**
178+
* @deprecated added for temporary debugging
179+
*/
180+
public _pollerStats() {
181+
return _pollerStats();
182+
}
183+
165184
public async fetchFlagConfigurations() {
166185
if (!this.configurationRequestParameters) {
167186
throw new Error(
@@ -264,6 +283,43 @@ export default class EppoClient implements IEppoClient {
264283
}
265284
}
266285

286+
/**
287+
* @deprecated added for temporary debugging
288+
*/
289+
public _getStringAssignmentWithReason(
290+
subjectKey: string,
291+
flagKey: string,
292+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
293+
subjectAttributes: Record<string, any> = {},
294+
assignmentHooks?: IAssignmentHooks | undefined,
295+
obfuscated = false,
296+
): { assignment: string | null; reason: string } {
297+
let assignment: string | null = null;
298+
let reason = 'Unknown; pre-getAssignmentVariation';
299+
try {
300+
const eppoValue = this.getAssignmentVariation(
301+
subjectKey,
302+
flagKey,
303+
subjectAttributes,
304+
assignmentHooks,
305+
obfuscated,
306+
ValueType.StringType,
307+
);
308+
const eppoValueAssignment = eppoValue.stringValue;
309+
reason = eppoValue.reason ?? 'Unknown; post-getAssignmentVariation';
310+
if (eppoValueAssignment === undefined) {
311+
assignment = null;
312+
reason += '; coalesced to null';
313+
} else {
314+
assignment = eppoValueAssignment;
315+
}
316+
} catch (error) {
317+
reason = 'Caught error: ' + error.message + '\n' + error.stack;
318+
this.rethrowIfNotGraceful(error);
319+
}
320+
return { assignment, reason };
321+
}
322+
267323
getBoolAssignment(
268324
subjectKey: string,
269325
flagKey: string,
@@ -431,21 +487,30 @@ export default class EppoClient implements IEppoClient {
431487
experimentConfig,
432488
expectedValueType,
433489
);
490+
allowListOverride.reason = 'In override list';
434491

435492
if (!allowListOverride.isNullType()) {
436493
if (!allowListOverride.isExpectedType()) {
494+
nullAssignment.assignment.reason = 'Allow list override is not the expected type';
437495
return nullAssignment;
438496
}
439497
return { ...nullAssignment, assignment: allowListOverride };
440498
}
441499

442500
// Check for disabled flag.
443-
if (!experimentConfig?.enabled) return nullAssignment;
501+
if (!experimentConfig?.enabled) {
502+
nullAssignment.assignment.reason = 'Experiment is not enabled';
503+
return nullAssignment;
504+
}
444505

445506
// check for overridden assignment via hook
446507
const overriddenAssignment = assignmentHooks?.onPreAssignment(flagKey, subjectKey);
447508
if (overriddenAssignment !== null && overriddenAssignment !== undefined) {
448-
if (!overriddenAssignment.isExpectedType()) return nullAssignment;
509+
if (!overriddenAssignment.isExpectedType()) {
510+
nullAssignment.assignment.reason = 'Override via hook is wrong type';
511+
return nullAssignment;
512+
}
513+
overriddenAssignment.reason = 'Overriden via hook';
449514
return { ...nullAssignment, assignment: overriddenAssignment };
450515
}
451516

@@ -455,12 +520,17 @@ export default class EppoClient implements IEppoClient {
455520
experimentConfig.rules,
456521
obfuscated,
457522
);
458-
if (!matchedRule) return nullAssignment;
523+
if (!matchedRule) {
524+
nullAssignment.assignment.reason = 'No matching targeting rule';
525+
return nullAssignment;
526+
}
459527

460528
// Check if subject is in allocation sample.
461529
const allocation = experimentConfig.allocations[matchedRule.allocationKey];
462-
if (!this.isInExperimentSample(subjectKey, flagKey, experimentConfig, allocation))
530+
if (!this.isInExperimentSample(subjectKey, flagKey, experimentConfig, allocation)) {
531+
nullAssignment.assignment.reason = 'Not in experiment sample';
463532
return nullAssignment;
533+
}
464534

465535
// Compute variation for subject.
466536
const { subjectShards } = experimentConfig;
@@ -469,23 +539,27 @@ export default class EppoClient implements IEppoClient {
469539
let assignedVariation: IVariation | undefined;
470540
let holdoutVariation = null;
471541

542+
let variationReason = '';
472543
const holdoutShard = getShard(`holdout-${subjectKey}`, subjectShards);
473544
const matchingHoldout = holdouts?.find((holdout) => {
474545
const { statusQuoShardRange, shippedShardRange } = holdout;
475546
if (isShardInRange(holdoutShard, statusQuoShardRange)) {
476547
assignedVariation = variations.find(
477548
(variation) => variation.variationKey === statusQuoVariationKey,
478549
);
550+
variationReason = 'Holdout during in-flight experiment, status quo variation';
479551
// Only log the holdout variation if this is a rollout allocation
480552
// Only rollout allocations have shippedShardRange specified
481553
if (shippedShardRange) {
482554
holdoutVariation = HoldoutVariationEnum.STATUS_QUO;
555+
variationReason = 'Holdout after rollout, status quo variation';
483556
}
484557
} else if (shippedShardRange && isShardInRange(holdoutShard, shippedShardRange)) {
485558
assignedVariation = variations.find(
486559
(variation) => variation.variationKey === shippedVariationKey,
487560
);
488561
holdoutVariation = HoldoutVariationEnum.ALL_SHIPPED;
562+
variationReason = 'Holdout after rollout, shipped variation';
489563
}
490564
return assignedVariation;
491565
});
@@ -495,24 +569,33 @@ export default class EppoClient implements IEppoClient {
495569
assignedVariation = variations.find((variation) =>
496570
isShardInRange(assignmentShard, variation.shardRange),
497571
);
572+
variationReason = 'Normal assignment randomization';
498573
}
499574

575+
const variationEppoValue = EppoValue.generateEppoValue(
576+
expectedValueType,
577+
assignedVariation?.value,
578+
assignedVariation?.typedValue,
579+
);
580+
variationEppoValue.reason = variationReason;
581+
582+
const typeMismatchAssignment = nullAssignment;
583+
typeMismatchAssignment.assignment.reason = 'Uexpected variation assignment type';
584+
500585
const internalAssignment: {
501586
allocationKey: string;
502587
assignment: EppoValue;
503588
holdoutKey: string | null;
504589
holdoutVariation: NullableHoldoutVariationType;
505590
} = {
506591
allocationKey: matchedRule.allocationKey,
507-
assignment: EppoValue.generateEppoValue(
508-
expectedValueType,
509-
assignedVariation?.value,
510-
assignedVariation?.typedValue,
511-
),
592+
assignment: variationEppoValue,
512593
holdoutKey,
513594
holdoutVariation: holdoutVariation as NullableHoldoutVariationType,
514595
};
515-
return internalAssignment.assignment.isExpectedType() ? internalAssignment : nullAssignment;
596+
return internalAssignment.assignment.isExpectedType()
597+
? internalAssignment
598+
: typeMismatchAssignment;
516599
}
517600

518601
public setLogger(logger: IAssignmentLogger) {

src/eppo_value.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ export class EppoValue {
1717
public stringValue: string | undefined;
1818
public objectValue: object | undefined;
1919

20+
public reason: string | undefined;
21+
2022
private constructor(
2123
valueType: ValueType,
2224
boolValue: boolean | undefined,

src/poller.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as td from 'testdouble';
22

33
import { POLL_INTERVAL_MS, POLL_JITTER_PCT } from './constants';
4-
import initPoller from './poller';
4+
import initPoller, { pollerStats } from './poller';
55

66
describe('poller', () => {
77
const testIntervalMs = POLL_INTERVAL_MS;
@@ -15,6 +15,7 @@ describe('poller', () => {
1515
afterEach(() => {
1616
td.reset();
1717
jest.clearAllTimers();
18+
console.log('>>>> POLLER STATS', pollerStats());
1819
});
1920

2021
afterAll(() => {

src/poller.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@ export interface IPoller {
99
stop: () => void;
1010
}
1111

12+
// Basic stats
13+
let initializations = 0;
14+
let attemptedPolls = 0;
15+
let failedPolls = 0;
16+
let succeededPolls = 0;
17+
const pollDurations: number[] = [];
18+
const failureMessages: string[] = [];
19+
20+
/**
21+
* @deprecated added for temporary debugging
22+
*/
23+
export function _pollerStats() {
24+
return {
25+
initializations,
26+
attemptedPolls,
27+
failedPolls,
28+
succeededPolls,
29+
pollDurations,
30+
failureMessages,
31+
};
32+
}
33+
1234
// TODO: change this to a class with methods instead of something that returns a function
1335

1436
export default function initPoller(
@@ -24,6 +46,7 @@ export default function initPoller(
2446
pollAfterFailedStart?: boolean;
2547
},
2648
): IPoller {
49+
initializations += 1;
2750
let stopped = false;
2851
let failedAttempts = 0;
2952
let nextPollMs = intervalMs;
@@ -40,11 +63,17 @@ export default function initPoller(
4063

4164
while (!startRequestSuccess && startAttemptsRemaining > 0) {
4265
try {
66+
attemptedPolls += 1;
67+
const timerStart = Date.now();
4368
await callback();
69+
pollDurations.push(Date.now() - timerStart);
70+
succeededPolls += 1;
4471
startRequestSuccess = true;
4572
previousPollFailed = false;
4673
console.log('Eppo SDK successfully requested initial configuration');
4774
} catch (pollingError) {
75+
failedPolls += 1;
76+
failureMessages.push(pollingError.message);
4877
previousPollFailed = true;
4978
console.warn(
5079
`Eppo SDK encountered an error with initial poll of configurations: ${pollingError.message}`,
@@ -104,15 +133,22 @@ export default function initPoller(
104133
}
105134

106135
try {
136+
attemptedPolls += 1;
137+
const timerStart = Date.now();
107138
await callback();
139+
pollDurations.push(Date.now() - timerStart);
108140
// If no error, reset any retrying
141+
succeededPolls += 1;
109142
failedAttempts = 0;
110143
nextPollMs = intervalMs;
111144
if (previousPollFailed) {
112145
previousPollFailed = false;
113146
console.log('Eppo SDK poll successful; resuming normal polling');
114147
}
115148
} catch (error) {
149+
failedPolls += 1;
150+
failureMessages.push(error.message);
151+
116152
previousPollFailed = true;
117153
console.warn(`Eppo SDK encountered an error polling configurations: ${error.message}`);
118154
const maxTries = 1 + (options?.maxPollRetries ?? DEFAULT_POLL_CONFIG_REQUEST_RETRIES);

0 commit comments

Comments
 (0)