Skip to content

Commit 71b20c8

Browse files
committed
adding debug mode; version 2.1.1-alpha.0
1 parent ce0c424 commit 71b20c8

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.0",
3+
"version": "2.1.1-alpha.0",
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
*
@@ -152,6 +164,13 @@ export default class EppoClient implements IEppoClient {
152164
this.configurationRequestConfig = configurationRequestConfig;
153165
}
154166

167+
/**
168+
* @deprecated added for temporary debugging
169+
*/
170+
public _pollerStats() {
171+
return _pollerStats();
172+
}
173+
155174
public async fetchFlagConfigurations() {
156175
if (!this.configurationRequestConfig) {
157176
throw new Error(
@@ -253,6 +272,43 @@ export default class EppoClient implements IEppoClient {
253272
}
254273
}
255274

275+
/**
276+
* @deprecated added for temporary debugging
277+
*/
278+
public _getStringAssignmentWithReason(
279+
subjectKey: string,
280+
flagKey: string,
281+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
282+
subjectAttributes: Record<string, any> = {},
283+
assignmentHooks?: IAssignmentHooks | undefined,
284+
obfuscated = false,
285+
): { assignment: string | null; reason: string } {
286+
let assignment: string | null = null;
287+
let reason = 'Unknown; pre-getAssignmentVariation';
288+
try {
289+
const eppoValue = this.getAssignmentVariation(
290+
subjectKey,
291+
flagKey,
292+
subjectAttributes,
293+
assignmentHooks,
294+
obfuscated,
295+
ValueType.StringType,
296+
);
297+
const eppoValueAssignment = eppoValue.stringValue;
298+
reason = eppoValue.reason ?? 'Unknown; post-getAssignmentVariation';
299+
if (eppoValueAssignment === undefined) {
300+
assignment = null;
301+
reason += '; coalesced to null';
302+
} else {
303+
assignment = eppoValueAssignment;
304+
}
305+
} catch (error) {
306+
reason = 'Caught error: ' + error.message + '\n' + error.stack;
307+
this.rethrowIfNotGraceful(error);
308+
}
309+
return { assignment, reason };
310+
}
311+
256312
getBoolAssignment(
257313
subjectKey: string,
258314
flagKey: string,
@@ -420,21 +476,30 @@ export default class EppoClient implements IEppoClient {
420476
experimentConfig,
421477
expectedValueType,
422478
);
479+
allowListOverride.reason = 'In override list';
423480

424481
if (!allowListOverride.isNullType()) {
425482
if (!allowListOverride.isExpectedType()) {
483+
nullAssignment.assignment.reason = 'Allow list override is not the expected type';
426484
return nullAssignment;
427485
}
428486
return { ...nullAssignment, assignment: allowListOverride };
429487
}
430488

431489
// Check for disabled flag.
432-
if (!experimentConfig?.enabled) return nullAssignment;
490+
if (!experimentConfig?.enabled) {
491+
nullAssignment.assignment.reason = 'Experiment is not enabled';
492+
return nullAssignment;
493+
}
433494

434495
// check for overridden assignment via hook
435496
const overriddenAssignment = assignmentHooks?.onPreAssignment(flagKey, subjectKey);
436497
if (overriddenAssignment !== null && overriddenAssignment !== undefined) {
437-
if (!overriddenAssignment.isExpectedType()) return nullAssignment;
498+
if (!overriddenAssignment.isExpectedType()) {
499+
nullAssignment.assignment.reason = 'Override via hook is wrong type';
500+
return nullAssignment;
501+
}
502+
overriddenAssignment.reason = 'Overriden via hook';
438503
return { ...nullAssignment, assignment: overriddenAssignment };
439504
}
440505

@@ -444,12 +509,17 @@ export default class EppoClient implements IEppoClient {
444509
experimentConfig.rules,
445510
obfuscated,
446511
);
447-
if (!matchedRule) return nullAssignment;
512+
if (!matchedRule) {
513+
nullAssignment.assignment.reason = 'No matching targeting rule';
514+
return nullAssignment;
515+
}
448516

449517
// Check if subject is in allocation sample.
450518
const allocation = experimentConfig.allocations[matchedRule.allocationKey];
451-
if (!this.isInExperimentSample(subjectKey, flagKey, experimentConfig, allocation))
519+
if (!this.isInExperimentSample(subjectKey, flagKey, experimentConfig, allocation)) {
520+
nullAssignment.assignment.reason = 'Not in experiment sample';
452521
return nullAssignment;
522+
}
453523

454524
// Compute variation for subject.
455525
const { subjectShards } = experimentConfig;
@@ -458,23 +528,27 @@ export default class EppoClient implements IEppoClient {
458528
let assignedVariation: IVariation | undefined;
459529
let holdoutVariation = null;
460530

531+
let variationReason = '';
461532
const holdoutShard = getShard(`holdout-${subjectKey}`, subjectShards);
462533
const matchingHoldout = holdouts?.find((holdout) => {
463534
const { statusQuoShardRange, shippedShardRange } = holdout;
464535
if (isShardInRange(holdoutShard, statusQuoShardRange)) {
465536
assignedVariation = variations.find(
466537
(variation) => variation.variationKey === statusQuoVariationKey,
467538
);
539+
variationReason = 'Holdout during in-flight experiment, status quo variation';
468540
// Only log the holdout variation if this is a rollout allocation
469541
// Only rollout allocations have shippedShardRange specified
470542
if (shippedShardRange) {
471543
holdoutVariation = HoldoutVariationEnum.STATUS_QUO;
544+
variationReason = 'Holdout after rollout, status quo variation';
472545
}
473546
} else if (shippedShardRange && isShardInRange(holdoutShard, shippedShardRange)) {
474547
assignedVariation = variations.find(
475548
(variation) => variation.variationKey === shippedVariationKey,
476549
);
477550
holdoutVariation = HoldoutVariationEnum.ALL_SHIPPED;
551+
variationReason = 'Holdout after rollout, shipped variation';
478552
}
479553
return assignedVariation;
480554
});
@@ -484,24 +558,33 @@ export default class EppoClient implements IEppoClient {
484558
assignedVariation = variations.find((variation) =>
485559
isShardInRange(assignmentShard, variation.shardRange),
486560
);
561+
variationReason = 'Normal assignment randomization';
487562
}
488563

564+
const variationEppoValue = EppoValue.generateEppoValue(
565+
expectedValueType,
566+
assignedVariation?.value,
567+
assignedVariation?.typedValue,
568+
);
569+
variationEppoValue.reason = variationReason;
570+
571+
const typeMismatchAssignment = nullAssignment;
572+
typeMismatchAssignment.assignment.reason = 'Uexpected variation assignment type';
573+
489574
const internalAssignment: {
490575
allocationKey: string;
491576
assignment: EppoValue;
492577
holdoutKey: string | null;
493578
holdoutVariation: NullableHoldoutVariationType;
494579
} = {
495580
allocationKey: matchedRule.allocationKey,
496-
assignment: EppoValue.generateEppoValue(
497-
expectedValueType,
498-
assignedVariation?.value,
499-
assignedVariation?.typedValue,
500-
),
581+
assignment: variationEppoValue,
501582
holdoutKey,
502583
holdoutVariation: holdoutVariation as NullableHoldoutVariationType,
503584
};
504-
return internalAssignment.assignment.isExpectedType() ? internalAssignment : nullAssignment;
585+
return internalAssignment.assignment.isExpectedType()
586+
? internalAssignment
587+
: typeMismatchAssignment;
505588
}
506589

507590
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)