From 3f02f8297db1b7ea07f0d1519524d12950b849b0 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 26 Feb 2025 00:25:30 +0600 Subject: [PATCH 1/4] cmab client --- lib/core/decision_service/cmab/cmab_client.ts | 116 ++++++++++++++++++ lib/message/error_message.ts | 2 + 2 files changed, 118 insertions(+) create mode 100644 lib/core/decision_service/cmab/cmab_client.ts diff --git a/lib/core/decision_service/cmab/cmab_client.ts b/lib/core/decision_service/cmab/cmab_client.ts new file mode 100644 index 000000000..5011f100c --- /dev/null +++ b/lib/core/decision_service/cmab/cmab_client.ts @@ -0,0 +1,116 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { OptimizelyError } from "../../../error/optimizly_error"; +import { CMAB_FETCH_FAILED, INVALID_CMAB_FETCH_RESPONSE } from "../../../message/error_message"; +import { UserAttributes } from "../../../shared_types"; +import { runWithRetry } from "../../../utils/executor/backoff_retry_runner"; +import { sprintf } from "../../../utils/fns"; +import { RequestHandler } from "../../../utils/http_request_handler/http"; +import { isSuccessStatusCode } from "../../../utils/http_request_handler/http_util"; +import { BackoffController } from "../../../utils/repeater/repeater"; +import { Producer } from "../../../utils/type"; + +export interface CmabClient { + fetchVariation( + experimentId: string, + userId: string, + attributes: UserAttributes, + cmabUuid: string + ): Promise +} + +const CMAB_PREDICTION_ENDPOINT = 'https://prediction.cmab.optimizely.com/predict/%s'; + +export type RetryConfig = { + maxRetries: number, + backoffProvider?: Producer; +} + +export type CmabClientConfig = { + requestHandler: RequestHandler, + retryConfig?: RetryConfig; +} + +export class DefaultCmabClient implements CmabClient { + private requestHandler: RequestHandler; + private retryConfig?: RetryConfig; + + constructor(config: CmabClientConfig) { + this.requestHandler = config.requestHandler; + this.retryConfig = config.retryConfig; + } + + async fetchVariation( + experimentId: string, + userId: string, + attributes: UserAttributes, + cmabUuid: string + ): Promise { + const url = sprintf(CMAB_PREDICTION_ENDPOINT, experimentId); + + const cmabAttributes = Object.keys(attributes).map((key) => ({ + id: key, + value: attributes[key], + type: 'custom_attribute', + })); + + const body = { + instances: [ + { + visitorId: userId, + experimentId, + attributes: cmabAttributes, + cmabUUID: cmabUuid, + } + ] + } + + const variation = await (this.retryConfig ? + runWithRetry( + () => this.doFetch(url, JSON.stringify(body)), + this.retryConfig.backoffProvider?.(), + this.retryConfig.maxRetries, + ).result : this.doFetch(url, JSON.stringify(body)) + ); + + return variation; + } + + private async doFetch(url: string, data: string): Promise { + const response = await this.requestHandler.makeRequest( + url, + { 'Content-Type': 'application/json' }, + 'POST', + data, + ).responsePromise; + + if (isSuccessStatusCode(response.statusCode)) { + return Promise.reject(new OptimizelyError(CMAB_FETCH_FAILED, response.statusCode)); + } + + const body = JSON.parse(response.body); + if (!this.validateResponse(body)) { + return Promise.reject(new OptimizelyError(INVALID_CMAB_FETCH_RESPONSE)); + } + + return String(body.predictions[0].variation_id); + } + + private validateResponse(body: any): boolean { + return body.predictions && body.predictions.length > 0 && body.predictions[0].variation_id; + } +} diff --git a/lib/message/error_message.ts b/lib/message/error_message.ts index 66bf469f7..b0b5247ae 100644 --- a/lib/message/error_message.ts +++ b/lib/message/error_message.ts @@ -106,5 +106,7 @@ export const ODP_EVENT_MANAGER_STOPPED = "ODP event manager stopped before it co export const DATAFILE_MANAGER_FAILED_TO_START = 'Datafile manager failed to start'; export const UNABLE_TO_ATTACH_UNLOAD = 'unable to bind optimizely.close() to page unload event: "%s"'; export const UNABLE_TO_PARSE_AND_SKIPPED_HEADER = 'Unable to parse & skipped header item'; +export const CMAB_FETCH_FAILED = 'CMAB variation fetch failed with status: %s'; +export const INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'; export const messages: string[] = []; From ef217c4b2fa1f4ed0e8314d865cb3d5e4f05e9df Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 27 Feb 2025 05:42:20 +0600 Subject: [PATCH 2/4] [FSSDK-11125] implement CMAB client --- .../decision_service/cmab/cmab_client.spec.ts | 341 ++++++++++++++++++ lib/core/decision_service/cmab/cmab_client.ts | 6 +- 2 files changed, 344 insertions(+), 3 deletions(-) create mode 100644 lib/core/decision_service/cmab/cmab_client.spec.ts diff --git a/lib/core/decision_service/cmab/cmab_client.spec.ts b/lib/core/decision_service/cmab/cmab_client.spec.ts new file mode 100644 index 000000000..4f536639a --- /dev/null +++ b/lib/core/decision_service/cmab/cmab_client.spec.ts @@ -0,0 +1,341 @@ +import { describe, it, expect, vi, Mocked, Mock, MockInstance, beforeEach, afterEach } from 'vitest'; + +import { DefaultCmabClient } from './cmab_client'; +import { getMockAbortableRequest, getMockRequestHandler } from '../../../tests/mock/mock_request_handler'; +import { RequestHandler } from '../../../utils/http_request_handler/http'; +import { advanceTimersByTime, exhaustMicrotasks } from '../../../tests/testUtils'; +import { OptimizelyError } from '../../../error/optimizly_error'; + +const mockSuccessResponse = (variation: string) => Promise.resolve({ + statusCode: 200, + body: JSON.stringify({ + predictions: [ + { + variation_id: variation, + }, + ], + }), + headers: {} +}); + +const mockErrorResponse = (statusCode: number) => Promise.resolve({ + statusCode, + body: '', + headers: {}, +}); + +const assertRequest = ( + call: number, + mockRequestHandler: MockInstance, + experimentId: string, + userId: string, + attributes: Record, + cmabUuid: string, +) => { + const [requestUrl, headers, method, data] = mockRequestHandler.mock.calls[call]; + expect(requestUrl).toBe(`https://prediction.cmab.optimizely.com/predict/${experimentId}`); + expect(method).toBe('POST'); + expect(headers).toEqual({ + 'Content-Type': 'application/json', + }); + + const parsedData = JSON.parse(data!); + expect(parsedData.instances).toEqual([ + { + visitorId: userId, + experimentId, + attributes: Object.keys(attributes).map((key) => ({ + id: key, + value: attributes[key], + type: 'custom_attribute', + })), + cmabUUID: cmabUuid, + } + ]); +}; + +describe('DefaultCmabClient', () => { + it('should fetch variation using correct parameters', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(mockSuccessResponse('var123'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + const variation = await cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid); + + expect(variation).toBe('var123'); + assertRequest(0, mockMakeRequest, experimentId, userId, attributes, cmabUuid); + }); + + it('should retry fetch if retryConfig is provided', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValueOnce(getMockAbortableRequest(Promise.reject('error'))) + .mockReturnValueOnce(getMockAbortableRequest(mockErrorResponse(500))) + .mockReturnValueOnce(getMockAbortableRequest(mockSuccessResponse('var123'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + }, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + const variation = await cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid); + + expect(variation).toBe('var123'); + expect(mockMakeRequest.mock.calls.length).toBe(3); + for(let i = 0; i < 3; i++) { + assertRequest(i, mockMakeRequest, experimentId, userId, attributes, cmabUuid); + } + }); + + it('should use backoff provider if provided', async () => { + vi.useFakeTimers(); + + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValueOnce(getMockAbortableRequest(Promise.reject('error'))) + .mockReturnValueOnce(getMockAbortableRequest(mockErrorResponse(500))) + .mockReturnValueOnce(getMockAbortableRequest(mockErrorResponse(500))) + .mockReturnValueOnce(getMockAbortableRequest(mockSuccessResponse('var123'))); + + const backoffProvider = () => { + let call = 0; + const values = [100, 200, 300]; + return { + reset: () => {}, + backoff: () => { + return values[call++]; + }, + }; + } + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + backoffProvider, + }, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + const fetchPromise = cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid); + + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(1); + + // first backoff is 100ms, should not retry yet + await advanceTimersByTime(90); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(1); + + // first backoff is 100ms, should retry now + await advanceTimersByTime(10); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(2); + + // second backoff is 200ms, should not retry 2nd time yet + await advanceTimersByTime(150); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(2); + + // second backoff is 200ms, should retry 2nd time now + await advanceTimersByTime(50); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(3); + + // third backoff is 300ms, should not retry 3rd time yet + await advanceTimersByTime(280); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(3); + + // third backoff is 300ms, should retry 3rd time now + await advanceTimersByTime(20); + await exhaustMicrotasks(); + expect(mockMakeRequest.mock.calls.length).toBe(4); + + const variation = await fetchPromise; + + expect(variation).toBe('var123'); + expect(mockMakeRequest.mock.calls.length).toBe(4); + for(let i = 0; i < 4; i++) { + assertRequest(i, mockMakeRequest, experimentId, userId, attributes, cmabUuid); + } + vi.useRealTimers(); + }); + + it('should reject the promise after retries are exhausted', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(Promise.reject('error'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + }, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow(); + expect(mockMakeRequest.mock.calls.length).toBe(6); + }); + + it('should reject the promise after retries are exhausted with error status', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(mockErrorResponse(500))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + retryConfig: { + maxRetries: 5, + }, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow(); + expect(mockMakeRequest.mock.calls.length).toBe(6); + }); + + it('should not retry if retryConfig is not provided', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValueOnce(getMockAbortableRequest(Promise.reject('error'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow(); + expect(mockMakeRequest.mock.calls.length).toBe(1); + }); + + it('should reject the promise if response status code is not 200', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(mockErrorResponse(500))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toMatchObject( + new OptimizelyError('CMAB_FETCH_FAILED', 500), + ); + }); + + it('should reject the promise if api response is not valid', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(Promise.resolve({ + statusCode: 200, + body: JSON.stringify({ + predictions: [], + }), + headers: {}, + }))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toMatchObject( + new OptimizelyError('INVALID_CMAB_RESPONSE'), + ); + }); + + it('should reject the promise if requestHandler.makeRequest rejects', async () => { + const requestHandler = getMockRequestHandler(); + + const mockMakeRequest: MockInstance = requestHandler.makeRequest; + mockMakeRequest.mockReturnValue(getMockAbortableRequest(Promise.reject('error'))); + + const cmabClient = new DefaultCmabClient({ + requestHandler, + }); + + const experimentId = '123'; + const userId = 'user123'; + const attributes = { + browser: 'chrome', + isMobile: true, + }; + const cmabUuid = 'uuid123'; + + await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow('error'); + }); +}); diff --git a/lib/core/decision_service/cmab/cmab_client.ts b/lib/core/decision_service/cmab/cmab_client.ts index 5011f100c..5ad726385 100644 --- a/lib/core/decision_service/cmab/cmab_client.ts +++ b/lib/core/decision_service/cmab/cmab_client.ts @@ -29,7 +29,7 @@ export interface CmabClient { experimentId: string, userId: string, attributes: UserAttributes, - cmabUuid: string + cmabUuid: string, ): Promise } @@ -58,7 +58,7 @@ export class DefaultCmabClient implements CmabClient { experimentId: string, userId: string, attributes: UserAttributes, - cmabUuid: string + cmabUuid: string, ): Promise { const url = sprintf(CMAB_PREDICTION_ENDPOINT, experimentId); @@ -98,7 +98,7 @@ export class DefaultCmabClient implements CmabClient { data, ).responsePromise; - if (isSuccessStatusCode(response.statusCode)) { + if (!isSuccessStatusCode(response.statusCode)) { return Promise.reject(new OptimizelyError(CMAB_FETCH_FAILED, response.statusCode)); } From f4e3a1fc0fc3d7163088eba10fc8cd66eeb68c2b Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 27 Feb 2025 05:44:55 +0600 Subject: [PATCH 3/4] copyright --- .../decision_service/cmab/cmab_client.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lib/core/decision_service/cmab/cmab_client.spec.ts b/lib/core/decision_service/cmab/cmab_client.spec.ts index 4f536639a..1145f571c 100644 --- a/lib/core/decision_service/cmab/cmab_client.spec.ts +++ b/lib/core/decision_service/cmab/cmab_client.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2025, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { describe, it, expect, vi, Mocked, Mock, MockInstance, beforeEach, afterEach } from 'vitest'; import { DefaultCmabClient } from './cmab_client'; From dcd6d16668618467687f0976f3be3d42e513a474 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 6 Mar 2025 00:21:35 +0600 Subject: [PATCH 4/4] fix review comments --- .../decision_service/cmab/cmab_client.spec.ts | 48 +++++++++---------- lib/core/decision_service/cmab/cmab_client.ts | 12 ++--- lib/message/error_message.ts | 2 +- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/lib/core/decision_service/cmab/cmab_client.spec.ts b/lib/core/decision_service/cmab/cmab_client.spec.ts index 1145f571c..04c7246ca 100644 --- a/lib/core/decision_service/cmab/cmab_client.spec.ts +++ b/lib/core/decision_service/cmab/cmab_client.spec.ts @@ -43,13 +43,13 @@ const mockErrorResponse = (statusCode: number) => Promise.resolve({ const assertRequest = ( call: number, mockRequestHandler: MockInstance, - experimentId: string, + ruleId: string, userId: string, attributes: Record, cmabUuid: string, ) => { const [requestUrl, headers, method, data] = mockRequestHandler.mock.calls[call]; - expect(requestUrl).toBe(`https://prediction.cmab.optimizely.com/predict/${experimentId}`); + expect(requestUrl).toBe(`https://prediction.cmab.optimizely.com/predict/${ruleId}`); expect(method).toBe('POST'); expect(headers).toEqual({ 'Content-Type': 'application/json', @@ -59,7 +59,7 @@ const assertRequest = ( expect(parsedData.instances).toEqual([ { visitorId: userId, - experimentId, + experimentId: ruleId, attributes: Object.keys(attributes).map((key) => ({ id: key, value: attributes[key], @@ -81,7 +81,7 @@ describe('DefaultCmabClient', () => { requestHandler, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -89,10 +89,10 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - const variation = await cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid); + const variation = await cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); expect(variation).toBe('var123'); - assertRequest(0, mockMakeRequest, experimentId, userId, attributes, cmabUuid); + assertRequest(0, mockMakeRequest, ruleId, userId, attributes, cmabUuid); }); it('should retry fetch if retryConfig is provided', async () => { @@ -110,7 +110,7 @@ describe('DefaultCmabClient', () => { }, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -118,12 +118,12 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - const variation = await cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid); + const variation = await cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); expect(variation).toBe('var123'); expect(mockMakeRequest.mock.calls.length).toBe(3); for(let i = 0; i < 3; i++) { - assertRequest(i, mockMakeRequest, experimentId, userId, attributes, cmabUuid); + assertRequest(i, mockMakeRequest, ruleId, userId, attributes, cmabUuid); } }); @@ -157,7 +157,7 @@ describe('DefaultCmabClient', () => { }, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -165,7 +165,7 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - const fetchPromise = cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid); + const fetchPromise = cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid); await exhaustMicrotasks(); expect(mockMakeRequest.mock.calls.length).toBe(1); @@ -205,7 +205,7 @@ describe('DefaultCmabClient', () => { expect(variation).toBe('var123'); expect(mockMakeRequest.mock.calls.length).toBe(4); for(let i = 0; i < 4; i++) { - assertRequest(i, mockMakeRequest, experimentId, userId, attributes, cmabUuid); + assertRequest(i, mockMakeRequest, ruleId, userId, attributes, cmabUuid); } vi.useRealTimers(); }); @@ -223,7 +223,7 @@ describe('DefaultCmabClient', () => { }, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -231,7 +231,7 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow(); + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow(); expect(mockMakeRequest.mock.calls.length).toBe(6); }); @@ -248,7 +248,7 @@ describe('DefaultCmabClient', () => { }, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -256,7 +256,7 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow(); + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow(); expect(mockMakeRequest.mock.calls.length).toBe(6); }); @@ -270,7 +270,7 @@ describe('DefaultCmabClient', () => { requestHandler, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -278,7 +278,7 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow(); + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow(); expect(mockMakeRequest.mock.calls.length).toBe(1); }); @@ -292,7 +292,7 @@ describe('DefaultCmabClient', () => { requestHandler, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -300,7 +300,7 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toMatchObject( + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toMatchObject( new OptimizelyError('CMAB_FETCH_FAILED', 500), ); }); @@ -321,7 +321,7 @@ describe('DefaultCmabClient', () => { requestHandler, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -329,7 +329,7 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toMatchObject( + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toMatchObject( new OptimizelyError('INVALID_CMAB_RESPONSE'), ); }); @@ -344,7 +344,7 @@ describe('DefaultCmabClient', () => { requestHandler, }); - const experimentId = '123'; + const ruleId = '123'; const userId = 'user123'; const attributes = { browser: 'chrome', @@ -352,6 +352,6 @@ describe('DefaultCmabClient', () => { }; const cmabUuid = 'uuid123'; - await expect(cmabClient.fetchVariation(experimentId, userId, attributes, cmabUuid)).rejects.toThrow('error'); + await expect(cmabClient.fetchDecision(ruleId, userId, attributes, cmabUuid)).rejects.toThrow('error'); }); }); diff --git a/lib/core/decision_service/cmab/cmab_client.ts b/lib/core/decision_service/cmab/cmab_client.ts index 5ad726385..efe3a72ed 100644 --- a/lib/core/decision_service/cmab/cmab_client.ts +++ b/lib/core/decision_service/cmab/cmab_client.ts @@ -25,8 +25,8 @@ import { BackoffController } from "../../../utils/repeater/repeater"; import { Producer } from "../../../utils/type"; export interface CmabClient { - fetchVariation( - experimentId: string, + fetchDecision( + ruleId: string, userId: string, attributes: UserAttributes, cmabUuid: string, @@ -54,13 +54,13 @@ export class DefaultCmabClient implements CmabClient { this.retryConfig = config.retryConfig; } - async fetchVariation( - experimentId: string, + async fetchDecision( + ruleId: string, userId: string, attributes: UserAttributes, cmabUuid: string, ): Promise { - const url = sprintf(CMAB_PREDICTION_ENDPOINT, experimentId); + const url = sprintf(CMAB_PREDICTION_ENDPOINT, ruleId); const cmabAttributes = Object.keys(attributes).map((key) => ({ id: key, @@ -72,7 +72,7 @@ export class DefaultCmabClient implements CmabClient { instances: [ { visitorId: userId, - experimentId, + experimentId: ruleId, attributes: cmabAttributes, cmabUUID: cmabUuid, } diff --git a/lib/message/error_message.ts b/lib/message/error_message.ts index b0b5247ae..e6a2260a3 100644 --- a/lib/message/error_message.ts +++ b/lib/message/error_message.ts @@ -106,7 +106,7 @@ export const ODP_EVENT_MANAGER_STOPPED = "ODP event manager stopped before it co export const DATAFILE_MANAGER_FAILED_TO_START = 'Datafile manager failed to start'; export const UNABLE_TO_ATTACH_UNLOAD = 'unable to bind optimizely.close() to page unload event: "%s"'; export const UNABLE_TO_PARSE_AND_SKIPPED_HEADER = 'Unable to parse & skipped header item'; -export const CMAB_FETCH_FAILED = 'CMAB variation fetch failed with status: %s'; +export const CMAB_FETCH_FAILED = 'CMAB decision fetch failed with status: %s'; export const INVALID_CMAB_FETCH_RESPONSE = 'Invalid CMAB fetch response'; export const messages: string[] = [];