Skip to content

Commit

Permalink
Return null for initializevalues when initialize fails (#139)
Browse files Browse the repository at this point in the history
* return null for initializevalues when initialize fails

* Use specstore update time

* feedback
  • Loading branch information
tore-statsig authored Mar 25, 2022
1 parent bc71106 commit a440fbc
Show file tree
Hide file tree
Showing 9 changed files with 149 additions and 4 deletions.
9 changes: 9 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,15 @@ declare module 'statsig-node' {
*/
export function shutdown(): void;

/**
* Returns the initialize values for the given user
* Can be used to bootstrap a client SDK with up to date values
* @param user the user to evaluate configurations for
*/
export function getClientInitializeResponse(
user: StatsigUser,
): Record<string, unknown> | null;

/**
* Overrides the given gate with the provided value
* If no userID is provided, it will override for all users
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "statsig-node",
"version": "4.12.0",
"version": "4.12.0-beta.5",
"description": "Statsig Node.js SDK for usage in multi-user server environments.",
"main": "dist/src/index.js",
"scripts": {
Expand Down
3 changes: 3 additions & 0 deletions src/Evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ const Evaluator = {
* @returns {Record<string, unknown>}
*/
getClientInitializeResponse(user) {
if (!SpecStore.isServingChecks()) {
return null;
}
const gates = Object.entries(SpecStore.store.gates)
.map(([gate, spec]) => {
if (spec.entity === 'segment') {
Expand Down
8 changes: 7 additions & 1 deletion src/SpecStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ const SpecStore = {
this.initialized = true;
},

isServingChecks() {
return this.time !== 0;
},

async _syncValues() {
try {
const response = await fetcher.post(
Expand All @@ -69,7 +73,9 @@ const SpecStore = {
}
} catch (e) {
console.error(
`statsigSDK::sync> Failed while attempting to sync values: ${e.message ?? ''}`,
`statsigSDK::sync> Failed while attempting to sync values: ${
e?.message ?? ''
}`,
);
}

Expand Down
105 changes: 105 additions & 0 deletions src/__tests__/ClientInitializeResponseConsistency.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
const fs = require('fs');
const path = require('path');
const fetch = require('node-fetch');

let secret = process.env.test_api_key;
if (!secret) {
try {
secret = fs.readFileSync(
path.resolve(
__dirname,
'../../../ops/secrets/prod_keys/statsig-rulesets-eval-consistency-test-secret.key',
),
'utf8',
);
} catch {}
}

if (secret) {
describe('Verify e2e behavior consistency /initialize vs getClientInitializeResponse', () => {
beforeEach(() => {
jest.restoreAllMocks();
jest.resetModules();
});

['https://api.statsig.com/v1', 'https://latest.api.statsig.com/v1'].map(
(url) =>
test(`server and SDK evaluates gates to the same results on ${url}`, async () => {
await _validateInitializeConsistency(url);
}),
);
});
} else {
describe('fail for non employees', () => {
test('Intended failing test. Proceed with pull request unless you are a Statsig employee.', () => {
console.log(
'THIS TEST IS EXPECTED TO FAIL FOR NON-STATSIG EMPLOYEES! If this is the only test failing, please proceed to submit a pull request. If you are a Statsig employee, chat with jkw.',
);
expect(true).toBe(false);
});
});
}

async function _validateInitializeConsistency(api) {
expect.assertions(1);
const user = {
userID: '12345',
email: '[email protected]',
country: 'US',
custom: {
test: '123',
},
};
const response = await fetch(api + '/initialize', {
method: 'POST',
body: JSON.stringify({
user: user,
statsigMetadata: {
sdkType: 'consistency-test',
sessionID: 'x123',
},
}),
headers: {
'Content-type': 'application/json; charset=UTF-8',
'STATSIG-API-KEY': 'client-wlH3WMkysINMhMU8VrNBkbjrEr2JQrqgxKwDPOUosJK',
'STATSIG-CLIENT-TIME': Date.now(),
},
});
const testData = await response.json();
// for sake of comparison, normalize the initialize response
// drop unused fields, set the time to 0
testData.time = 0;

for (const topLevel in testData) {
for (const property in testData[topLevel]) {
const item = testData[topLevel][property];
if (item.secondary_exposures) {
item.secondary_exposures.map((item) => {
delete item.gate;
});
}
// TODO for full layers future proofing
delete item['explicit_parameters'];
delete item['is_in_layer'];
}
}

const statsig = require('../index');
await statsig.initialize(secret, { api: api });

const sdkInitializeResponse = statsig.getClientInitializeResponse(user);

for (const topLevel in sdkInitializeResponse) {
for (const property in sdkInitializeResponse[topLevel]) {
const item = sdkInitializeResponse[topLevel][property];
if (item.secondary_exposures) {
// initialize has these hashed, we are putting them in plain text
// exposure logging still works
item.secondary_exposures.map((item) => {
delete item.gate;
});
}
}
}
expect(sdkInitializeResponse).toEqual(testData);
}
2 changes: 1 addition & 1 deletion src/__tests__/RulesetsEvalConsistency.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ if (secret) {
);
});
} else {
describe('', () => {
describe('fail for non employees', () => {
test('Intended failing test. Proceed with pull request unless you are a Statsig employee.', () => {
console.log(
'THIS TEST IS EXPECTED TO FAIL FOR NON-STATSIG EMPLOYEES! If this is the only test failing, please proceed to submit a pull request. If you are a Statsig employee, chat with jkw.',
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/StatsigE2ETest.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,23 @@ describe('Verify e2e behavior of the SDK with mocked network', () => {
ok: true,
});
}
if (url.includes('get_id_lists')) {
postedLogs = JSON.parse(params.body);
return Promise.resolve({
ok: true,
json: () => Promise.resolve({}),
});
}
return Promise.reject();
});
});

test('Verify checkGate and exposure logs', async () => {
const statsig = require('../index');
await statsig.initialize('secret-123');
expect(statsig.getClientInitializeResponse(statsigUser)).toEqual(
INIT_RESPONSE,
);
const on = await statsig.checkGate(statsigUser, 'always_on_gate');
expect(on).toEqual(true);
const passingEmail = await statsig.checkGate(
Expand Down
11 changes: 11 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,17 @@ const statsig = {
Evaluator.shutdown();
},

getClientInitializeResponse(user) {
if (statsig._ready !== true) {
return Promise.reject(
new Error(
'statsigSDK::getClientInitializeResponse> Must call initialize() first.',
),
);
}
return Evaluator.getClientInitializeResponse(user);
},

overrideGate(gateName, value, userID = '') {
if (typeof value !== 'boolean') {
console.warn(
Expand Down
3 changes: 2 additions & 1 deletion src/utils/StatsigFetcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,11 @@ const fetcher = {
}
return Promise.resolve(res);
})
.catch(() => {
.catch((e) => {
if (retries > 0) {
return this._retry(url, sdkKey, body, retries, backoff);
}
return Promise.reject(e);
})
.finally(() => {
fetcher.leakyBucket[url] = Math.max(fetcher.leakyBucket[url] - 1, 0);
Expand Down

0 comments on commit a440fbc

Please sign in to comment.