From 1aab3f3156475846d0aef8c39c64e24959ddf0f8 Mon Sep 17 00:00:00 2001 From: tore-statsig <74584483+tore-statsig@users.noreply.github.com> Date: Fri, 17 Feb 2023 12:54:55 -0800 Subject: [PATCH] Add sdk option to bypass window undefined checks (#234) * Add option to bypass window undefined check * allow logger interval as well * fix window check * check that flush interval is setup * bump v4.29.0 --- package-lock.json | 4 +- package.json | 2 +- src/StatsigLogger.ts | 4 +- src/StatsigNetwork.ts | 9 ++-- src/StatsigSDKOptions.ts | 7 +++ src/__tests__/StatsigClientOnServer.test.ts | 47 +++++++++++++++++++ src/__tests__/basic_initialize_response.json | 46 +++++++++++++++++++ src/__tests__/index.test.ts | 48 +------------------- 8 files changed, 112 insertions(+), 55 deletions(-) create mode 100644 src/__tests__/basic_initialize_response.json diff --git a/package-lock.json b/package-lock.json index 171cb77..1f22fb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "statsig-js", - "version": "4.28.1", + "version": "4.29.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "statsig-js", - "version": "4.28.1", + "version": "4.29.0", "license": "ISC", "dependencies": { "js-sha256": "^0.9.0", diff --git a/package.json b/package.json index 4d94dbf..8148f5e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "statsig-js", - "version": "4.28.1", + "version": "4.29.0", "description": "Statsig JavaScript client SDK for single user environments.", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/src/StatsigLogger.ts b/src/StatsigLogger.ts index a8eaf83..a62261b 100644 --- a/src/StatsigLogger.ts +++ b/src/StatsigLogger.ts @@ -78,7 +78,7 @@ export default class StatsigLogger { this.flush(document.visibilityState !== 'visible'); }); } - if (typeof window === 'undefined' || window == null) { + if (!this.sdkInternal.getOptions().getIgnoreWindowUndefined() && (typeof window === 'undefined' || window == null)) { // dont set the flush interval outside of client browser environments return; } @@ -111,7 +111,7 @@ export default class StatsigLogger { event.addStatsigMetadata('currentPage', parts[0]); } } - } catch (_e) {} + } catch (_e) { } this.queue.push(event.toJsonObject()); diff --git a/src/StatsigNetwork.ts b/src/StatsigNetwork.ts index ebc2bde..4b17f47 100644 --- a/src/StatsigNetwork.ts +++ b/src/StatsigNetwork.ts @@ -46,7 +46,7 @@ export default class StatsigNetwork { if (!this.sdkInternal.getOptions().getDisableNetworkKeepalive()) { try { this.canUseKeepalive = 'keepalive' in new Request(''); - } catch (_e) {} + } catch (_e) { } } } @@ -190,7 +190,7 @@ export default class StatsigNetwork { } const url = new URL( this.sdkInternal.getOptions().getEventLoggingApi() + - StatsigEndpoint.LogEventBeacon, + StatsigEndpoint.LogEventBeacon, ); url.searchParams.append('k', this.sdkInternal.getSDKKey()); payload.clientTime = Date.now() + ''; @@ -218,8 +218,8 @@ export default class StatsigNetwork { return Promise.reject('fetch is not defined'); } - if (typeof window === 'undefined') { - // dont issue requests from the server + if (typeof window === 'undefined' && !this.sdkInternal.getOptions().getIgnoreWindowUndefined()) { + // by default, dont issue requests from the server return Promise.reject('window is not defined'); } @@ -246,6 +246,7 @@ export default class StatsigNetwork { let shouldEncode = endpointName === StatsigEndpoint.Initialize && StatsigRuntime.encodeInitializeCall && + typeof window !== 'undefined' && typeof window?.btoa === 'function'; let postBody = JSON.stringify(body); diff --git a/src/StatsigSDKOptions.ts b/src/StatsigSDKOptions.ts index 8cd091e..d9347ca 100644 --- a/src/StatsigSDKOptions.ts +++ b/src/StatsigSDKOptions.ts @@ -34,6 +34,7 @@ export type StatsigOptions = { initCompletionCallback?: InitCompletionCallback | null; disableDiagnosticsLogging?: boolean; logLevel?: LogLevel | null; + ignoreWindowUndefined?: boolean; }; export enum LogLevel { @@ -67,6 +68,7 @@ export default class StatsigSDKOptions { private initCompletionCallback: InitCompletionCallback | null; private disableDiagnosticsLogging: boolean; private logLevel: LogLevel; + private ignoreWindowUndefined: boolean; constructor(options?: StatsigOptions | null) { if (options == null) { @@ -113,6 +115,7 @@ export default class StatsigSDKOptions { this.initCompletionCallback = options.initCompletionCallback ?? null; this.disableDiagnosticsLogging = options.disableDiagnosticsLogging ?? false; this.logLevel = options?.logLevel ?? LogLevel.NONE; + this.ignoreWindowUndefined = options?.ignoreWindowUndefined ?? false; } getApi(): string { @@ -183,6 +186,10 @@ export default class StatsigSDKOptions { return this.logLevel; } + getIgnoreWindowUndefined(): boolean { + return this.ignoreWindowUndefined; + } + private normalizeNumberInput( input: number | undefined, bounds: BoundedNumberInput, diff --git a/src/__tests__/StatsigClientOnServer.test.ts b/src/__tests__/StatsigClientOnServer.test.ts index d3ed673..b625108 100644 --- a/src/__tests__/StatsigClientOnServer.test.ts +++ b/src/__tests__/StatsigClientOnServer.test.ts @@ -32,4 +32,51 @@ describe('Verify behavior of StatsigClient outside of browser environment', () = await client.initializeAsync(); client.shutdown(); }); + + test('Client ignores window undefined if specified in options', async () => { + expect.assertions(8); + + // @ts-ignore + global.fetch = jest.fn((url, params) => { + if (url.toString().includes('rgstr')) { + return Promise.resolve({ ok: true }); + } + if (url.toString().includes('initialize')) { + return Promise.resolve({ + ok: true, + text: () => + Promise.resolve( + JSON.stringify(TestData), + ), + }); + } + }); + + // verify window is undefined + expect(typeof window).toBe('undefined'); + + const client = new StatsigClient( + 'client-xyz', + { email: 'tore@statsig.com' }, + { ignoreWindowUndefined: true }, + ); + + await client.initializeAsync(); + // flush interval is setup + // @ts-ignore + expect(client.getLogger().flushInterval).not.toBeNull(); + + // initialized from network (fetch mock) + expect(client.checkGate('test_gate')).toBe(false); + expect(client.checkGate('i_dont_exist')).toBe(false); + expect(client.checkGate('always_on_gate')).toBe(true); + expect(client.checkGate('on_for_statsig_email')).toBe(true); + expect(client.getConfig('test_config').get('number', 10)).toEqual(7); + expect(client.getConfig('test_config').getEvaluationDetails()).toEqual({ + reason: EvaluationReason.Network, + time: expect.any(Number), + }); + + client.shutdown(); + }); }); diff --git a/src/__tests__/basic_initialize_response.json b/src/__tests__/basic_initialize_response.json new file mode 100644 index 0000000..40e0906 --- /dev/null +++ b/src/__tests__/basic_initialize_response.json @@ -0,0 +1,46 @@ +{ + "disableAutoEventLogging": true, + "gates": { + "AoZS0F06Ub+W2ONx+94rPTS7MRxuxa+GnXro5Q1uaGY=": true + }, + "feature_gates": { + "AoZS0F06Ub+W2ONx+94rPTS7MRxuxa+GnXro5Q1uaGY=": { + "value": true, + "rule_id": "ruleID123", + "name": "AoZS0F06Ub+W2ONx+94rPTS7MRxuxa+GnXro5Q1uaGY=", + "secondary_exposures": [ + { + "gate": "dependent_gate_1", + "gateValue": "true", + "ruleID": "rule_1" + }, + { + "gate": "dependent_gate_2", + "gateValue": "false", + "ruleID": "default" + } + ] + } + }, + "dynamic_configs": { + "RMv0YJlLOBe7cY7HgZ3Jox34R0Wrk7jLv3DZyBETA7I=": { + "value": { + "bool": true, + "number": 2, + "string": "string", + "object": { + "key": "value", + "key2": 123 + }, + "boolStr1": "true", + "boolStr2": "FALSE", + "numberStr1": "3", + "numberStr2": "3.3", + "numberStr3": "3.3.3" + }, + "rule_id": "ruleID" + } + }, + "layer_configs": {}, + "has_updates": true +} diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index 6911b7c..e8ae754 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -6,6 +6,7 @@ import Statsig from '..'; import LogEvent from '../LogEvent'; import StatsigClient from '../StatsigClient'; import { EvaluationReason } from '../StatsigStore'; +import * as TestData from './basic_initialize_response.json'; let statsig: typeof Statsig; export type StatsigInitializeResponse = { @@ -38,52 +39,7 @@ describe('Verify behavior of top level index functions', () => { ok: true, text: () => Promise.resolve( - JSON.stringify({ - disableAutoEventLogging: true, - gates: { - 'AoZS0F06Ub+W2ONx+94rPTS7MRxuxa+GnXro5Q1uaGY=': true, - }, - feature_gates: { - 'AoZS0F06Ub+W2ONx+94rPTS7MRxuxa+GnXro5Q1uaGY=': { - value: true, - rule_id: 'ruleID123', - name: 'AoZS0F06Ub+W2ONx+94rPTS7MRxuxa+GnXro5Q1uaGY=', - secondary_exposures: [ - { - gate: 'dependent_gate_1', - gateValue: 'true', - ruleID: 'rule_1', - }, - { - gate: 'dependent_gate_2', - gateValue: 'false', - ruleID: 'default', - }, - ], - }, - }, - dynamic_configs: { - 'RMv0YJlLOBe7cY7HgZ3Jox34R0Wrk7jLv3DZyBETA7I=': { - value: { - bool: true, - number: 2, - string: 'string', - object: { - key: 'value', - key2: 123, - }, - boolStr1: 'true', - boolStr2: 'FALSE', - numberStr1: '3', - numberStr2: '3.3', - numberStr3: '3.3.3', - }, - rule_id: 'ruleID', - }, - }, - layer_configs: {}, - has_updates: true, - }), + JSON.stringify(TestData), ), }); }