From 9667701d0a3ef899d456cd9b51a416081168c9e4 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Mon, 18 Nov 2024 22:37:49 +0600 Subject: [PATCH 1/5] [FSSDK-10882] ssr support addition --- lib/optimizely/index.ts | 1 + lib/project_config/project_config_manager.ts | 16 ++++++++++++++++ lib/shared_types.ts | 1 + lib/tests/mock/mock_project_config_manager.ts | 1 + 4 files changed, 19 insertions(+) diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index c78154311..000559126 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -146,6 +146,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.ts b/lib/project_config/project_config_manager.ts index c03ee9b4c..b3921ebb6 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; @@ -54,6 +55,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf public datafileManager?: DatafileManager; private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter(); private logger?: LoggerFacade; + private isSsr?: boolean; constructor(config: ProjectConfigManagerConfig) { super(); @@ -79,6 +81,11 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf return; } + if(this.isSsr) { + // If isSsr is true, we don't need to poll for datafile updates + this.datafileManager = undefined + } + if (this.datafile) { this.handleNewDatafile(this.datafile, true); } @@ -216,4 +223,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 8902820eb..3caf1a0fd 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -291,6 +291,7 @@ export interface OptimizelyOptions { sdkKey?: string; userProfileService?: UserProfileService | null; defaultDecideOptions?: OptimizelyDecideOption[]; + isSsr?:boolean; odpManager?: IOdpManager; notificationCenter: NotificationCenterImpl; } diff --git a/lib/tests/mock/mock_project_config_manager.ts b/lib/tests/mock/mock_project_config_manager.ts index af7a8ba84..505731dd0 100644 --- a/lib/tests/mock/mock_project_config_manager.ts +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -28,6 +28,7 @@ export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigMan return { config: opt.initConfig, start: () => {}, + setSsr: () => {}, onRunning: () => opt.onRunning || Promise.resolve(), stop: () => {}, onTerminated: () => opt.onTerminated || Promise.resolve(), From e1bdc8b287b90634acd8028b4f71e68468fa26a9 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:52:49 +0600 Subject: [PATCH 2/5] [FSSDK-10882] test addition --- lib/index.node.tests.js | 17 +++++++++++++++++ .../project_config_manager.spec.ts | 11 +++++++++++ lib/shared_types.ts | 1 + lib/tests/mock/mock_project_config_manager.ts | 4 +++- 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 8ff0edeff..7684aa71b 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -24,6 +24,7 @@ import * as loggerPlugin from './plugins/logger'; import optimizelyFactory from './index.node'; import configValidator from './utils/config_validator'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; +import { createProjectConfig } from './project_config/project_config'; describe('optimizelyFactory', function() { describe('APIs', function() { @@ -88,6 +89,22 @@ describe('optimizelyFactory', function() { assert.equal(optlyInstance.clientVersion, '5.3.4'); }); + it('should create an instance of optimizely with ssr flag, project config must be available', () => { + const optlyInstance = optimizelyFactory.createInstance({ + projectConfigManager: getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + logger: fakeLogger, + isSsr: true, + }); + + assert.instanceOf(optlyInstance, Optimizely); + assert.equal(optlyInstance.projectConfigManager.isSsr, true); + assert.deepEqual(optlyInstance.getProjectConfig(), createProjectConfig(testData.getTestProjectConfig())); + }); + // TODO: user will create and inject an event processor // these tests will be refactored accordingly // describe('event processor configuration', function() { diff --git a/lib/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts index 5a568188d..2d7ebc8ca 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', () => { diff --git a/lib/shared_types.ts b/lib/shared_types.ts index 3caf1a0fd..0560e4f0a 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -426,6 +426,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 505731dd0..c4cb31d8c 100644 --- a/lib/tests/mock/mock_project_config_manager.ts +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -28,7 +28,9 @@ export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigMan return { config: opt.initConfig, start: () => {}, - setSsr: () => {}, + setSsr: function(isSsr?:boolean) { + this.isSsr = isSsr; + }, onRunning: () => opt.onRunning || Promise.resolve(), stop: () => {}, onTerminated: () => opt.onTerminated || Promise.resolve(), From c4c98a1622089f85eb355c4c9e28af5971cd9f64 Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:39:50 +0600 Subject: [PATCH 3/5] [FSSDK-10882] improvements --- lib/index.node.tests.js | 18 +---- lib/optimizely/index.spec.ts | 78 +++++++++++++++++++ .../project_config_manager.spec.ts | 10 +++ lib/project_config/project_config_manager.ts | 18 +++-- lib/tests/mock/mock_project_config_manager.ts | 1 + 5 files changed, 101 insertions(+), 24 deletions(-) create mode 100644 lib/optimizely/index.spec.ts diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 7684aa71b..63bb7c60e 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -88,23 +88,7 @@ describe('optimizelyFactory', function() { assert.instanceOf(optlyInstance, Optimizely); assert.equal(optlyInstance.clientVersion, '5.3.4'); }); - - it('should create an instance of optimizely with ssr flag, project config must be available', () => { - const optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfig()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - isSsr: true, - }); - - assert.instanceOf(optlyInstance, Optimizely); - assert.equal(optlyInstance.projectConfigManager.isSsr, true); - assert.deepEqual(optlyInstance.getProjectConfig(), createProjectConfig(testData.getTestProjectConfig())); - }); - + // 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/project_config/project_config_manager.spec.ts b/lib/project_config/project_config_manager.spec.ts index 2d7ebc8ca..967aec83c 100644 --- a/lib/project_config/project_config_manager.spec.ts +++ b/lib/project_config/project_config_manager.spec.ts @@ -409,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 b3921ebb6..d200c2826 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -55,7 +55,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf public datafileManager?: DatafileManager; private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter(); private logger?: LoggerFacade; - private isSsr?: boolean; + private isSsr = true; constructor(config: ProjectConfigManagerConfig) { super(); @@ -75,17 +75,21 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf } this.state = ServiceState.Starting; - if (!this.datafile && !this.datafileManager) { - // TODO: replace message with imported constants - this.handleInitError(new Error('You must provide at least one of sdkKey or datafile')); - return; - } 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(errorMessage)); + return; + } + if (this.datafile) { this.handleNewDatafile(this.datafile, true); } @@ -229,7 +233,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf * @param {Boolean} isSsr * @returns {void} */ - setSsr(isSsr?: boolean): void { + setSsr(isSsr: boolean): void { this.isSsr = isSsr; } } diff --git a/lib/tests/mock/mock_project_config_manager.ts b/lib/tests/mock/mock_project_config_manager.ts index c4cb31d8c..f95733c8b 100644 --- a/lib/tests/mock/mock_project_config_manager.ts +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -26,6 +26,7 @@ type MockOpt = { export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigManager => { return { + isSsr: false, config: opt.initConfig, start: () => {}, setSsr: function(isSsr?:boolean) { From 6873375824831fe52c833ccaf793bc56d08a049d Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:42:24 +0600 Subject: [PATCH 4/5] [FSSDK-10882] improvements --- lib/index.node.tests.js | 2 -- lib/tests/mock/mock_project_config_manager.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 63bb7c60e..b6ef0b8fb 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -24,7 +24,6 @@ import * as loggerPlugin from './plugins/logger'; import optimizelyFactory from './index.node'; import configValidator from './utils/config_validator'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; -import { createProjectConfig } from './project_config/project_config'; describe('optimizelyFactory', function() { describe('APIs', function() { @@ -88,7 +87,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/tests/mock/mock_project_config_manager.ts b/lib/tests/mock/mock_project_config_manager.ts index f95733c8b..b76f71e2d 100644 --- a/lib/tests/mock/mock_project_config_manager.ts +++ b/lib/tests/mock/mock_project_config_manager.ts @@ -29,7 +29,7 @@ export const getMockProjectConfigManager = (opt: MockOpt = {}): ProjectConfigMan isSsr: false, config: opt.initConfig, start: () => {}, - setSsr: function(isSsr?:boolean) { + setSsr: function(isSsr:boolean) { this.isSsr = isSsr; }, onRunning: () => opt.onRunning || Promise.resolve(), From 377731220bafb1d27e6f0235157516162836b48e Mon Sep 17 00:00:00 2001 From: Md Junaed Hossain <169046794+junaed-optimizely@users.noreply.github.com> Date: Thu, 21 Nov 2024 19:45:12 +0600 Subject: [PATCH 5/5] [FSSDK-10882] fix --- lib/project_config/project_config_manager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/project_config/project_config_manager.ts b/lib/project_config/project_config_manager.ts index d200c2826..825f17049 100644 --- a/lib/project_config/project_config_manager.ts +++ b/lib/project_config/project_config_manager.ts @@ -55,7 +55,7 @@ export class ProjectConfigManagerImpl extends BaseService implements ProjectConf public datafileManager?: DatafileManager; private eventEmitter: EventEmitter<{ update: ProjectConfig }> = new EventEmitter(); private logger?: LoggerFacade; - private isSsr = true; + private isSsr = false; constructor(config: ProjectConfigManagerConfig) { super();