From 566daaff800e349a8e7dfbb8beeef5cca7b930f4 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 18 Mar 2024 16:49:01 -0400 Subject: [PATCH 01/36] fix: remove successful fetch requirement for onReady --- src/client.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/client.ts b/src/client.ts index 008df57..26a8812 100644 --- a/src/client.ts +++ b/src/client.ts @@ -290,17 +290,10 @@ class OptimizelyReactSDKClient implements ReactSDKClient { }, timeout) as any; }); - return Promise.race([this.dataReadyPromise, timeoutPromise]).then(async res => { + return Promise.race([this.dataReadyPromise, timeoutPromise]).then(res => { clearTimeout(timeoutId); if (res.success && !this.initialConfig.odpOptions?.disabled) { - const isSegmentsFetched = await this.fetchQualifiedSegments(); - if (!isSegmentsFetched) { - return { - success: false, - reason: 'USER_NOT_READY', - message: 'Failed to fetch qualified segments', - }; - } + this.fetchQualifiedSegments(); } return res; }); From 77c4f56f2c54cb379546501e9dd5d8de2158ca4c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 19 Mar 2024 09:37:39 -0400 Subject: [PATCH 02/36] Revert "fix: remove successful fetch requirement for onReady" This reverts commit 566daaff800e349a8e7dfbb8beeef5cca7b930f4. --- src/client.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 26a8812..008df57 100644 --- a/src/client.ts +++ b/src/client.ts @@ -290,10 +290,17 @@ class OptimizelyReactSDKClient implements ReactSDKClient { }, timeout) as any; }); - return Promise.race([this.dataReadyPromise, timeoutPromise]).then(res => { + return Promise.race([this.dataReadyPromise, timeoutPromise]).then(async res => { clearTimeout(timeoutId); if (res.success && !this.initialConfig.odpOptions?.disabled) { - this.fetchQualifiedSegments(); + const isSegmentsFetched = await this.fetchQualifiedSegments(); + if (!isSegmentsFetched) { + return { + success: false, + reason: 'USER_NOT_READY', + message: 'Failed to fetch qualified segments', + }; + } } return res; }); From 0f190d6b106e73969e7ac6a821581afbbb700d52 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 21 Mar 2024 15:52:35 -0400 Subject: [PATCH 03/36] fix: error with OnReadyResult being undefined --- src/hooks.ts | 66 ++++++++++++++++++++++++---------------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index f13bd30..17d9a47 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -1,5 +1,5 @@ /** - * Copyright 2018-2019, 2022-2023, Optimizely + * Copyright 2018-2019, 2022-2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import { useCallback, useContext, useEffect, useState, useRef } from 'react'; import { UserAttributes, OptimizelyDecideOption, getLogger } from '@optimizely/optimizely-sdk'; import { setupAutoUpdateListeners } from './autoUpdate'; -import { ReactSDKClient, VariableValuesObject, OnReadyResult } from './client'; +import { ReactSDKClient, VariableValuesObject, OnReadyResult, DEFAULT_ON_READY_TIMEOUT } from './client'; import { notifier } from './notifier'; import { OptimizelyContext } from './Context'; import { areAttributesEqual, OptimizelyDecision, createFailedDecision } from './utils'; @@ -66,7 +66,7 @@ interface UseExperiment { (experimentKey: string, options?: HookOptions, overrides?: HookOverrides): [ ExperimentDecisionValues['variation'], ClientReady, - DidTimeout, + DidTimeout ]; } @@ -75,7 +75,7 @@ interface UseFeature { FeatureDecisionValues['isEnabled'], FeatureDecisionValues['variables'], ClientReady, - DidTimeout, + DidTimeout ]; } @@ -83,7 +83,7 @@ interface UseDecision { (featureKey: string, options?: DecideHooksOptions, overrides?: HookOverrides): [ OptimizelyDecision, ClientReady, - DidTimeout, + DidTimeout ]; } @@ -123,6 +123,8 @@ function subscribeToInitialization( timeout: number | undefined, onInitStateChange: (initState: InitializationState) => void ): void { + timeout = timeout || DEFAULT_ON_READY_TIMEOUT; + optimizely .onReady({ timeout }) .then((res: OnReadyResult) => { @@ -143,7 +145,7 @@ function subscribeToInitialization( clientReady: false, didTimeout: false, }); - res.dataReadyPromise!.then(() => { + res?.dataReadyPromise?.then(() => { hooksLogger.info('Client became ready.'); onInitStateChange({ clientReady: true, @@ -159,7 +161,7 @@ function subscribeToInitialization( clientReady: false, didTimeout: true, }); - res.dataReadyPromise!.then(() => { + res?.dataReadyPromise?.then(() => { hooksLogger.info('Client became ready after timeout already elapsed'); onInitStateChange({ clientReady: true, @@ -192,7 +194,9 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext); if (!optimizely) { - hooksLogger.error(`Unable to use experiment ${experimentKey}. optimizely prop must be supplied via a parent `); + hooksLogger.error( + `Unable to use experiment ${experimentKey}. optimizely prop must be supplied via a parent ` + ); return [null, false, false]; } @@ -259,7 +263,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri })); }); } - return (): void => { }; + return (): void => {}; }, [optimizely.getIsReadyPromiseFulfilled(), options.autoUpdate, optimizely, experimentKey, getCurrentDecision]); useEffect( @@ -287,13 +291,10 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext); if (!optimizely) { - hooksLogger.error(`Unable to properly use feature ${featureKey}. optimizely prop must be supplied via a parent `); - return [ - false, - {}, - false, - false, - ]; + hooksLogger.error( + `Unable to properly use feature ${featureKey}. optimizely prop must be supplied via a parent ` + ); + return [false, {}, false, false]; } const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes); @@ -359,15 +360,10 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) })); }); } - return (): void => { }; + return (): void => {}; }, [optimizely.getIsReadyPromiseFulfilled(), options.autoUpdate, optimizely, featureKey, getCurrentDecision]); - return [ - state.isEnabled, - state.variables, - state.clientReady, - state.didTimeout, - ]; + return [state.isEnabled, state.variables, state.clientReady, state.didTimeout]; }; /** @@ -381,7 +377,9 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) const { optimizely, isServerSide, timeout } = useContext(OptimizelyContext); if (!optimizely) { - hooksLogger.error(`Unable to use decision ${flagKey}. optimizely prop must be supplied via a parent `); + hooksLogger.error( + `Unable to use decision ${flagKey}. optimizely prop must be supplied via a parent ` + ); return [ createFailedDecision(flagKey, 'Optimizely SDK not configured properly yet.', { id: null, @@ -389,7 +387,7 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) }), false, false, - ] + ]; } const overrideAttrs = useCompareAttrsMemoize(overrides.overrideAttributes); @@ -403,11 +401,11 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) const decisionState = isClientReady ? getCurrentDecision() : { - decision: createFailedDecision(flagKey, 'Optimizely SDK not configured properly yet.', { - id: overrides.overrideUserId || null, - attributes: overrideAttrs, - }), - }; + decision: createFailedDecision(flagKey, 'Optimizely SDK not configured properly yet.', { + id: overrides.overrideUserId || null, + attributes: overrideAttrs, + }), + }; return { ...decisionState, clientReady: isClientReady, @@ -473,12 +471,8 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) })); }); } - return (): void => { }; + return (): void => {}; }, [optimizely.getIsReadyPromiseFulfilled(), options.autoUpdate, optimizely, flagKey, getCurrentDecision]); - return [ - state.decision, - state.clientReady, - state.didTimeout, - ]; + return [state.decision, state.clientReady, state.didTimeout]; }; From 2fc9703b4af0c99e1684b39e83c6aa72915db60d Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 21 Mar 2024 15:53:15 -0400 Subject: [PATCH 04/36] fix: setUser should use VUID if possible --- src/client.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 008df57..120e9cd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -46,7 +46,7 @@ const REACT_SDK_CLIENT_ENGINE = 'react-sdk'; const REACT_SDK_CLIENT_VERSION = '3.0.1'; export const DefaultUser: UserInfo = { - id: null, + id: null, // null signals JS SDK to try to use VUID attributes: {}, }; @@ -351,10 +351,11 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public async setUser(userInfo: UserInfo): Promise { + // The JS SDK will try to assign VUID if the user ID is null this.setCurrentUserContext(userInfo); this.user = { - id: userInfo.id || DefaultUser.id, + id: userInfo.id || this.getUserContext()?.getUserId() || DefaultUser.id, attributes: userInfo.attributes || DefaultUser.attributes, }; From 682e8a70d8160902fe18aef25b324f779cdeeebb Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 21 Mar 2024 16:30:00 -0400 Subject: [PATCH 05/36] revert: timeout of undefined --- src/hooks.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index 17d9a47..0f75e86 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -18,7 +18,7 @@ import { useCallback, useContext, useEffect, useState, useRef } from 'react'; import { UserAttributes, OptimizelyDecideOption, getLogger } from '@optimizely/optimizely-sdk'; import { setupAutoUpdateListeners } from './autoUpdate'; -import { ReactSDKClient, VariableValuesObject, OnReadyResult, DEFAULT_ON_READY_TIMEOUT } from './client'; +import { ReactSDKClient, VariableValuesObject, OnReadyResult } from './client'; import { notifier } from './notifier'; import { OptimizelyContext } from './Context'; import { areAttributesEqual, OptimizelyDecision, createFailedDecision } from './utils'; @@ -123,8 +123,6 @@ function subscribeToInitialization( timeout: number | undefined, onInitStateChange: (initState: InitializationState) => void ): void { - timeout = timeout || DEFAULT_ON_READY_TIMEOUT; - optimizely .onReady({ timeout }) .then((res: OnReadyResult) => { From bbea7cae2c5baef7e289df4608ed0885d6404a01 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 21 Mar 2024 16:30:15 -0400 Subject: [PATCH 06/36] docs: update copyright year --- src/Provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index 17b7fb3..7ef16ad 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From 21348c1f74fdf1848a5f1540b2f435f2684edbae Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 22 Mar 2024 14:36:17 -0400 Subject: [PATCH 07/36] revert: Provider.tsx copyright since no code change --- src/Provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index 7ef16ad..17b7fb3 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2022-2024, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From cb157202115b29eae192ffc2bcd6b8c3ba3dfa03 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 25 Mar 2024 09:54:38 -0400 Subject: [PATCH 08/36] build: bump JS SDK version --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8d55dd1..35bb050 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "access": "public" }, "dependencies": { - "@optimizely/optimizely-sdk": "^5.2.0", + "@optimizely/optimizely-sdk": "^5.2.1", "hoist-non-react-statics": "^3.3.0", "prop-types": "^15.6.2", "utility-types": "^2.1.0 || ^3.0.0" diff --git a/yarn.lock b/yarn.lock index 8f314f7..7fc16ee 100644 --- a/yarn.lock +++ b/yarn.lock @@ -655,10 +655,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@optimizely/optimizely-sdk@^5.2.0": - version "5.2.0" - resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-5.2.0.tgz#6449336182515dd4cca9d6913e24ca8e814e6278" - integrity sha512-JxdhWHU9OiJKrKfyWe9lL+2/e28gBZnPNHQAA+heyWTAtcuxZWdSZ24bIkLwAAuIf26dh89o9LNB2L8aKPmwWw== +"@optimizely/optimizely-sdk@^5.2.1": + version "5.2.1" + resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-5.2.1.tgz#b42b6d5b39a27b81432918b87a7984584bf9c5fc" + integrity sha512-B6JugOJ6+H0GhXcSQCb9yRDU3S8ogXQxfKN5Dl3S1YfyrXmvCU7Y1J9PvV6/PBz9itXhcvnMfje6iyMCt57wGg== dependencies: decompress-response "^4.2.1" json-schema "^0.4.0" From fd5cfd1d723d3e18a9e0d6224f006cfa8196b4c5 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 25 Mar 2024 15:38:30 -0400 Subject: [PATCH 09/36] refactor: `res` should never be `undefined` --- src/hooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/hooks.ts b/src/hooks.ts index 0f75e86..a5b7115 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -143,7 +143,7 @@ function subscribeToInitialization( clientReady: false, didTimeout: false, }); - res?.dataReadyPromise?.then(() => { + res.dataReadyPromise?.then(() => { hooksLogger.info('Client became ready.'); onInitStateChange({ clientReady: true, @@ -159,7 +159,7 @@ function subscribeToInitialization( clientReady: false, didTimeout: true, }); - res?.dataReadyPromise?.then(() => { + res.dataReadyPromise?.then(() => { hooksLogger.info('Client became ready after timeout already elapsed'); onInitStateChange({ clientReady: true, From ae88a38c3398ba03ff7ddef836c3005fcf17c72f Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 25 Mar 2024 15:45:41 -0400 Subject: [PATCH 10/36] docs: add clarifying comment --- src/hooks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/hooks.ts b/src/hooks.ts index a5b7115..0396e9b 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -159,6 +159,7 @@ function subscribeToInitialization( clientReady: false, didTimeout: true, }); + // dataReadyPromise? is optional in the OnReadyResult interface in client.ts res.dataReadyPromise?.then(() => { hooksLogger.info('Client became ready after timeout already elapsed'); onInitStateChange({ From 1c1c501f26d7e7042343b1b5fa4aa0dbe596dd9e Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 27 Mar 2024 16:07:32 -0400 Subject: [PATCH 11/36] revert: retrieval & use of current user context --- src/client.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 120e9cd..008df57 100644 --- a/src/client.ts +++ b/src/client.ts @@ -46,7 +46,7 @@ const REACT_SDK_CLIENT_ENGINE = 'react-sdk'; const REACT_SDK_CLIENT_VERSION = '3.0.1'; export const DefaultUser: UserInfo = { - id: null, // null signals JS SDK to try to use VUID + id: null, attributes: {}, }; @@ -351,11 +351,10 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public async setUser(userInfo: UserInfo): Promise { - // The JS SDK will try to assign VUID if the user ID is null this.setCurrentUserContext(userInfo); this.user = { - id: userInfo.id || this.getUserContext()?.getUserId() || DefaultUser.id, + id: userInfo.id || DefaultUser.id, attributes: userInfo.attributes || DefaultUser.attributes, }; From 92f09fb804d5d6d73f78d5eccdb7f79efbcd7308 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Thu, 28 Mar 2024 17:05:37 -0400 Subject: [PATCH 12/36] wip: partial solution; needs collab --- src/Provider.tsx | 1 + src/client.ts | 90 ++++++++++++++++++++++++++++++++---------------- src/hooks.ts | 30 +++++++++++----- yarn.lock | 6 ++-- 4 files changed, 86 insertions(+), 41 deletions(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index 17b7fb3..17be848 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -81,6 +81,7 @@ export class OptimizelyProvider extends React.Component void; type OnForcedVariationsUpdateHandler = () => void; -type NotReadyReason = 'TIMEOUT' | 'NO_CLIENT' | 'USER_NOT_READY'; +export enum NotReadyReason { + TIMEOUT = 'TIMEOUT', + NO_CLIENT = 'NO_CLIENT', + USER_NOT_READY = 'USER_NOT_READY', +} type ResolveResult = { success: boolean; @@ -192,6 +196,12 @@ export const DEFAULT_ON_READY_TIMEOUT = 5_000; class OptimizelyReactSDKClient implements ReactSDKClient { private userContext: optimizely.OptimizelyUserContext | null = null; private onUserUpdateHandlers: OnUserUpdateHandler[] = []; + private userPromiseResolver: (resolveResult: ResolveResult) => void; + private isUserPromiseResolved = false; + // Its usually true from the beginning when user is provided as an object in the `OptimizelyProvider` + // This becomes more significant when a promise is provided instead. + private isUserReady = false; + private onForcedVariationsUpdateHandlers: OnForcedVariationsUpdateHandler[] = []; private forcedDecisionFlagKeys: Set = new Set(); @@ -199,14 +209,14 @@ class OptimizelyReactSDKClient implements ReactSDKClient { private isClientReady = false; // We need to add autoupdate listener to the hooks after the instance became fully ready to avoid redundant updates to hooks - private isReadyPromiseFulfilled = false; + private clientAndUserReadyPromiseFulfilled = false; private isUsingSdkKey = false; private readonly _client: optimizely.Client | null; // promise keeping track of async requests for initializing client instance - private dataReadyPromise: Promise; + private clientAndUserReadyPromise: Promise; public initialConfig: optimizely.Config; public user: UserInfo = { ...DefaultUser }; @@ -217,36 +227,45 @@ class OptimizelyReactSDKClient implements ReactSDKClient { */ constructor(config: optimizely.Config) { this.initialConfig = config; - const configWithClientInfo = { ...config, clientEngine: REACT_SDK_CLIENT_ENGINE, clientVersion: REACT_SDK_CLIENT_VERSION, }; + this.userPromiseResolver = () => {}; + const userReadyPromise = new Promise(resolve => { + this.userPromiseResolver = resolve; + }); + this._client = optimizely.createInstance(configWithClientInfo); this.isClientReady = !!configWithClientInfo.datafile; this.isUsingSdkKey = !!configWithClientInfo.sdkKey; if (this._client) { - this.dataReadyPromise = this._client.onReady().then((clientResult: { success: boolean }) => { - this.isReadyPromiseFulfilled = true; - this.isClientReady = true; + const clientReadyPromise = this._client.onReady(); + this.clientAndUserReadyPromise = Promise.all([userReadyPromise, clientReadyPromise]).then( + ([userResult, clientResult]) => { + console.log('||| results', userResult, clientResult); + this.isClientReady = clientResult.success; + this.isUserReady = userResult.success; - return { - success: true, - message: clientResult.success - ? 'Successfully resolved client datafile.' - : 'Client datafile was not not ready.', - }; - }); + // Client and user can become ready synchronously and/or asynchronously. This flag specifically indicates that they became ready asynchronously. + this.clientAndUserReadyPromiseFulfilled = true; + + return { + success: true, // needs to always be true + message: this.isReady() ? 'Client and user are both.' : 'Client or user did not become ready.', + }; + } + ); } else { logger.warn('Unable to resolve datafile and user information because Optimizely client failed to initialize.'); - this.dataReadyPromise = new Promise(resolve => { + this.clientAndUserReadyPromise = new Promise(resolve => { resolve({ success: false, - reason: 'NO_CLIENT', + reason: NotReadyReason.NO_CLIENT, message: 'Optimizely client failed to initialize.', }); }); @@ -265,39 +284,42 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public getIsReadyPromiseFulfilled(): boolean { - return this.isReadyPromiseFulfilled; + return this.clientAndUserReadyPromiseFulfilled; } public getIsUsingSdkKey(): boolean { return this.isUsingSdkKey; } + private get odpExplicitlyOff() { + return this.initialConfig.odpOptions?.disabled; + } + public onReady(config: { timeout?: number } = {}): Promise { let timeoutId: number | undefined; let timeout: number = DEFAULT_ON_READY_TIMEOUT; if (config && config.timeout !== undefined) { timeout = config.timeout; } - const timeoutPromise = new Promise(resolve => { timeoutId = setTimeout(() => { resolve({ success: false, - reason: 'TIMEOUT', - message: 'Failed to initialize onReady before timeout, data was not set before the timeout period', - dataReadyPromise: this.dataReadyPromise, + reason: NotReadyReason.TIMEOUT, + message: 'Failed to initialize before timeout', + dataReadyPromise: this.clientAndUserReadyPromise, }); }, timeout) as any; }); - return Promise.race([this.dataReadyPromise, timeoutPromise]).then(async res => { + return Promise.race([this.clientAndUserReadyPromise, timeoutPromise]).then(async res => { clearTimeout(timeoutId); - if (res.success && !this.initialConfig.odpOptions?.disabled) { + if (res.success && !this.odpExplicitlyOff) { const isSegmentsFetched = await this.fetchQualifiedSegments(); if (!isSegmentsFetched) { return { success: false, - reason: 'USER_NOT_READY', + reason: NotReadyReason.USER_NOT_READY, message: 'Failed to fetch qualified segments', }; } @@ -328,6 +350,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { if (!this.userContext || (this.userContext && !areUsersEqual(userInfo, this.user))) { this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); + console.log('||| user context now', this.userContext); } } @@ -343,7 +366,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public async fetchQualifiedSegments(options?: optimizely.OptimizelySegmentOption[]): Promise { - if (!this.userContext || !this.isReady()) { + if (!this.userContext || !this.isReady() || this.odpExplicitlyOff || !this.getIsReadyPromiseFulfilled()) { return false; } @@ -354,13 +377,19 @@ class OptimizelyReactSDKClient implements ReactSDKClient { this.setCurrentUserContext(userInfo); this.user = { - id: userInfo.id || DefaultUser.id, + id: userInfo.id || this.getUserContext()?.getUserId() || DefaultUser.id, attributes: userInfo.attributes || DefaultUser.attributes, }; - if (this.getIsReadyPromiseFulfilled()) { - await this.fetchQualifiedSegments(); - } + // Set user can occur before the client is ready or later if the user is updated. + await this.fetchQualifiedSegments(); + + this.isUserReady = true; + + if (!this.isUserPromiseResolved) { + this.userPromiseResolver({ success: true }); + this.isUserPromiseResolved = true; + } this.onUserUpdateHandlers.forEach(handler => handler(this.user)); } @@ -394,7 +423,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public isReady(): boolean { - return this.isClientReady; + // React SDK Instance only becomes ready when both JS SDK client and the user info are ready. + return this.isUserReady && this.isClientReady; } /** diff --git a/src/hooks.ts b/src/hooks.ts index 0396e9b..2583115 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -18,7 +18,7 @@ import { useCallback, useContext, useEffect, useState, useRef } from 'react'; import { UserAttributes, OptimizelyDecideOption, getLogger } from '@optimizely/optimizely-sdk'; import { setupAutoUpdateListeners } from './autoUpdate'; -import { ReactSDKClient, VariableValuesObject, OnReadyResult } from './client'; +import { ReactSDKClient, VariableValuesObject, OnReadyResult, NotReadyReason } from './client'; import { notifier } from './notifier'; import { OptimizelyContext } from './Context'; import { areAttributesEqual, OptimizelyDecision, createFailedDecision } from './utils'; @@ -127,7 +127,7 @@ function subscribeToInitialization( .onReady({ timeout }) .then((res: OnReadyResult) => { if (res.success) { - hooksLogger.info('Client became ready'); + hooksLogger.info('Client immediately ready'); onInitStateChange({ clientReady: true, didTimeout: false, @@ -137,7 +137,7 @@ function subscribeToInitialization( switch (res.reason) { // Optimizely client failed to initialize. - case 'NO_CLIENT': + case NotReadyReason.NO_CLIENT: hooksLogger.warn(`Client not ready, reason="${res.message}"`); onInitStateChange({ clientReady: false, @@ -151,15 +151,26 @@ function subscribeToInitialization( }); }); break; - // Assume timeout for all other cases. - // TODO: Other reasons may fall into this case - need to update later to specify 'TIMEOUT' case and general fallback case. - default: - hooksLogger.info(`Client did not become ready before timeout of ${timeout}ms, reason="${res.message}"`); + case NotReadyReason.USER_NOT_READY: + hooksLogger.warn(`User was not ready, reason="${res.message}"`); + onInitStateChange({ + clientReady: false, + didTimeout: false, + }); + res.dataReadyPromise?.then(() => { + hooksLogger.info('User became ready later.'); + onInitStateChange({ + clientReady: true, + didTimeout: false, + }); + }); + break; + case NotReadyReason.TIMEOUT: + hooksLogger.info(`Client did not become ready before timeout of ${timeout} ms, reason="${res.message}"`); onInitStateChange({ clientReady: false, didTimeout: true, }); - // dataReadyPromise? is optional in the OnReadyResult interface in client.ts res.dataReadyPromise?.then(() => { hooksLogger.info('Client became ready after timeout already elapsed'); onInitStateChange({ @@ -167,6 +178,9 @@ function subscribeToInitialization( didTimeout: true, }); }); + break; + default: + hooksLogger.warn(`Other reason client not ready, reason="${res.message}"`); } }) .catch(() => { diff --git a/yarn.lock b/yarn.lock index 7fc16ee..433febf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1624,7 +1624,7 @@ decode-uri-component@^0.2.0: decompress-response@^4.2.1: version "4.2.1" - resolved "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz" + resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-4.2.1.tgz#414023cc7a302da25ce2ec82d0d5238ccafd8986" integrity sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw== dependencies: mimic-response "^2.0.0" @@ -3311,7 +3311,7 @@ json-schema-traverse@^0.4.1: json-schema@^0.4.0: version "0.4.0" - resolved "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== json-stable-stringify-without-jsonify@^1.0.1: @@ -3511,7 +3511,7 @@ mimic-fn@^2.1.0: mimic-response@^2.0.0: version "2.1.0" - resolved "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz" + resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-2.1.0.tgz#d13763d35f613d09ec37ebb30bac0469c0ee8f43" integrity sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA== min-indent@^1.0.0: From 6bc5c45aa77ddcf5125832a19a830f77e8c9728d Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 29 Mar 2024 17:08:37 -0400 Subject: [PATCH 13/36] refactor: setUser logic updated --- src/client.ts | 42 +++++++++++++++++++++++------------------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/src/client.ts b/src/client.ts index 21d3796..a5c1c3e 100644 --- a/src/client.ts +++ b/src/client.ts @@ -244,9 +244,9 @@ class OptimizelyReactSDKClient implements ReactSDKClient { if (this._client) { const clientReadyPromise = this._client.onReady(); + this.clientAndUserReadyPromise = Promise.all([userReadyPromise, clientReadyPromise]).then( ([userResult, clientResult]) => { - console.log('||| results', userResult, clientResult); this.isClientReady = clientResult.success; this.isUserReady = userResult.success; @@ -314,16 +314,6 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return Promise.race([this.clientAndUserReadyPromise, timeoutPromise]).then(async res => { clearTimeout(timeoutId); - if (res.success && !this.odpExplicitlyOff) { - const isSegmentsFetched = await this.fetchQualifiedSegments(); - if (!isSegmentsFetched) { - return { - success: false, - reason: NotReadyReason.USER_NOT_READY, - message: 'Failed to fetch qualified segments', - }; - } - } return res; }); } @@ -350,7 +340,6 @@ class OptimizelyReactSDKClient implements ReactSDKClient { if (!this.userContext || (this.userContext && !areUsersEqual(userInfo, this.user))) { this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); - console.log('||| user context now', this.userContext); } } @@ -374,22 +363,37 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public async setUser(userInfo: UserInfo): Promise { - this.setCurrentUserContext(userInfo); - this.user = { - id: userInfo.id || this.getUserContext()?.getUserId() || DefaultUser.id, + id: userInfo.id || DefaultUser.id, attributes: userInfo.attributes || DefaultUser.attributes, }; - // Set user can occur before the client is ready or later if the user is updated. - await this.fetchQualifiedSegments(); + // if user is anonymous... + if (userInfo.id === DefaultUser.id) { + // wait for the SDK client to be ready before + await this._client?.onReady(); + // setting the user context + this.setCurrentUserContext(userInfo); - this.isUserReady = true; + this.user.id = this.userContext?.getUserId() || DefaultUser.id; + + this.fetchQualifiedSegments(); + } else { // otherwise if we have the user info, we can... + // create the user context + this.setCurrentUserContext(userInfo); + + this.user.id = this.userContext?.getUserId() || DefaultUser.id; + + // but we still have to wait for the client SDK to be ready + await this._client?.onReady(); + // before we can fetch segments + this.fetchQualifiedSegments(); + } if (!this.isUserPromiseResolved) { this.userPromiseResolver({ success: true }); this.isUserPromiseResolved = true; - } + } this.onUserUpdateHandlers.forEach(handler => handler(this.user)); } From 8e9c122ce135ce3877d520737892121f0de23554 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 29 Mar 2024 17:09:27 -0400 Subject: [PATCH 14/36] revert: move setUser back to Provider constructor --- src/Provider.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index 17be848..6bda6ed 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -43,9 +43,7 @@ interface OptimizelyProviderState { export class OptimizelyProvider extends React.Component { constructor(props: OptimizelyProviderProps) { super(props); - } - - componentDidMount(): void { + this.setUserInOptimizely(); } @@ -84,7 +82,7 @@ export class OptimizelyProvider extends React.Component Date: Fri, 29 Mar 2024 17:11:40 -0400 Subject: [PATCH 15/36] style: remove commented code --- src/Provider.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index 6bda6ed..c72a389 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -43,7 +43,7 @@ interface OptimizelyProviderState { export class OptimizelyProvider extends React.Component { constructor(props: OptimizelyProviderProps) { super(props); - + this.setUserInOptimizely(); } @@ -82,7 +82,6 @@ export class OptimizelyProvider extends React.Component Date: Fri, 29 Mar 2024 17:18:55 -0400 Subject: [PATCH 16/36] fix: fetchQualifiedSegments under SSR/sync scenario --- src/client.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/client.ts b/src/client.ts index a5c1c3e..4616bc6 100644 --- a/src/client.ts +++ b/src/client.ts @@ -369,7 +369,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { }; // if user is anonymous... - if (userInfo.id === DefaultUser.id) { + if (userInfo.id === DefaultUser.id) { // wait for the SDK client to be ready before await this._client?.onReady(); // setting the user context @@ -378,16 +378,18 @@ class OptimizelyReactSDKClient implements ReactSDKClient { this.user.id = this.userContext?.getUserId() || DefaultUser.id; this.fetchQualifiedSegments(); - } else { // otherwise if we have the user info, we can... + } else { + // otherwise if we have the user info, we can... // create the user context this.setCurrentUserContext(userInfo); this.user.id = this.userContext?.getUserId() || DefaultUser.id; - // but we still have to wait for the client SDK to be ready - await this._client?.onReady(); - // before we can fetch segments - this.fetchQualifiedSegments(); + // but we still have to wait for the client SDK to be ready + this._client?.onReady().then(() => { + // before we can fetch segments + this.fetchQualifiedSegments(); + }); } if (!this.isUserPromiseResolved) { From b541ff4c332c5553ec01de67abba02f9c7949721 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 1 Apr 2024 15:41:41 -0400 Subject: [PATCH 17/36] ci: VS Code jest settings to run via extension --- .vscode/launch.json | 25 +++++++++++++++++++++++++ .vscode/settings.json | 5 ++--- 2 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4193108 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,25 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "name": "vscode-jest-tests.v2.react-sdk", + "request": "launch", + "args": [ + "--runInBand", + "--watchAll=false", + "--testNamePattern", + "${jest.testNamePattern}", + "--runTestsByPath", + "${jest.testFile}" + ], + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "internalConsoleOptions": "neverOpen", + "program": "${workspaceFolder}/node_modules/.bin/jest", + "windows": { + "program": "${workspaceFolder}/node_modules/jest/bin/jest" + } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 669180a..e79b4db 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,5 +1,4 @@ { - "jest.autoRun": { - "onStartup": ["all-tests"] - } + "jest.runMode": "on-demand", + "jest.jestCommandLine": "~/.nvm/nvm-exec yarn test" } From 41c51fa3df23645a700539a27cafdc5d54c3c4df Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 1 Apr 2024 17:17:17 -0400 Subject: [PATCH 18/36] test: use NotReadyReason enum --- src/Feature.spec.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Feature.spec.tsx b/src/Feature.spec.tsx index a747e80..bc2b824 100644 --- a/src/Feature.spec.tsx +++ b/src/Feature.spec.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2018-2019, 2023 Optimizely + * Copyright 2018-2019, 2023-2024 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import '@testing-library/jest-dom/extend-expect'; import { OptimizelyProvider } from './Provider'; -import { ReactSDKClient, VariableValuesObject } from './client'; +import { NotReadyReason, ReactSDKClient, VariableValuesObject } from './client'; import { OptimizelyFeature } from './Feature'; describe('', () => { @@ -298,7 +298,7 @@ describe('', () => { // while it's waiting for onReady() expect(container.innerHTML).toBe(''); - resolver.resolve({ success: false, reason: 'fail', dataReadyPromise: Promise.resolve() }); + resolver.resolve({ success: false, reason: NotReadyReason.TIMEOUT, dataReadyPromise: Promise.resolve() }); // Simulate config update notification firing after datafile fetched await optimizelyMock.onReady().then(res => res.dataReadyPromise); From 81645828cf638da0d24466d52ace3ca1e8aa8681 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 1 Apr 2024 17:18:14 -0400 Subject: [PATCH 19/36] test: use NotReadyReason & add missing getUserId in mock user context --- src/client.spec.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/client.spec.ts b/src/client.spec.ts index 6db3c8f..93fb4f6 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -28,7 +28,7 @@ jest.mock('./logger', () => { import * as optimizely from '@optimizely/optimizely-sdk'; -import { createInstance, OnReadyResult, ReactSDKClient } from './client'; +import { createInstance, NotReadyReason, OnReadyResult, ReactSDKClient } from './client'; import { logger } from './logger'; interface MockedReactSDKClient extends ReactSDKClient { @@ -51,6 +51,7 @@ describe('ReactSDKClient', () => { decideAll: jest.fn(), decideForKeys: jest.fn(), fetchQualifiedSegments: jest.fn(), + getUserId: jest.fn(), setForcedDecision: jest.fn(), removeForcedDecision: jest.fn(), removeAllForcedDecisions: jest.fn(), @@ -164,12 +165,13 @@ describe('ReactSDKClient', () => { describe('if Optimizely client is null', () => { beforeEach(() => { - // Mocks dataReadyPromise value instead of _client = null because test initialization of instance causes dataReadyPromise to return { success: true } + // Mocks clientAndUserReadyPromise value instead of _client = null because test initialization of + // instance causes clientAndUserReadyPromise to return { success: true } // @ts-ignore - instance.dataReadyPromise = new Promise((resolve, reject) => { + instance.clientAndUserReadyPromise = new Promise((resolve) => { resolve({ success: false, - reason: 'NO_CLIENT', + reason: NotReadyReason.NO_CLIENT, message: 'Optimizely client failed to initialize.', }); }); From 286838f58a657a905a42a6e02553bedfa70a4904 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 1 Apr 2024 17:22:28 -0400 Subject: [PATCH 20/36] fix: add onInitStateChange for default ready result --- src/hooks.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/hooks.ts b/src/hooks.ts index 2583115..038f3c5 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -181,6 +181,17 @@ function subscribeToInitialization( break; default: hooksLogger.warn(`Other reason client not ready, reason="${res.message}"`); + onInitStateChange({ + clientReady: false, + didTimeout: true, // assume timeout + }); + res.dataReadyPromise?.then(() => { + hooksLogger.info('Client became ready later'); + onInitStateChange({ + clientReady: true, + didTimeout: true, // assume timeout + }); + }); } }) .catch(() => { From 425de330c29649e0069ac110ed552922794d5040 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 1 Apr 2024 18:11:13 -0400 Subject: [PATCH 21/36] fix: logic in Promise.all user & client readiness --- src/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 4616bc6..bbf7c05 100644 --- a/src/client.ts +++ b/src/client.ts @@ -254,8 +254,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient { this.clientAndUserReadyPromiseFulfilled = true; return { - success: true, // needs to always be true - message: this.isReady() ? 'Client and user are both.' : 'Client or user did not become ready.', + success: this.isReady(), + message: this.isReady() ? 'Client and user are both ready.' : 'Client or user did not become ready.', }; } ); From 81be217c9651e6fc80eff0278695217e576f5cf3 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 2 Apr 2024 17:03:49 -0400 Subject: [PATCH 22/36] refactor: isReady() to isReactClientReady() --- src/client.ts | 27 +++++++++++++++++---------- src/hooks.ts | 6 +++--- 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/src/client.ts b/src/client.ts index bbf7c05..0d5355d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -60,7 +60,7 @@ export interface ReactSDKClient extends Omit; setUser(userInfo: UserInfo): Promise; onUserUpdate(handler: OnUserUpdateHandler): DisposeFn; - isReady(): boolean; + isReactClientReady(): boolean; getIsReadyPromiseFulfilled(): boolean; getIsUsingSdkKey(): boolean; @@ -249,13 +249,15 @@ class OptimizelyReactSDKClient implements ReactSDKClient { ([userResult, clientResult]) => { this.isClientReady = clientResult.success; this.isUserReady = userResult.success; + const clientAndUserReady = this.isClientReady && this.isUserReady; - // Client and user can become ready synchronously and/or asynchronously. This flag specifically indicates that they became ready asynchronously. this.clientAndUserReadyPromiseFulfilled = true; return { - success: this.isReady(), - message: this.isReady() ? 'Client and user are both ready.' : 'Client or user did not become ready.', + success: clientAndUserReady, + message: clientAndUserReady + ? 'Client and user are both ready.' + : 'Client or user did not become ready.', }; } ); @@ -344,7 +346,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } private makeUserContextInstance(userInfo: UserInfo): optimizely.OptimizelyUserContext | null { - if (!this._client || !this.isReady()) { + if (!this._client) { logger.warn( `Unable to create user context for ${userInfo.id}. Optimizely client failed to initialize or not ready.` ); @@ -355,7 +357,11 @@ class OptimizelyReactSDKClient implements ReactSDKClient { } public async fetchQualifiedSegments(options?: optimizely.OptimizelySegmentOption[]): Promise { - if (!this.userContext || !this.isReady() || this.odpExplicitlyOff || !this.getIsReadyPromiseFulfilled()) { + if (this.odpExplicitlyOff) { + return true; + } + + if (!this.userContext) { return false; } @@ -377,7 +383,9 @@ class OptimizelyReactSDKClient implements ReactSDKClient { this.user.id = this.userContext?.getUserId() || DefaultUser.id; - this.fetchQualifiedSegments(); + if (this.isClientReady) { + this.fetchQualifiedSegments(); + } } else { // otherwise if we have the user info, we can... // create the user context @@ -428,9 +436,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient { }; } - public isReady(): boolean { - // React SDK Instance only becomes ready when both JS SDK client and the user info are ready. - return this.isUserReady && this.isClientReady; + public isReactClientReady(): boolean { + return this.isClientReady; } /** diff --git a/src/hooks.ts b/src/hooks.ts index 038f3c5..2e9f10f 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -232,7 +232,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri [optimizely, experimentKey, overrides.overrideUserId, overrideAttrs] ); - const isClientReady = isServerSide || optimizely.isReady(); + const isClientReady = isServerSide || optimizely.isReactClientReady(); const [state, setState] = useState(() => { const decisionState = isClientReady ? getCurrentDecision() : { variation: null }; return { @@ -330,7 +330,7 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) [optimizely, featureKey, overrides.overrideUserId, overrideAttrs] ); - const isClientReady = isServerSide || optimizely.isReady(); + const isClientReady = isServerSide || optimizely.isReactClientReady(); const [state, setState] = useState(() => { const decisionState = isClientReady ? getCurrentDecision() : { isEnabled: false, variables: {} }; return { @@ -420,7 +420,7 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) decision: optimizely.decide(flagKey, options.decideOptions, overrides.overrideUserId, overrideAttrs), }); - const isClientReady = isServerSide || optimizely.isReady(); + const isClientReady = isServerSide || optimizely.isReactClientReady(); const [state, setState] = useState<{ decision: OptimizelyDecision } & InitializationState>(() => { const decisionState = isClientReady ? getCurrentDecision() From 2d6dd19c011ea3d34af75e58c740570285fe479c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Tue, 2 Apr 2024 17:04:29 -0400 Subject: [PATCH 23/36] test: fixes for uses of getUserId() in setUser() --- src/client.spec.ts | 58 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/src/client.spec.ts b/src/client.spec.ts index 93fb4f6..26ee7d8 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -28,7 +28,7 @@ jest.mock('./logger', () => { import * as optimizely from '@optimizely/optimizely-sdk'; -import { createInstance, NotReadyReason, OnReadyResult, ReactSDKClient } from './client'; +import { createInstance, DefaultUser, NotReadyReason, OnReadyResult, ReactSDKClient } from './client'; import { logger } from './logger'; interface MockedReactSDKClient extends ReactSDKClient { @@ -37,6 +37,7 @@ interface MockedReactSDKClient extends ReactSDKClient { } describe('ReactSDKClient', () => { + const validVuid = 'vuid_8de3bb278fce47f6b000cadc1ac'; const config: optimizely.Config = { datafile: {}, }; @@ -165,10 +166,10 @@ describe('ReactSDKClient', () => { describe('if Optimizely client is null', () => { beforeEach(() => { - // Mocks clientAndUserReadyPromise value instead of _client = null because test initialization of + // Mocks clientAndUserReadyPromise value instead of _client = null because test initialization of // instance causes clientAndUserReadyPromise to return { success: true } // @ts-ignore - instance.clientAndUserReadyPromise = new Promise((resolve) => { + instance.clientAndUserReadyPromise = new Promise(resolve => { resolve({ success: false, reason: NotReadyReason.NO_CLIENT, @@ -192,11 +193,15 @@ describe('ReactSDKClient', () => { resolveInnerClientOnReady = res; }); mockInnerClientOnReady.mockReturnValueOnce(mockReadyPromise); + const userId = 'user999'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); + await instance.setUser({ - id: 'user999', + id: userId, }); resolveInnerClientOnReady!({ success: true }); const result = await instance.onReady(); + expect(result.success).toBe(false); }); }); @@ -219,16 +224,29 @@ describe('ReactSDKClient', () => { }); describe('setUser', () => { + it('can be called with no/default user set', async () => { + const instance = createInstance(config); + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(validVuid); + + await instance.setUser(DefaultUser); + + expect(instance.user.id).toEqual(validVuid); + }); + it('updates the user object with id and attributes', async () => { + const userId = 'xxfueaojfe8&86'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); + const instance = createInstance(config); await instance.setUser({ - id: 'xxfueaojfe8&86', + id: userId, attributes: { foo: 'bar', }, }); + expect(instance.user).toEqual({ - id: 'xxfueaojfe8&86', + id: userId, attributes: { foo: 'bar', }, @@ -236,25 +254,31 @@ describe('ReactSDKClient', () => { }); it('adds and removes update handlers', async () => { + const userId = 'newUser'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); const instance = createInstance(config); const onUserUpdateListener = jest.fn(); const dispose = instance.onUserUpdate(onUserUpdateListener); + await instance.setUser({ - id: 'newUser', + id: userId, }); + expect(onUserUpdateListener).toBeCalledTimes(1); expect(onUserUpdateListener).toBeCalledWith({ - id: 'newUser', + id: userId, attributes: {}, }); + dispose(); await instance.setUser({ id: 'newUser2', }); + expect(onUserUpdateListener).toBeCalledTimes(1); }); - it('does not call fetchqualifiedsegements on setUser if onready is not calleed initially', async () => { + it('does not call fetchqualifiedsegements on setUser if onready is not called initially', async () => { const instance = createInstance(config); jest.spyOn(instance, 'fetchQualifiedSegments').mockImplementation(async () => true); @@ -288,9 +312,11 @@ describe('ReactSDKClient', () => { describe('pre-set user and user overrides', () => { let instance: ReactSDKClient; beforeEach(async () => { + const userId = 'user1'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); instance = createInstance(config); await instance.setUser({ - id: 'user1', + id: userId, attributes: { foo: 'bar', }, @@ -1069,9 +1095,11 @@ describe('ReactSDKClient', () => { } } ); + const userId = 'user1123'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); const instance = createInstance(config); await instance.setUser({ - id: 'user1123', + id: userId, }); const result = instance.getFeatureVariables('feat1'); expect(result).toEqual({ @@ -1355,9 +1383,11 @@ describe('ReactSDKClient', () => { describe('setForcedDecision', () => { let instance: ReactSDKClient; beforeEach(async () => { + const userId = 'user1'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); instance = createInstance(config); await instance.setUser({ - id: 'user1', + id: userId, attributes: { foo: 'bar', }, @@ -1376,7 +1406,6 @@ describe('ReactSDKClient', () => { variables: {}, variationKey: 'varition1', }); - // @ts-ignore instance._client = null; @@ -1464,6 +1493,8 @@ describe('ReactSDKClient', () => { describe('removeForcedDecision', () => { let instance: ReactSDKClient; beforeEach(async () => { + const userId = 'user1'; + jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); instance = createInstance(config); await instance.setUser({ id: 'user1', @@ -1642,7 +1673,6 @@ describe('ReactSDKClient', () => { }); it('should return a valid vuid', async () => { - const validVuid = 'vuid_8de3bb278fce47f6b000cadc1ac'; const mockGetVuid = mockInnerClient.getVuid as jest.Mock; mockGetVuid.mockReturnValue(validVuid); From e8116ebf987149041fe653b8514bf898bdddebff Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 3 Apr 2024 12:39:13 -0400 Subject: [PATCH 24/36] wip: fixing tests --- src/client.spec.ts | 13 ++++++++++--- src/client.ts | 4 +--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/client.spec.ts b/src/client.spec.ts index 26ee7d8..8fe0b6a 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -146,21 +146,27 @@ describe('ReactSDKClient', () => { }); it('fulfills the returned promise with success: true when a user is set', async () => { - jest.spyOn(instance, 'fetchQualifiedSegments').mockImplementation(async () => true); + jest.spyOn(mockInnerClient, 'onReady').mockResolvedValue({ success: true }); + const instance = createInstance(config); await instance.setUser({ id: 'user12345', }); + const result = await instance.onReady(); + expect(result.success).toBe(true); }); it('fulfills the returned promise with success: false when fetchqualifiedsegment is false', async () => { - jest.spyOn(instance, 'fetchQualifiedSegments').mockImplementation(async () => false); + jest.spyOn(mockInnerClient, 'onReady').mockResolvedValue({ success: true }); + const instance = createInstance(config); + jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(false); await instance.setUser({ id: 'user12345', }); const result = await instance.onReady(); + expect(result.success).toBe(false); }); @@ -213,6 +219,7 @@ describe('ReactSDKClient', () => { resolveInnerClientOnReady = res; }); mockInnerClientOnReady.mockReturnValueOnce(mockReadyPromise); + const instance = createInstance(config); jest.spyOn(instance, 'fetchQualifiedSegments').mockImplementation(async () => true); await instance.setUser({ id: 'user999', @@ -280,7 +287,7 @@ describe('ReactSDKClient', () => { it('does not call fetchqualifiedsegements on setUser if onready is not called initially', async () => { const instance = createInstance(config); - jest.spyOn(instance, 'fetchQualifiedSegments').mockImplementation(async () => true); + jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(true); await instance.setUser({ id: 'xxfueaojfe8&86', diff --git a/src/client.ts b/src/client.ts index 0d5355d..a9c3e43 100644 --- a/src/client.ts +++ b/src/client.ts @@ -255,9 +255,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return { success: clientAndUserReady, - message: clientAndUserReady - ? 'Client and user are both ready.' - : 'Client or user did not become ready.', + message: clientAndUserReady ? 'Client and user are both ready.' : 'Client or user did not become ready.', }; } ); From 12a5b787eea0ab85557bd3daffc142dbfc7903ba Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 3 Apr 2024 14:42:38 -0400 Subject: [PATCH 25/36] wip: fixed more tests --- src/client.spec.ts | 12 +++++++----- src/client.ts | 19 ++++++------------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/client.spec.ts b/src/client.spec.ts index 8fe0b6a..ccf6aaa 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -148,12 +148,14 @@ describe('ReactSDKClient', () => { it('fulfills the returned promise with success: true when a user is set', async () => { jest.spyOn(mockInnerClient, 'onReady').mockResolvedValue({ success: true }); const instance = createInstance(config); + jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(true); + await instance.setUser({ id: 'user12345', }); const result = await instance.onReady(); - + expect(result.success).toBe(true); }); @@ -194,18 +196,18 @@ describe('ReactSDKClient', () => { it('waits for the inner client onReady to fulfill with success = false before fulfilling the returned promise', async () => { const mockInnerClientOnReady = jest.spyOn(mockInnerClient, 'onReady'); - let resolveInnerClientOnReady: (result: OnReadyResult) => void; + let resolveInnerClientOnReady: (result: OnReadyResult) => void = () => {}; const mockReadyPromise: Promise = new Promise(res => { resolveInnerClientOnReady = res; }); mockInnerClientOnReady.mockReturnValueOnce(mockReadyPromise); const userId = 'user999'; jest.spyOn(mockOptimizelyUserContext, 'getUserId').mockReturnValue(userId); + resolveInnerClientOnReady({ success: true }); await instance.setUser({ id: userId, }); - resolveInnerClientOnReady!({ success: true }); const result = await instance.onReady(); expect(result.success).toBe(false); @@ -285,7 +287,7 @@ describe('ReactSDKClient', () => { expect(onUserUpdateListener).toBeCalledTimes(1); }); - it('does not call fetchqualifiedsegements on setUser if onready is not called initially', async () => { + it('implicitly calls fetchqualifiedsegements', async () => { const instance = createInstance(config); jest.spyOn(instance, 'fetchQualifiedSegments').mockResolvedValue(true); @@ -293,7 +295,7 @@ describe('ReactSDKClient', () => { id: 'xxfueaojfe8&86', }); - expect(instance.fetchQualifiedSegments).toBeCalledTimes(0); + expect(instance.fetchQualifiedSegments).toBeCalledTimes(1); }); it('calls fetchqualifiedsegements internally on each setuser call after onready', async () => { diff --git a/src/client.ts b/src/client.ts index a9c3e43..dfa2845 100644 --- a/src/client.ts +++ b/src/client.ts @@ -372,8 +372,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient { attributes: userInfo.attributes || DefaultUser.attributes, }; - // if user is anonymous... - if (userInfo.id === DefaultUser.id) { + let fetchQualifedSegmentsSucceed = false; + if (userInfo.id === DefaultUser.id) { // if user is anonymous... // wait for the SDK client to be ready before await this._client?.onReady(); // setting the user context @@ -381,25 +381,18 @@ class OptimizelyReactSDKClient implements ReactSDKClient { this.user.id = this.userContext?.getUserId() || DefaultUser.id; - if (this.isClientReady) { - this.fetchQualifiedSegments(); - } + fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); } else { - // otherwise if we have the user info, we can... - // create the user context this.setCurrentUserContext(userInfo); this.user.id = this.userContext?.getUserId() || DefaultUser.id; - // but we still have to wait for the client SDK to be ready - this._client?.onReady().then(() => { - // before we can fetch segments - this.fetchQualifiedSegments(); - }); + await this._client?.onReady(); + fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); } if (!this.isUserPromiseResolved) { - this.userPromiseResolver({ success: true }); + this.userPromiseResolver({ success: fetchQualifedSegmentsSucceed }); this.isUserPromiseResolved = true; } From 08ef8de30e037242f9a3d534fc3253ca078204f0 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 3 Apr 2024 14:50:22 -0400 Subject: [PATCH 26/36] revert: refactor of isReactClientReady() --- src/client.ts | 7 ++++--- src/hooks.ts | 6 +++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client.ts b/src/client.ts index dfa2845..b057491 100644 --- a/src/client.ts +++ b/src/client.ts @@ -60,7 +60,7 @@ export interface ReactSDKClient extends Omit; setUser(userInfo: UserInfo): Promise; onUserUpdate(handler: OnUserUpdateHandler): DisposeFn; - isReactClientReady(): boolean; + isReady(): boolean; getIsReadyPromiseFulfilled(): boolean; getIsUsingSdkKey(): boolean; @@ -373,7 +373,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient { }; let fetchQualifedSegmentsSucceed = false; - if (userInfo.id === DefaultUser.id) { // if user is anonymous... + if (userInfo.id === DefaultUser.id) { + // if user is anonymous... // wait for the SDK client to be ready before await this._client?.onReady(); // setting the user context @@ -427,7 +428,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { }; } - public isReactClientReady(): boolean { + public isReady(): boolean { return this.isClientReady; } diff --git a/src/hooks.ts b/src/hooks.ts index 2e9f10f..038f3c5 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -232,7 +232,7 @@ export const useExperiment: UseExperiment = (experimentKey, options = {}, overri [optimizely, experimentKey, overrides.overrideUserId, overrideAttrs] ); - const isClientReady = isServerSide || optimizely.isReactClientReady(); + const isClientReady = isServerSide || optimizely.isReady(); const [state, setState] = useState(() => { const decisionState = isClientReady ? getCurrentDecision() : { variation: null }; return { @@ -330,7 +330,7 @@ export const useFeature: UseFeature = (featureKey, options = {}, overrides = {}) [optimizely, featureKey, overrides.overrideUserId, overrideAttrs] ); - const isClientReady = isServerSide || optimizely.isReactClientReady(); + const isClientReady = isServerSide || optimizely.isReady(); const [state, setState] = useState(() => { const decisionState = isClientReady ? getCurrentDecision() : { isEnabled: false, variables: {} }; return { @@ -420,7 +420,7 @@ export const useDecision: UseDecision = (flagKey, options = {}, overrides = {}) decision: optimizely.decide(flagKey, options.decideOptions, overrides.overrideUserId, overrideAttrs), }); - const isClientReady = isServerSide || optimizely.isReactClientReady(); + const isClientReady = isServerSide || optimizely.isReady(); const [state, setState] = useState<{ decision: OptimizelyDecision } & InitializationState>(() => { const decisionState = isClientReady ? getCurrentDecision() From bdb469c5192542ee6222425f054720c19b3c8c40 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 3 Apr 2024 14:53:29 -0400 Subject: [PATCH 27/36] docs: Update copyrights --- src/Provider.tsx | 2 +- src/client.spec.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index c72a389..e3ad5cd 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/src/client.spec.ts b/src/client.spec.ts index ccf6aaa..ffa629b 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -5,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * 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, From 8967c158163bf301cc31d02113113016fff8ecde Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Wed, 3 Apr 2024 15:58:15 -0400 Subject: [PATCH 28/36] fix: later setUser not getting new usercontext (manual testing) --- src/client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index b057491..9567d4b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -338,9 +338,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return; } - if (!this.userContext || (this.userContext && !areUsersEqual(userInfo, this.user))) { - this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); - } + this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); } private makeUserContextInstance(userInfo: UserInfo): optimizely.OptimizelyUserContext | null { From 01e72c77897332598bb36f7ecd9c277af88f60ca Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 5 Apr 2024 10:23:57 -0400 Subject: [PATCH 29/36] fix: PR review changes --- src/Provider.tsx | 5 ++++- src/client.spec.ts | 1 + src/client.ts | 15 ++++++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/Provider.tsx b/src/Provider.tsx index e3ad5cd..a3612bc 100644 --- a/src/Provider.tsx +++ b/src/Provider.tsx @@ -76,7 +76,10 @@ export class OptimizelyProvider extends React.Component { decideForKeys: jest.fn(), fetchQualifiedSegments: jest.fn(), getUserId: jest.fn(), + getAttributes: jest.fn(), setForcedDecision: jest.fn(), removeForcedDecision: jest.fn(), removeAllForcedDecisions: jest.fn(), diff --git a/src/client.ts b/src/client.ts index 9567d4b..11a8a39 100644 --- a/src/client.ts +++ b/src/client.ts @@ -338,7 +338,18 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return; } - this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); + if (!this.userContext) { + this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); + return; + } + + const currentUserContextUserInfo: UserInfo = { + id: this.userContext.getUserId(), + attributes: this.userContext.getAttributes(), + }; + if (!areUsersEqual(userInfo, currentUserContextUserInfo)) { + this.userContext = this._client.createUserContext(userInfo.id || undefined, userInfo.attributes); + } } private makeUserContextInstance(userInfo: UserInfo): optimizely.OptimizelyUserContext | null { @@ -378,12 +389,14 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // setting the user context this.setCurrentUserContext(userInfo); + // (potentially) retrieve the VUID set in JS userContext or noop or to DefaultUser this.user.id = this.userContext?.getUserId() || DefaultUser.id; fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); } else { this.setCurrentUserContext(userInfo); + // ensure the underlying userContext ID matches this.user.id = this.userContext?.getUserId() || DefaultUser.id; await this._client?.onReady(); From 569cf66fb11377a2cb9286fb3302a3a9f4a9780b Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 5 Apr 2024 12:36:33 -0400 Subject: [PATCH 30/36] test: add initial OptimizelyProvider tests --- src/Provider.spec.tsx | 95 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 src/Provider.spec.tsx diff --git a/src/Provider.spec.tsx b/src/Provider.spec.tsx new file mode 100644 index 0000000..98739c2 --- /dev/null +++ b/src/Provider.spec.tsx @@ -0,0 +1,95 @@ +/** + * 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. + */ + +/// + +//jest.mock('./client'); + +import React from 'react'; +import { render, act } from '@testing-library/react'; +import { OptimizelyProvider } from './Provider'; +import { DefaultUser, ReactSDKClient, createInstance } from './client'; + +describe('OptimizelyProvider', () => { + let mockReactClient: ReactSDKClient; + const config = { + datafile: {}, + }; + + beforeEach(() => { + mockReactClient = ({ + user: { + id: 'test-id', + attributes: {}, + }, + setUser: jest.fn().mockResolvedValue(undefined), + } as unknown) as ReactSDKClient; + }); + + it('should render successfully with user provided', () => { + act(() => { + render(); + }); + + expect(mockReactClient.setUser).toHaveBeenCalledWith({ + id: 'user1', + attributes: {}, + }); + }); + + it('should render successfully with userId provided', () => { + act(() => { + render(); + }); + + expect(mockReactClient.setUser).toHaveBeenCalledWith({ + id: 'user1', + attributes: {}, + }); + }); + + it('should render successfully without user or userId provided', () => { + act(() => { + render(); + }); + + expect(mockReactClient.setUser).toHaveBeenCalledWith(DefaultUser); + }); + + it('should render successfully with user id & attributes provided', () => { + act(() => { + render( + + ); + }); + + expect(mockReactClient.setUser).toHaveBeenCalledWith({ + id: 'user1', + attributes: { attr1: 'value1' }, + }); + }); + + it('should succeed just userAttributes provided', () => { + act(() => { + render(); + }); + + expect(mockReactClient.setUser).toHaveBeenCalledWith({ + id: DefaultUser.id, + attributes: { attr1: 'value1' }, + }); + }); +}); From b0503cf02641c305f3c5742d8109db2ee55bbd4c Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 5 Apr 2024 13:33:25 -0400 Subject: [PATCH 31/36] fix: PR review changes --- src/client.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/client.ts b/src/client.ts index 11a8a39..4dba122 100644 --- a/src/client.ts +++ b/src/client.ts @@ -381,9 +381,8 @@ class OptimizelyReactSDKClient implements ReactSDKClient { attributes: userInfo.attributes || DefaultUser.attributes, }; - let fetchQualifedSegmentsSucceed = false; + // if user is anonymous... if (userInfo.id === DefaultUser.id) { - // if user is anonymous... // wait for the SDK client to be ready before await this._client?.onReady(); // setting the user context @@ -391,17 +390,14 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // (potentially) retrieve the VUID set in JS userContext or noop or to DefaultUser this.user.id = this.userContext?.getUserId() || DefaultUser.id; - - fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); } else { + // we can set the underlying user context synchronously this.setCurrentUserContext(userInfo); - // ensure the underlying userContext ID matches - this.user.id = this.userContext?.getUserId() || DefaultUser.id; - + // we need to wait for fetch qualified segments await this._client?.onReady(); - fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); } + const fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); if (!this.isUserPromiseResolved) { this.userPromiseResolver({ success: fetchQualifedSegmentsSucceed }); From f5b9a14018bfdf883aef39d84c14268c12412fd5 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Fri, 5 Apr 2024 13:47:40 -0400 Subject: [PATCH 32/36] docs: add clarification inline comments --- src/client.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/client.ts b/src/client.ts index 4dba122..31d754a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -383,7 +383,7 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // if user is anonymous... if (userInfo.id === DefaultUser.id) { - // wait for the SDK client to be ready before + // wait for the SDK client to be ready before... await this._client?.onReady(); // setting the user context this.setCurrentUserContext(userInfo); @@ -391,12 +391,13 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // (potentially) retrieve the VUID set in JS userContext or noop or to DefaultUser this.user.id = this.userContext?.getUserId() || DefaultUser.id; } else { - // we can set the underlying user context synchronously + // synchronous user context setting is required including for server side rendering (SSR) this.setCurrentUserContext(userInfo); - // we need to wait for fetch qualified segments + // we need to wait for fetch qualified segments success for failure await this._client?.onReady(); } + const fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); if (!this.isUserPromiseResolved) { From d349a050cc39d98df34b0a860926dc3dacda99d3 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 8 Apr 2024 08:58:11 -0400 Subject: [PATCH 33/36] build: bump underlying JS SDK version to 5.3.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 35bb050..54de565 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "access": "public" }, "dependencies": { - "@optimizely/optimizely-sdk": "^5.2.1", + "@optimizely/optimizely-sdk": "^5.3.0", "hoist-non-react-statics": "^3.3.0", "prop-types": "^15.6.2", "utility-types": "^2.1.0 || ^3.0.0" diff --git a/yarn.lock b/yarn.lock index 433febf..43f39ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -655,10 +655,10 @@ "@jridgewell/resolve-uri" "^3.0.3" "@jridgewell/sourcemap-codec" "^1.4.10" -"@optimizely/optimizely-sdk@^5.2.1": - version "5.2.1" - resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-5.2.1.tgz#b42b6d5b39a27b81432918b87a7984584bf9c5fc" - integrity sha512-B6JugOJ6+H0GhXcSQCb9yRDU3S8ogXQxfKN5Dl3S1YfyrXmvCU7Y1J9PvV6/PBz9itXhcvnMfje6iyMCt57wGg== +"@optimizely/optimizely-sdk@^5.3.0": + version "5.3.0" + resolved "https://registry.yarnpkg.com/@optimizely/optimizely-sdk/-/optimizely-sdk-5.3.0.tgz#dd1ca9d19287b31675d2f95c24234e074ade9a8b" + integrity sha512-PzfjcApCvcHGir8XWSG3IBaOJXvPADjqpzXypEWTfArrONA3FlmqdnwDAlxF4b557fo/UZI6ZCyj3AWrG8cprg== dependencies: decompress-response "^4.2.1" json-schema "^0.4.0" From 24f0ae85053991e6f7f541f6c82470c858f291c0 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 8 Apr 2024 09:20:33 -0400 Subject: [PATCH 34/36] fix: add missing getProjectConfig() from JS SDK --- src/client.spec.ts | 1 + src/client.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/client.spec.ts b/src/client.spec.ts index 334db7c..1b7b65b 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -78,6 +78,7 @@ describe('ReactSDKClient', () => { getFeatureVariableInteger: jest.fn(() => null), getFeatureVariableString: jest.fn(() => null), getOptimizelyConfig: jest.fn(() => null), + getProjectConfig: jest.fn(() => null), onReady: jest.fn(() => Promise.resolve({ success: false })), close: jest.fn(), getVuid: jest.fn(), diff --git a/src/client.ts b/src/client.ts index 31d754a..7d72d85 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,6 +19,7 @@ import { OptimizelyDecision, UserInfo, createFailedDecision, areUsersEqual } fro import { notifier } from './notifier'; import { logger } from './logger'; import { FeatureVariableValue } from '@optimizely/optimizely-sdk'; +import { ProjectConfig } from '@optimizely/optimizely-sdk/dist/core/project_config'; export type VariableValuesObject = { [key: string]: any; @@ -391,13 +392,13 @@ class OptimizelyReactSDKClient implements ReactSDKClient { // (potentially) retrieve the VUID set in JS userContext or noop or to DefaultUser this.user.id = this.userContext?.getUserId() || DefaultUser.id; } else { - // synchronous user context setting is required including for server side rendering (SSR) + // synchronous user context setting is required including for server side rendering (SSR) this.setCurrentUserContext(userInfo); // we need to wait for fetch qualified segments success for failure await this._client?.onReady(); } - + const fetchQualifedSegmentsSucceed = await this.fetchQualifiedSegments(); if (!this.isUserPromiseResolved) { @@ -1165,6 +1166,18 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return this._client.getOptimizelyConfig(); } + /** + * Returns the ProjectConfig object from the underlying SDK + * @returns {ProjectConfig | null} optimizely project config + */ + public getProjectConfig(): ProjectConfig | null { + if (!this._client) { + logger.warn('Unable to get the project configuration because Optimizely client was not initialized.'); + return null; + } + return this._client.getProjectConfig(); + } + /** * Cleanup method for killing an running timers and flushing eventQueue * @returns {Promise<{ success: boolean; reason?: string }>} From 0a9951c35ec4cbb12ecb95bfb9fbc4a233ae7954 Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 8 Apr 2024 12:32:36 -0400 Subject: [PATCH 35/36] refactor: omit getProjectConfig instead of implementing it --- src/client.ts | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/src/client.ts b/src/client.ts index 7d72d85..ff759d0 100644 --- a/src/client.ts +++ b/src/client.ts @@ -55,7 +55,7 @@ export const DefaultUser: UserInfo = { attributes: {}, }; -export interface ReactSDKClient extends Omit { +export interface ReactSDKClient extends Omit { user: UserInfo; onReady(opts?: { timeout?: number }): Promise; @@ -1166,18 +1166,6 @@ class OptimizelyReactSDKClient implements ReactSDKClient { return this._client.getOptimizelyConfig(); } - /** - * Returns the ProjectConfig object from the underlying SDK - * @returns {ProjectConfig | null} optimizely project config - */ - public getProjectConfig(): ProjectConfig | null { - if (!this._client) { - logger.warn('Unable to get the project configuration because Optimizely client was not initialized.'); - return null; - } - return this._client.getProjectConfig(); - } - /** * Cleanup method for killing an running timers and flushing eventQueue * @returns {Promise<{ success: boolean; reason?: string }>} From ac5075c9d9be8e6efb333c00b8da913816a12d1e Mon Sep 17 00:00:00 2001 From: Mike Chu Date: Mon, 8 Apr 2024 12:33:34 -0400 Subject: [PATCH 36/36] style: remove unused import --- src/client.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index ff759d0..5c57bc7 100644 --- a/src/client.ts +++ b/src/client.ts @@ -19,7 +19,6 @@ import { OptimizelyDecision, UserInfo, createFailedDecision, areUsersEqual } fro import { notifier } from './notifier'; import { logger } from './logger'; import { FeatureVariableValue } from '@optimizely/optimizely-sdk'; -import { ProjectConfig } from '@optimizely/optimizely-sdk/dist/core/project_config'; export type VariableValuesObject = { [key: string]: any;