diff --git a/src/StatsigOptions.ts b/src/StatsigOptions.ts index 13ca794..6b510f4 100644 --- a/src/StatsigOptions.ts +++ b/src/StatsigOptions.ts @@ -1,7 +1,6 @@ import { IDataAdapter } from './interfaces/IDataAdapter'; +import { STATSIG_API, STATSIG_CDN } from './utils/StatsigFetcher'; -const DEFAULT_API = 'https://statsigapi.net/v1'; -const DEFAULT_API_FOR_DOWNLOAD_CONFIG_SPECS = 'https://api.statsigcdn.com/v1'; const DEFAULT_RULESETS_SYNC_INTERVAL = 10 * 1000; const MIN_RULESETS_SYNC_INTERVAL = 5 * 1000; const DEFAULT_ID_LISTS_SYNC_INTERVAL = 60 * 1000; @@ -34,6 +33,7 @@ export type ExplicitStatsigOptions = { api: string; apiForDownloadConfigSpecs: string; apiForGetIdLists: string; + fallbackToStatsigAPI: boolean; bootstrapValues: string | null; environment: StatsigEnvironment | null; rulesUpdatedCallback: RulesUpdatedCallback | null; @@ -65,15 +65,16 @@ export function OptionsWithDefaults( ): ExplicitStatsigOptions { return { api: normalizeUrl( - getString(opts, 'api', DEFAULT_API) ?? DEFAULT_API, + getString(opts, 'api', STATSIG_API) ?? STATSIG_API, ) as string, apiForDownloadConfigSpecs: normalizeUrl( getString(opts, 'apiForDownloadConfigSpecs', opts.api ?? null), - ) ?? DEFAULT_API_FOR_DOWNLOAD_CONFIG_SPECS, + ) ?? STATSIG_CDN, apiForGetIdLists: normalizeUrl(getString(opts, 'apiForGetIdLists', opts.api ?? null)) ?? - DEFAULT_API, + STATSIG_API, + fallbackToStatsigAPI: getBoolean(opts, 'fallbackToStatsigAPI', false), bootstrapValues: getString(opts, 'bootstrapValues', null), environment: opts.environment ? (getObject(opts, 'environment', {}) as StatsigEnvironment) diff --git a/src/__tests__/CustomDcsUrl.test.ts b/src/__tests__/CustomDcsUrl.test.ts index 769de5a..4bb1d8d 100644 --- a/src/__tests__/CustomDcsUrl.test.ts +++ b/src/__tests__/CustomDcsUrl.test.ts @@ -22,9 +22,9 @@ describe('Check custom DCS url', () => { disableDiagnostics: true, }); const secretKey = 'secret-123'; - const errorBoundary = new ErrorBoundary(secretKey, options, "sessionid-1") + const errorBoundary = new ErrorBoundary(secretKey, options, 'sessionid-1'); const fetcher = new StatsigFetcher(secretKey, options); - const logger = new LogEventProcessor(fetcher,errorBoundary, options); + const logger = new LogEventProcessor(fetcher, errorBoundary, options); const store = new SpecStore(fetcher, options); Diagnostics.initialize({ logger }); @@ -37,7 +37,12 @@ describe('Check custom DCS url', () => { logger.log(new LogEvent('test')); await logger.flush(); - expect(spy).toHaveBeenCalledWith('GET', customUrl + dcsPath + `/${secretKey}.json?sinceTime=0`, undefined); + expect(spy).toHaveBeenCalledWith( + 'GET', + customUrl + dcsPath + `/${secretKey}.json?sinceTime=0`, + undefined, + undefined, + ); expect(spy).not.toHaveBeenCalledWith( 'POST', customUrl + '/get_id_lists', diff --git a/src/utils/StatsigFetcher.ts b/src/utils/StatsigFetcher.ts index 20455e8..5b7fe05 100644 --- a/src/utils/StatsigFetcher.ts +++ b/src/utils/StatsigFetcher.ts @@ -14,9 +14,12 @@ import safeFetch from './safeFetch'; import StatsigContext from './StatsigContext'; const retryStatusCodes = [408, 500, 502, 503, 504, 522, 524, 599]; +export const STATSIG_API = 'https://statsigapi.net/v1'; +export const STATSIG_CDN = 'https://api.statsigcdn.com/v1'; type RequestOptions = Partial<{ retries: number; + retryURL: string; backoff: number | RetryBackoffFunc; isRetrying: boolean; signal: AbortSignal; @@ -25,9 +28,9 @@ type RequestOptions = Partial<{ }>; export default class StatsigFetcher { - private api: string; private apiForDownloadConfigSpecs: string; private apiForGetIdLists: string; + private fallbackToStatsigAPI: boolean; private sessionID: string; private leakyBucket: Record; private pendingTimers: NodeJS.Timer[]; @@ -42,9 +45,9 @@ export default class StatsigFetcher { errorBoundry: ErrorBoundary, sessionID: string, ) { - this.api = options.api; this.apiForDownloadConfigSpecs = options.apiForDownloadConfigSpecs; this.apiForGetIdLists = options.apiForGetIdLists; + this.fallbackToStatsigAPI = options.fallbackToStatsigAPI; this.sessionID = sessionID; this.leakyBucket = {}; this.pendingTimers = []; @@ -66,16 +69,30 @@ export default class StatsigFetcher { } public async downloadConfigSpecs(sinceTime: number): Promise { - return await this.get( - this.apiForDownloadConfigSpecs + - '/download_config_specs' + - `/${this.sdkKey}.json` + - `?sinceTime=${sinceTime}`, - ); + const path = + '/download_config_specs' + + `/${this.sdkKey}.json` + + `?sinceTime=${sinceTime}`; + const url = this.apiForDownloadConfigSpecs + path; + + let options: RequestOptions | undefined; + if (this.fallbackToStatsigAPI) { + options = { retries: 1, retryURL: STATSIG_CDN + path }; + } + + return await this.get(url, options); } - public async getIDLists(sinceTime?: number): Promise { - return await this.post(this.apiForGetIdLists + '/get_id_lists', {}); + public async getIDLists(): Promise { + const path = '/get_id_lists'; + const url = this.apiForGetIdLists + path; + + let options: RequestOptions | undefined; + if (this.fallbackToStatsigAPI) { + options = { retries: 1, retryURL: STATSIG_API + path }; + } + + return await this.post(url, {}, options); } public dispatch( @@ -95,7 +112,7 @@ export default class StatsigFetcher { } public async get(url: string, options?: RequestOptions): Promise { - return await this.request('GET', url, options); + return await this.request('GET', url, undefined, options); } public async request( @@ -105,6 +122,7 @@ export default class StatsigFetcher { options?: RequestOptions, ): Promise { const { + retryURL = url, retries = 0, backoff = 1000, isRetrying = false, @@ -172,7 +190,13 @@ export default class StatsigFetcher { .then((localRes) => { res = localRes; if ((!res.ok || retryStatusCodes.includes(res.status)) && retries > 0) { - return this._retry(method, url, body, retries - 1, backoffAdjusted); + return this._retry( + method, + retryURL, + body, + retries - 1, + backoffAdjusted, + ); } else if (!res.ok) { return Promise.reject( new Error(