Skip to content

Commit 3f02f82

Browse files
committed
cmab client
1 parent 6861f65 commit 3f02f82

File tree

2 files changed

+118
-0
lines changed

2 files changed

+118
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Copyright 2025, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { OptimizelyError } from "../../../error/optimizly_error";
18+
import { CMAB_FETCH_FAILED, INVALID_CMAB_FETCH_RESPONSE } from "../../../message/error_message";
19+
import { UserAttributes } from "../../../shared_types";
20+
import { runWithRetry } from "../../../utils/executor/backoff_retry_runner";
21+
import { sprintf } from "../../../utils/fns";
22+
import { RequestHandler } from "../../../utils/http_request_handler/http";
23+
import { isSuccessStatusCode } from "../../../utils/http_request_handler/http_util";
24+
import { BackoffController } from "../../../utils/repeater/repeater";
25+
import { Producer } from "../../../utils/type";
26+
27+
export interface CmabClient {
28+
fetchVariation(
29+
experimentId: string,
30+
userId: string,
31+
attributes: UserAttributes,
32+
cmabUuid: string
33+
): Promise<string>
34+
}
35+
36+
const CMAB_PREDICTION_ENDPOINT = 'https://prediction.cmab.optimizely.com/predict/%s';
37+
38+
export type RetryConfig = {
39+
maxRetries: number,
40+
backoffProvider?: Producer<BackoffController>;
41+
}
42+
43+
export type CmabClientConfig = {
44+
requestHandler: RequestHandler,
45+
retryConfig?: RetryConfig;
46+
}
47+
48+
export class DefaultCmabClient implements CmabClient {
49+
private requestHandler: RequestHandler;
50+
private retryConfig?: RetryConfig;
51+
52+
constructor(config: CmabClientConfig) {
53+
this.requestHandler = config.requestHandler;
54+
this.retryConfig = config.retryConfig;
55+
}
56+
57+
async fetchVariation(
58+
experimentId: string,
59+
userId: string,
60+
attributes: UserAttributes,
61+
cmabUuid: string
62+
): Promise<string> {
63+
const url = sprintf(CMAB_PREDICTION_ENDPOINT, experimentId);
64+
65+
const cmabAttributes = Object.keys(attributes).map((key) => ({
66+
id: key,
67+
value: attributes[key],
68+
type: 'custom_attribute',
69+
}));
70+
71+
const body = {
72+
instances: [
73+
{
74+
visitorId: userId,
75+
experimentId,
76+
attributes: cmabAttributes,
77+
cmabUUID: cmabUuid,
78+
}
79+
]
80+
}
81+
82+
const variation = await (this.retryConfig ?
83+
runWithRetry(
84+
() => this.doFetch(url, JSON.stringify(body)),
85+
this.retryConfig.backoffProvider?.(),
86+
this.retryConfig.maxRetries,
87+
).result : this.doFetch(url, JSON.stringify(body))
88+
);
89+
90+
return variation;
91+
}
92+
93+
private async doFetch(url: string, data: string): Promise<string> {
94+
const response = await this.requestHandler.makeRequest(
95+
url,
96+
{ 'Content-Type': 'application/json' },
97+
'POST',
98+
data,
99+
).responsePromise;
100+
101+
if (isSuccessStatusCode(response.statusCode)) {
102+
return Promise.reject(new OptimizelyError(CMAB_FETCH_FAILED, response.statusCode));
103+
}
104+
105+
const body = JSON.parse(response.body);
106+
if (!this.validateResponse(body)) {
107+
return Promise.reject(new OptimizelyError(INVALID_CMAB_FETCH_RESPONSE));
108+
}
109+
110+
return String(body.predictions[0].variation_id);
111+
}
112+
113+
private validateResponse(body: any): boolean {
114+
return body.predictions && body.predictions.length > 0 && body.predictions[0].variation_id;
115+
}
116+
}

lib/message/error_message.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,7 @@ export const ODP_EVENT_MANAGER_STOPPED = "ODP event manager stopped before it co
106106
export const DATAFILE_MANAGER_FAILED_TO_START = 'Datafile manager failed to start';
107107
export const UNABLE_TO_ATTACH_UNLOAD = 'unable to bind optimizely.close() to page unload event: "%s"';
108108
export const UNABLE_TO_PARSE_AND_SKIPPED_HEADER = 'Unable to parse & skipped header item';
109+
export const CMAB_FETCH_FAILED = 'CMAB variation fetch failed with status: %s';
110+
export const INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response';
109111

110112
export const messages: string[] = [];

0 commit comments

Comments
 (0)