Skip to content

Commit f86e3b2

Browse files
committed
refactor
1 parent a2305dc commit f86e3b2

2 files changed

Lines changed: 118 additions & 103 deletions

File tree

lib/core/decision_service/index.ts

Lines changed: 115 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ import {
7474
} from 'log_message';
7575
import { OptimizelyError } from '../../error/optimizly_error';
7676
import { CmabService } from './cmab/cmab_service';
77-
import { Maybe, OpType, OpValue } from '../../utils/type';
77+
import { Maybe, OpType } from '../../utils/type';
7878
import { Value } from '../../utils/promise/operation_value';
7979
import { Platform } from '../../platform_support';
8080

81+
import { localHoldout } from '../../feature_toggle';
82+
8183
export const EXPERIMENT_NOT_RUNNING = 'Experiment %s is not running.';
8284
export const RETURNING_STORED_VARIATION =
8385
'Returning previously activated variation "%s" of experiment "%s" for user "%s" from user profile.';
@@ -135,36 +137,48 @@ interface DecisionServiceOptions {
135137
cmabService: CmabService;
136138
}
137139

138-
interface DeliveryRuleResponse<T, K> extends DecisionResponse<T> {
139-
skipToEveryoneElse: K;
140-
/** Set when a local holdout was hit for this delivery rule (FSSDK-12369). */
141-
localHoldoutDecision?: DecisionObj;
142-
}
143140

144-
interface UserProfileTracker {
145-
userProfile: ExperimentBucketMap | null;
146-
isProfileUpdated: boolean;
147-
}
141+
export type DecisionReason = [string, ...any[]];
148142

149-
type VarationKeyWithCmabParams = {
150-
variationKey?: string;
143+
export type VariationIdWithCmabParams = {
144+
variationId? : string;
151145
cmabUuid?: string;
152-
/**
153-
* Set when the variation comes from a local holdout (FSSDK-12369).
154-
* When present, the caller should use this as the full decision result
155-
* instead of constructing one from the experiment.
156-
*/
157-
localHoldoutDecision?: DecisionObj;
158146
};
159-
export type DecisionReason = [string, ...any[]];
160-
export type VariationResult = DecisionResponse<VarationKeyWithCmabParams>;
161-
export type DecisionResult = DecisionResponse<DecisionObj>;
162-
type VariationIdWithCmabParams = {
163-
variationId? : string;
147+
148+
export type VariationKeyWithCmabParams = {
149+
variationKey? : string;
164150
cmabUuid?: string;
165151
};
152+
153+
export type DecisionResult = DecisionResponse<DecisionObj>;
154+
155+
156+
type LocalHoldoutResult = DecisionResponse<DecisionObj> & {
157+
holdoutDecision: true
158+
}
159+
160+
type ExperimentEvaluationResult = DecisionResponse<VariationKeyWithCmabParams> & {
161+
holdoutDecision?: false;
162+
}
163+
164+
export type ExperimentRuleResponse = ExperimentEvaluationResult | LocalHoldoutResult;
165+
166+
type DeliveryEvaluationResult = DecisionResponse<Variation | null> & {
167+
holdoutDecision?: false;
168+
skipToEveryoneElse: boolean;
169+
}
170+
171+
type DeliveryRuleResponse = DeliveryEvaluationResult | LocalHoldoutResult;
172+
173+
type VariationKeyResult = DecisionResponse<VariationKeyWithCmabParams>
174+
166175
export type DecideOptionsMap = Partial<Record<OptimizelyDecideOption, boolean>>;
167176

177+
interface UserProfileTracker {
178+
userProfile: ExperimentBucketMap | null;
179+
isProfileUpdated: boolean;
180+
}
181+
168182
export const CMAB_DUMMY_ENTITY_ID= '$'
169183

170184
export const LOGGER_NAME = 'DecisionService';
@@ -222,7 +236,7 @@ export class DecisionService {
222236
user: OptimizelyUserContext,
223237
decideOptions: DecideOptionsMap,
224238
userProfileTracker?: UserProfileTracker,
225-
): Value<OP, VariationResult> {
239+
): Value<OP, VariationKeyResult> {
226240
const userId = user.getUserId();
227241

228242
const experimentKey = experiment.key;
@@ -310,7 +324,7 @@ export class DecisionService {
310324
this.getDecisionForCmabExperiment(op, configObj, experiment, user, decideOptions) :
311325
this.getDecisionFromBucketer(op, configObj, experiment, user);
312326

313-
return decisionVariationValue.then((variationResult): Value<OP, VariationResult> => {
327+
return decisionVariationValue.then((variationResult): Value<OP, VariationKeyResult> => {
314328
decideReasons.push(...variationResult.reasons);
315329
if (variationResult.error) {
316330
return Value.of(op, {
@@ -1083,14 +1097,22 @@ export class DecisionService {
10831097
op, configObj, feature, fromIndex + 1, user, decideReasons, decideOptions, userProfileTracker);
10841098
}
10851099

1086-
const decisionVariationValue = this.getVariationFromExperimentRule(
1100+
const experimentResponse = this.getVariationFromExperimentRule(
10871101
op, configObj, feature.key, experiment, user, decideOptions, userProfileTracker,
10881102
);
10891103

1090-
return decisionVariationValue.then((decisionVariation) => {
1091-
decideReasons.push(...decisionVariation.reasons);
1104+
return experimentResponse.then((experimentResponse) => {
1105+
decideReasons.push(...experimentResponse.reasons);
10921106

1093-
if (decisionVariation.error) {
1107+
// If a local holdout was hit, return the holdout decision directly (preserves holdout as experiment).
1108+
if (experimentResponse.holdoutDecision) {
1109+
return Value.of(op, {
1110+
result: experimentResponse.result,
1111+
reasons: decideReasons,
1112+
});
1113+
}
1114+
1115+
if (experimentResponse.error) {
10941116
return Value.of(op, {
10951117
error: true,
10961118
result: {
@@ -1102,28 +1124,20 @@ export class DecisionService {
11021124
});
11031125
}
11041126

1105-
// If a local holdout was hit, return the holdout decision directly (preserves holdout as experiment).
1106-
if (decisionVariation.result.localHoldoutDecision) {
1107-
return Value.of(op, {
1108-
result: decisionVariation.result.localHoldoutDecision,
1109-
reasons: decideReasons,
1110-
});
1111-
}
1112-
1113-
if(!decisionVariation.result.variationKey) {
1127+
if(!experimentResponse.result.variationKey) {
11141128
return this.traverseFeatureExperimentList(
11151129
op, configObj, feature, fromIndex + 1, user, decideReasons, decideOptions, userProfileTracker);
11161130
}
11171131

1118-
const variationKey = decisionVariation.result.variationKey;
1132+
const variationKey = experimentResponse.result.variationKey;
11191133
let variation: Variation | null = experiment.variationKeyMap[variationKey];
11201134
if (!variation) {
11211135
variation = getFlagVariationByKey(configObj, feature.key, variationKey);
11221136
}
11231137

11241138
return Value.of(op, {
11251139
result: {
1126-
cmabUuid: decisionVariation.result.cmabUuid,
1140+
cmabUuid: experimentResponse.result.cmabUuid,
11271141
experiment,
11281142
variation,
11291143
decisionSource: DECISION_SOURCES.FEATURE_TEST,
@@ -1139,11 +1153,10 @@ export class DecisionService {
11391153
user: OptimizelyUserContext,
11401154
): DecisionResponse<DecisionObj> {
11411155
const decideReasons: DecisionReason[] = [];
1142-
let decisionObj: DecisionObj;
11431156
if (!feature.rolloutId) {
11441157
this.logger?.debug(NO_ROLLOUT_EXISTS, feature.key);
11451158
decideReasons.push([NO_ROLLOUT_EXISTS, feature.key]);
1146-
decisionObj = {
1159+
const decisionObj = {
11471160
experiment: null,
11481161
variation: null,
11491162
decisionSource: DECISION_SOURCES.ROLLOUT,
@@ -1163,7 +1176,7 @@ export class DecisionService {
11631176
feature.key,
11641177
);
11651178
decideReasons.push([INVALID_ROLLOUT_ID, feature.rolloutId, feature.key]);
1166-
decisionObj = {
1179+
const decisionObj = {
11671180
experiment: null,
11681181
variation: null,
11691182
decisionSource: DECISION_SOURCES.ROLLOUT,
@@ -1181,7 +1194,7 @@ export class DecisionService {
11811194
feature.rolloutId,
11821195
);
11831196
decideReasons.push([ROLLOUT_HAS_NO_EXPERIMENTS, feature.rolloutId]);
1184-
decisionObj = {
1197+
const decisionObj = {
11851198
experiment: null,
11861199
variation: null,
11871200
decisionSource: DECISION_SOURCES.ROLLOUT,
@@ -1191,26 +1204,34 @@ export class DecisionService {
11911204
reasons: decideReasons,
11921205
};
11931206
}
1194-
let decisionVariation;
1195-
let skipToEveryoneElse;
1196-
let variation;
1197-
let rolloutRule;
1207+
11981208
let index = 0;
11991209
while (index < rolloutRules.length) {
1200-
decisionVariation = this.getVariationFromDeliveryRule(configObj, feature.key, rolloutRules, index, user);
1201-
decideReasons.push(...decisionVariation.reasons);
1202-
variation = decisionVariation.result;
1203-
skipToEveryoneElse = decisionVariation.skipToEveryoneElse;
1204-
if (variation) {
1205-
// If a local holdout was hit for this delivery rule, use its decision object directly.
1206-
if (decisionVariation.localHoldoutDecision) {
1207-
return {
1208-
result: decisionVariation.localHoldoutDecision,
1209-
reasons: decideReasons,
1210-
};
1210+
const deliveryRuleResponse = this.getVariationFromDeliveryRule(configObj, feature.key, rolloutRules, index, user);
1211+
1212+
decideReasons.push(...deliveryRuleResponse.reasons);
1213+
1214+
// If a local holdout was hit for this delivery rule, use its decision object directly.
1215+
if (deliveryRuleResponse.holdoutDecision) {
1216+
return {
1217+
reasons: decideReasons,
1218+
result: deliveryRuleResponse.result,
12111219
}
1212-
rolloutRule = configObj.experimentIdMap[rolloutRules[index].id];
1213-
decisionObj = {
1220+
}
1221+
1222+
1223+
const variation = deliveryRuleResponse.result;
1224+
const skipToEveryoneElse = deliveryRuleResponse.skipToEveryoneElse;
1225+
1226+
if (variation) {
1227+
// if (decisionVariation.localHoldoutDecision) {
1228+
// return {
1229+
// result: decisionVariation.localHoldoutDecision,
1230+
// reasons: decideReasons,
1231+
// };
1232+
// }
1233+
const rolloutRule = configObj.experimentIdMap[rolloutRules[index].id];
1234+
const decisionObj = {
12141235
experiment: rolloutRule,
12151236
variation: variation,
12161237
decisionSource: DECISION_SOURCES.ROLLOUT
@@ -1224,7 +1245,7 @@ export class DecisionService {
12241245
index = skipToEveryoneElse ? (rolloutRules.length - 1) : (index + 1);
12251246
}
12261247

1227-
decisionObj = {
1248+
const decisionObj = {
12281249
experiment: null,
12291250
variation: null,
12301251
decisionSource: DECISION_SOURCES.ROLLOUT,
@@ -1573,7 +1594,7 @@ export class DecisionService {
15731594
user: OptimizelyUserContext,
15741595
decideOptions: DecideOptionsMap,
15751596
userProfileTracker?: UserProfileTracker,
1576-
): Value<OP, VariationResult> {
1597+
): Value<OP, ExperimentRuleResponse> {
15771598
const decideReasons: DecisionReason[] = [];
15781599

15791600
// check forced decision first
@@ -1588,44 +1609,34 @@ export class DecisionService {
15881609
});
15891610
}
15901611

1591-
// Check local holdouts targeting this specific experiment rule (FSSDK-12369).
1592-
// Inserted immediately after the forced-decision block, before regular rule evaluation.
1593-
const localHoldoutsForExperiment = getHoldoutsForRule(configObj, rule.id);
1594-
for (const holdout of localHoldoutsForExperiment) {
1595-
const holdoutDecision = this.getVariationForHoldout(configObj, holdout, user);
1596-
decideReasons.push(...holdoutDecision.reasons);
1597-
if (holdoutDecision.result.variation) {
1598-
// Signal the caller to use the holdout decision directly, preserving the holdout as experiment.
1599-
return Value.of(op, {
1600-
result: {
1601-
variationKey: holdoutDecision.result.variation.key,
1602-
localHoldoutDecision: holdoutDecision.result,
1603-
},
1604-
reasons: decideReasons,
1605-
});
1612+
if (localHoldout()) {
1613+
// Check local holdouts targeting this specific experiment rule.
1614+
// Inserted immediately after the forced-decision block, before regular rule evaluation.
1615+
const localHoldoutsForExperiment = getHoldoutsForRule(configObj, rule.id);
1616+
for (const holdout of localHoldoutsForExperiment) {
1617+
const holdoutDecision = this.getVariationForHoldout(configObj, holdout, user);
1618+
decideReasons.push(...holdoutDecision.reasons);
1619+
if (holdoutDecision.result.variation) {
1620+
// Signal the caller to use the holdout decision directly, preserving the holdout as experiment.
1621+
return Value.of<OP, LocalHoldoutResult>(op, {
1622+
result: holdoutDecision.result,
1623+
reasons: decideReasons,
1624+
holdoutDecision: true,
1625+
});
1626+
}
16061627
}
16071628
}
16081629

16091630
const decisionVariationValue = this.resolveVariation(op, configObj, rule, user, decideOptions, userProfileTracker);
16101631

16111632
return decisionVariationValue.then((variationResult) => {
16121633
decideReasons.push(...variationResult.reasons);
1613-
return Value.of(op, {
1634+
return Value.of<OP, ExperimentEvaluationResult>(op, {
16141635
error: variationResult.error,
16151636
result: variationResult.result,
16161637
reasons: decideReasons,
16171638
});
16181639
});
1619-
1620-
// return response;
1621-
1622-
// decideReasons.push(...decisionVariation.reasons);
1623-
// const variationKey = decisionVariation.result;
1624-
1625-
// return {
1626-
// result: variationKey,
1627-
// reasons: decideReasons,
1628-
// };
16291640
}
16301641

16311642
private getVariationFromDeliveryRule(
@@ -1634,7 +1645,7 @@ export class DecisionService {
16341645
rules: Experiment[],
16351646
ruleIndex: number,
16361647
user: OptimizelyUserContext
1637-
): DeliveryRuleResponse<Variation | null, boolean> {
1648+
): DeliveryRuleResponse {
16381649
const decideReasons: DecisionReason[] = [];
16391650
let skipToEveryoneElse = false;
16401651

@@ -1652,19 +1663,20 @@ export class DecisionService {
16521663
};
16531664
}
16541665

1655-
// Check local holdouts targeting this specific delivery rule (FSSDK-12369).
1656-
// Inserted immediately after the forced-decision block, before audience and traffic allocation checks.
1657-
const localHoldoutsForDelivery = getHoldoutsForRule(configObj, rule.id);
1658-
for (const holdout of localHoldoutsForDelivery) {
1659-
const holdoutDecision = this.getVariationForHoldout(configObj, holdout, user);
1660-
decideReasons.push(...holdoutDecision.reasons);
1661-
if (holdoutDecision.result.variation) {
1662-
return {
1663-
result: holdoutDecision.result.variation,
1664-
reasons: decideReasons,
1665-
skipToEveryoneElse,
1666-
localHoldoutDecision: holdoutDecision.result,
1667-
};
1666+
if (localHoldout()) {
1667+
// Check local holdouts targeting this specific delivery rule (FSSDK-12369).
1668+
// Inserted immediately after the forced-decision block, before audience and traffic allocation checks.
1669+
const localHoldoutsForDelivery = getHoldoutsForRule(configObj, rule.id);
1670+
for (const holdout of localHoldoutsForDelivery) {
1671+
const holdoutDecision = this.getVariationForHoldout(configObj, holdout, user);
1672+
decideReasons.push(...holdoutDecision.reasons);
1673+
if (holdoutDecision.result.variation) {
1674+
return {
1675+
result: holdoutDecision.result,
1676+
reasons: decideReasons,
1677+
holdoutDecision: true,
1678+
};
1679+
}
16681680
}
16691681
}
16701682

lib/feature_toggle.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ import { Platform } from './platform_support';
3939

4040
export type IfActive<T extends () => boolean, Y, N = unknown> = ReturnType<T> extends true ? Y : N;
4141

42+
43+
export const localHoldout = () => true as const;
44+
4245
export const __platforms: Platform[] = ['__universal__'];

0 commit comments

Comments
 (0)