diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index aa0f8743e..3495b036b 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -86,7 +86,6 @@ describe('optimizelyFactory', function() { assert.instanceOf(optlyInstance, Optimizely); assert.equal(optlyInstance.clientVersion, '5.3.4'); }); - // TODO: user will create and inject an event processor // these tests will be refactored accordingly // describe('event processor configuration', function() { diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts new file mode 100644 index 000000000..a4b88017f --- /dev/null +++ b/lib/optimizely/index.spec.ts @@ -0,0 +1,78 @@ +/** + * Copyright 2024, 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 } from 'vitest'; +import Optimizely from '.'; +import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; +import * as logger from '../plugins/logger'; +import * as jsonSchemaValidator from '../utils/json_schema_validator'; +import { LOG_LEVEL } from '../common_exports'; +import { createNotificationCenter } from '../core/notification_center'; +import testData from '../tests/test_data'; +import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; +import { LoggerFacade } from '../modules/logging'; +import { createProjectConfig } from '../project_config/project_config'; + +describe('lib/optimizely', () => { + const errorHandler = { handleError: function() {} }; + + const eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; + + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + + const createdLogger: LoggerFacade = { + ...logger.createLogger({ + logLevel: LOG_LEVEL.INFO, + }), + info: () => {}, + debug: () => {}, + warn: () => {}, + error: () => {}, + log: () => {}, + }; + + const notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler }); + + it('should pass ssr to the project config manager', () => { + const projectConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + vi.spyOn(projectConfigManager, 'setSsr'); + + const instance = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + errorHandler, + jsonSchemaValidator, + logger: createdLogger, + notificationCenter, + eventProcessor, + isSsr: true, + isValidInstance: true, + }); + + expect(projectConfigManager.setSsr).toHaveBeenCalledWith(true); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(instance.getProjectConfig()).toBe(projectConfigManager.config); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(projectConfigManager.isSsr).toBe(true); + }); +}); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index f9b29a6b4..a15a7711f 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -144,6 +144,7 @@ export default class Optimizely implements Client { this.updateOdpSettings(); }); + this.projectConfigManager.setSsr(config.isSsr) this.projectConfigManager.start(); const projectConfigManagerRunningPromise = this.projectConfigManager.onRunning(); diff --git a/lib/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts index 5a568188d..967aec83c 100644 --- a/lib/project_config/project_config_manager.spec.ts +++ b/lib/project_config/project_config_manager.spec.ts @@ -165,6 +165,17 @@ describe('ProjectConfigManagerImpl', () => { await manager.onRunning(); expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); }); + + it('should not start datafileManager if isSsr is true and return correct config', () => { + const datafileManager = getMockDatafileManager({}); + vi.spyOn(datafileManager, 'start'); + const manager = new ProjectConfigManagerImpl({ datafile: testData.getTestProjectConfig(), datafileManager }); + manager.setSsr(true); + manager.start(); + + expect(manager.getConfig()).toEqual(createProjectConfig(testData.getTestProjectConfig())); + expect(datafileManager.start).not.toHaveBeenCalled(); + }); }); describe('when datafile is invalid', () => { @@ -398,6 +409,16 @@ describe('ProjectConfigManagerImpl', () => { expect(logger.error).toHaveBeenCalled(); }); + it('should reject onRunning() and log error if isSsr is true and datafile is not provided', async () =>{ + const logger = getMockLogger(); + const manager = new ProjectConfigManagerImpl({ logger, datafileManager: getMockDatafileManager({})}); + manager.setSsr(true); + manager.start(); + + await expect(manager.onRunning()).rejects.toThrow(); + expect(logger.error).toHaveBeenCalled(); + }); + it('should reject onRunning() and log error if the datafile version is not supported', async () => { const logger = getMockLogger(); const datafile = testData.getUnsupportedVersionConfig(); diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts index 94c83902b..46c79238c 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -34,6 +34,7 @@ interface ProjectConfigManagerConfig { export interface ProjectConfigManager extends Service { setLogger(logger: LoggerFacade): void; + setSsr(isSsr?: boolean): void; getConfig(): ProjectConfig | undefined; getOptimizelyConfig(): OptimizelyConfig | undefined; onUpdate(listener: Consumer): Fn; @@ -53,6 +54,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf public jsonSchemaValidator?: Transformer; public datafileManager?: DatafileManager; private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter(); + private isSsr = false; constructor(config: ProjectConfigManagerConfig) { super(); @@ -68,9 +70,18 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf } this.state = ServiceState.Starting; + + if(this.isSsr) { + // If isSsr is true, we don't need to poll for datafile updates + this.datafileManager = undefined + } + if (!this.datafile && !this.datafileManager) { + const errorMessage = this.isSsr + ? 'You must provide datafile in SSR' + : 'You must provide at least one of sdkKey or datafile'; // TODO: replace message with imported constants - this.handleInitError(new Error('You must provide at least one of sdkKey or datafile')); + this.handleInitError(new Error(errorMessage)); return; } @@ -211,4 +222,13 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf this.stopPromise.reject(err); }); } + + /** + * Set the isSsr flag to indicate if the project config manager is being used in a server side rendering environment + * @param {Boolean} isSsr + * @returns {void} + */ + setSsr(isSsr: boolean): void { + this.isSsr = isSsr; + } } diff --git a/lib/shared_types.ts b/lib/shared_types.ts index f27657378..b5249266f 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -292,6 +292,7 @@ export interface OptimizelyOptions { sdkKey?: string; userProfileService?: UserProfileService | null; defaultDecideOptions?: OptimizelyDecideOption[]; + isSsr?:boolean; odpManager?: IOdpManager; notificationCenter: NotificationCenterImpl; } @@ -426,6 +427,7 @@ export interface ConfigLite { defaultDecideOptions?: OptimizelyDecideOption[]; clientEngine?: string; clientVersion?: string; + isSsr?: boolean; } export type OptimizelyExperimentsMap = { diff --git a/lib/tests/mock/mock_project_config_manager.ts b/lib/tests/mock/mock_project_config_manager.ts index af7a8ba84..b76f71e2d 100644 --- a/lib/tests/mock/mock_project_config_manager.ts +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -26,8 +26,12 @@ type MockOpt = { export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigManager => { return { + isSsr: false, config: opt.initConfig, start: () => {}, + setSsr: function(isSsr:boolean) { + this.isSsr = isSsr; + }, onRunning: () => opt.onRunning || Promise.resolve(), stop: () => {}, onTerminated: () => opt.onTerminated || Promise.resolve(),