Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default uninitialized EppoClient instance #48

Merged
merged 5 commits into from
Feb 5, 2024

Conversation

aarsilv
Copy link
Contributor

@aarsilv aarsilv commented Feb 3, 2024

🎟️ (Eppo Internal) FF-1557 - JS Client SDK should return null when not initialized
πŸ‘― Related PR: js-client-sdk #37

#45 introduced a change of behavior, where calls to getInstance() before the client was initialized would throw an error instead of returning an uninitialized client. This Pull request reverts that behavior change so that there will always be a client, and if uninitialized, it will just return null on any assignment calls.

This is done by instantiating a client statically at import time--as we were previously doing (link)--as opposed to during init().

So that we can still leverage the client handling polling for configuration requests, we use EppoClients newly-added (see the related PR)setConfigurationRequestParameters() where we can pass in the configuration request parameters during init() instead of needing the when constructing the instance.

**Signature:**

```typescript
static initialized: boolean;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs got updated because the way index.ts is structured, initialized has to be public πŸ™„

@@ -4,7 +4,7 @@

## IClientConfig.throwOnFailedInitialization property

Throw on error if unable to fetch an initial configuration during initialization. (default: true)
Throw an error if unable to fetch an initial configuration during initialization. (default: true)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Grammar! πŸ“ˆ

package.json Outdated
@@ -58,7 +58,7 @@
"xhr-mock": "^2.5.1"
},
"dependencies": {
"@eppo/js-client-sdk-common": "2.1.0",
"@eppo/js-client-sdk-common": "file:../js-client-sdk-common",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will be replaced with "2.1.1" once published

@@ -379,7 +382,7 @@ describe('EppoJSClient E2E test', () => {
flags: {
[hashedFlagKey]: mockExperimentConfig,
},
};
} as unknown as Record<string, IExperimentConfiguration>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TypeScript insisted on the double as because we are wiling out with our types here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I've seen typescript do this to me before

Comment on lines +558 to +564
jest.isolateModules(() => {
// Isolate and re-require so that the static instance is reset to it's default state
// eslint-disable-next-line @typescript-eslint/no-var-requires
const reloadedModule = require('./index');
init = reloadedModule.init;
getInstance = reloadedModule.getInstance;
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isolateModules() and require() combo to reset the static EppoJSClient instance took me a bit to figure out and it had weird side effects, such as making test double (td) not work (hence me using returnRac() instead of mocking a response like other tests cases). Also, using this outside of a beforeEach()--which I did at first because I just have one test case--results in console.log() not actually logging anything 🀯

TL;DR; use singletons and jest.isolateModules() with extreme caution, but it is a potentially useful tool to have your toolbox.

@@ -69,12 +69,15 @@ export interface IClientConfig {

export { IAssignmentLogger, IAssignmentEvent, IEppoClient } from '@eppo/js-client-sdk-common';

const localStorage = new EppoLocalStorage();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved this back to the module-level (where it was) so it can be used in the static initialization of EppoJSClient.instance

@@ -83,6 +86,7 @@ export class EppoJSClient extends EppoClient {
subjectAttributes?: Record<string, any>,
assignmentHooks?: IAssignmentHooks,
): string | null {
EppoJSClient.getAssignmentInitializationCheck();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we do want to warn people who request assignments on uninitialized instances.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, we can mute it in the future if requested. Currently the greater issue seems to be confusion about unexpected null returns

Comment on lines -217 to -219
if (!EppoJSClient.instance) {
throw Error('init() must first be called to initialize a client instance');
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Farewell...for now... 😒

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the intention of alerting if the SDK is functioning in an unintended way, but I won't miss the risk of throwing exceptions in client code

@@ -379,7 +382,7 @@ describe('EppoJSClient E2E test', () => {
flags: {
[hashedFlagKey]: mockExperimentConfig,
},
};
} as unknown as Record<string, IExperimentConfiguration>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes, I've seen typescript do this to me before

let getInstance: Function;
beforeEach(() => {
jest.isolateModules(() => {
// Isolate and re-require so that the static instance is reset to it's default state
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment

@@ -83,6 +86,7 @@ export class EppoJSClient extends EppoClient {
subjectAttributes?: Record<string, any>,
assignmentHooks?: IAssignmentHooks,
): string | null {
EppoJSClient.getAssignmentInitializationCheck();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, we can mute it in the future if requested. Currently the greater issue seems to be confusion about unexpected null returns

Comment on lines -217 to -219
if (!EppoJSClient.instance) {
throw Error('init() must first be called to initialize a client instance');
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the intention of alerting if the SDK is functioning in an unintended way, but I won't miss the risk of throwing exceptions in client code

@aarsilv aarsilv merged commit e842de5 into main Feb 5, 2024
2 checks passed
@aarsilv aarsilv deleted the aaron/ff-1557/optional-dummy-initial branch February 5, 2024 16:13
@aarsilv aarsilv mentioned this pull request Feb 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants