Skip to content

Commit

Permalink
fix: ensure environment is set on prefetch (#263)
Browse files Browse the repository at this point in the history
  • Loading branch information
daniel-statsig authored Jul 15, 2024
1 parent 5901129 commit 35cc6fd
Show file tree
Hide file tree
Showing 8 changed files with 178 additions and 45 deletions.
29 changes: 18 additions & 11 deletions packages/client-core/src/DataAdapterCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
DataSource,
} from './StatsigDataAdapter';
import { AnyStatsigOptions } from './StatsigOptionsCommon';
import { StatsigUser, _getFullUserHash, _normalizeUser } from './StatsigUser';
import {
StatsigUser,
StatsigUserInternal,
_getFullUserHash,
_normalizeUser,
} from './StatsigUser';
import {
Storage,
_getObjectFromStorage,
Expand Down Expand Up @@ -37,8 +42,9 @@ export abstract class DataAdapterCore {
}

getDataSync(user?: StatsigUser | undefined): DataAdapterResult | null {
const cacheKey = this._getCacheKey(user);
const inMem = this._inMemoryCache.get(cacheKey, user);
const normalized = user && _normalizeUser(user, this._options);
const cacheKey = this._getCacheKey(normalized);
const inMem = this._inMemoryCache.get(cacheKey, normalized);

if (inMem) {
return inMem;
Expand All @@ -47,14 +53,14 @@ export abstract class DataAdapterCore {
const cache = this._loadFromCache(cacheKey);
if (cache) {
this._inMemoryCache.add(cacheKey, cache);
return this._inMemoryCache.get(cacheKey, user);
return this._inMemoryCache.get(cacheKey, normalized);
}

return null;
}

setData(data: string, user?: StatsigUser): void {
const normalized = user && _normalizeUser(user, this._options?.environment);
const normalized = user && _normalizeUser(user, this._options);
const cacheKey = this._getCacheKey(normalized);

this._inMemoryCache.add(
Expand All @@ -74,7 +80,7 @@ export abstract class DataAdapterCore {

protected async _getDataAsyncImpl(
current: DataAdapterResult | null,
user?: StatsigUser,
user?: StatsigUserInternal,
options?: DataAdapterAsyncOptions,
): Promise<DataAdapterResult | null> {
const cache = current ?? this.getDataSync(user);
Expand All @@ -97,8 +103,9 @@ export abstract class DataAdapterCore {
user?: StatsigUser,
options?: DataAdapterAsyncOptions,
): Promise<void> {
const cacheKey = this._getCacheKey(user);
const result = await this._getDataAsyncImpl(null, user, options);
const normalized = user && _normalizeUser(user, this._options);
const cacheKey = this._getCacheKey(normalized);
const result = await this._getDataAsyncImpl(null, normalized, options);
if (result) {
this._inMemoryCache.add(cacheKey, { ...result, source: 'Prefetch' });
}
Expand All @@ -110,7 +117,7 @@ export abstract class DataAdapterCore {
options?: DataAdapterAsyncOptions,
): Promise<string | null>;

protected abstract _getCacheKey(user?: StatsigUser): string;
protected abstract _getCacheKey(user?: StatsigUserInternal): string;

protected abstract _isCachedResultValidFor204(
result: DataAdapterResult,
Expand All @@ -119,7 +126,7 @@ export abstract class DataAdapterCore {

private async _fetchAndPrepFromNetwork(
cachedResult: DataAdapterResult | null,
user: StatsigUser | undefined,
user: StatsigUserInternal | undefined,
options: DataAdapterAsyncOptions | undefined,
): Promise<DataAdapterResult | null> {
let cachedData: string | null = null;
Expand Down Expand Up @@ -232,7 +239,7 @@ class InMemoryCache {

get(
cacheKey: string,
user: StatsigUser | undefined,
user: StatsigUserInternal | undefined,
): DataAdapterResult | null {
const result = this._data[cacheKey];
const cached = result?.stableID;
Expand Down
2 changes: 1 addition & 1 deletion packages/client-core/src/EventLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export class EventLogger {
return true;
}

const user = event.user ? event.user : {};
const user = event.user ? event.user : { statsigEnvironment: undefined };
const metadata = event.metadata ? event.metadata : {};
const key = [
event.eventName,
Expand Down
12 changes: 6 additions & 6 deletions packages/client-core/src/StatsigEvent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EvaluationDetails, SecondaryExposure } from './EvaluationTypes';
import { DynamicConfig, FeatureGate, Layer } from './StatsigTypes';
import { StatsigUser } from './StatsigUser';
import { StatsigUserInternal } from './StatsigUser';

export type StatsigEvent = {
eventName: string;
Expand All @@ -9,7 +9,7 @@ export type StatsigEvent = {
};

export type StatsigEventInternal = Omit<StatsigEvent, 'metadata'> & {
user: StatsigUser | null;
user: StatsigUserInternal | null;
time: number;
metadata?: { [key: string]: unknown } | null;
secondaryExposures?: SecondaryExposure[];
Expand All @@ -21,7 +21,7 @@ const LAYER_EXPOSURE_NAME = 'statsig::layer_exposure';

const _createExposure = (
eventName: string,
user: StatsigUser,
user: StatsigUserInternal,
details: EvaluationDetails,
metadata: Record<string, string>,
secondaryExposures: SecondaryExposure[],
Expand All @@ -43,7 +43,7 @@ export const _isExposureEvent = ({
};

export const _createGateExposure = (
user: StatsigUser,
user: StatsigUserInternal,
gate: FeatureGate,
): StatsigEventInternal => {
return _createExposure(
Expand All @@ -60,7 +60,7 @@ export const _createGateExposure = (
};

export const _createConfigExposure = (
user: StatsigUser,
user: StatsigUserInternal,
config: DynamicConfig,
): StatsigEventInternal => {
return _createExposure(
Expand All @@ -76,7 +76,7 @@ export const _createConfigExposure = (
};

export const _createLayerParameterExposure = (
user: StatsigUser,
user: StatsigUserInternal,
layer: Layer,
parameterName: string,
): StatsigEventInternal => {
Expand Down
17 changes: 10 additions & 7 deletions packages/client-core/src/StatsigUser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { _DJB2Object } from './Hashing';
import { Log } from './Log';
import type { StatsigEnvironment } from './StatsigOptionsCommon';
import type {
AnyStatsigOptions,
StatsigEnvironment,
} from './StatsigOptionsCommon';

type StatsigUserPrimitives =
| string
Expand All @@ -26,24 +29,24 @@ export type StatsigUser = {
};

export type StatsigUserInternal = StatsigUser & {
statsigEnvironment?: StatsigEnvironment;
statsigEnvironment: StatsigEnvironment | undefined;
};

export function _normalizeUser(
original: StatsigUser,
environment?: StatsigEnvironment,
): StatsigUser {
options?: AnyStatsigOptions | null,
): StatsigUserInternal {
try {
const copy = JSON.parse(JSON.stringify(original)) as StatsigUserInternal;

if (environment != null) {
copy.statsigEnvironment = environment;
if (options != null && options.environment != null) {
copy.statsigEnvironment = options.environment;
}

return copy;
} catch (error) {
Log.error('Failed to JSON.stringify user');
return {};
return { statsigEnvironment: undefined };
}
}

Expand Down
7 changes: 4 additions & 3 deletions packages/js-client/src/StatsigClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
StatsigEvent,
StatsigSession,
StatsigUser,
StatsigUserInternal,
_createConfigExposure,
_createGateExposure,
_createLayerParameterExposure,
Expand All @@ -48,7 +49,7 @@ export default class StatsigClient
implements PrecomputedEvaluationsInterface
{
private _store: EvaluationStore;
private _user: StatsigUser;
private _user: StatsigUserInternal;

/**
* Retrieves an instance of the StatsigClient based on the provided SDK key.
Expand Down Expand Up @@ -97,7 +98,7 @@ export default class StatsigClient
);

this._store = new EvaluationStore();
this._user = user;
this._user = _normalizeUser(user, options);
}

/**
Expand Down Expand Up @@ -411,7 +412,7 @@ export default class StatsigClient
this._logger.reset();
this._store.reset();

this._user = _normalizeUser(user, this._options.environment);
this._user = _normalizeUser(user, this._options);

const stableIdOverride = this._user.customIDs?.stableID;
if (stableIdOverride) {
Expand Down
10 changes: 8 additions & 2 deletions packages/js-client/src/StatsigEvaluationsDataAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
InitializeResponse,
Log,
StatsigUser,
StatsigUserInternal,
_getFullUserHash,
_getStorageKey,
_normalizeUser,
_typedJsonParse,
} from '@statsig/client-core';

Expand Down Expand Up @@ -36,7 +38,11 @@ export class StatsigEvaluationsDataAdapter
user: StatsigUser,
options?: DataAdapterAsyncOptions,
): Promise<DataAdapterResult | null> {
return this._getDataAsyncImpl(current, user, options);
return this._getDataAsyncImpl(
current,
_normalizeUser(user, this._options),
options,
);
}

prefetchData(
Expand Down Expand Up @@ -78,7 +84,7 @@ export class StatsigEvaluationsDataAdapter
return result ?? null;
}

protected override _getCacheKey(user?: StatsigUser): string {
protected override _getCacheKey(user?: StatsigUserInternal): string {
const key = _getStorageKey(
this._getSdkKey(),
user,
Expand Down
92 changes: 92 additions & 0 deletions packages/js-client/src/__tests__/ClientAndEvironments.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import fetchMock from 'jest-fetch-mock';
import { InitResponseString, MockLocalStorage } from 'statsig-test-helpers';

import StatsigClient from '../StatsigClient';

describe('StatsigClient and Environments', () => {
const user = { userID: 'a-user' };
const env = { statsigEnvironment: { tier: 'dev' } };
const expectedCacheKey = 'statsig.cached.evaluations.1769418430'; // DJB2(JSON({userID: 'a-user', statsigEnvironment: {tier: 'dev'}}))

let storageMock: MockLocalStorage;
let client: StatsigClient;

beforeAll(() => {
storageMock = MockLocalStorage.enabledMockStorage();
fetchMock.enableMocks();
});

afterAll(() => {
jest.clearAllMocks();
MockLocalStorage.disableMockStorage();
});

beforeEach(() => {
storageMock.clear();

fetchMock.mock.calls = [];
fetchMock.mockResponse(InitResponseString);

client = new StatsigClient('client-key', user, {
environment: { tier: 'dev' },
disableStatsigEncoding: true,
});
});

describe('When triggered by StatsigClient', () => {
it('sets the environment on post sync init requests', async () => {
client.initializeSync();
await new Promise((r) => setTimeout(r, 1));

const [, req] = fetchMock.mock.calls[0];
const body = JSON.parse(String(req?.body));

expect(body.user).toMatchObject(env);
expect(storageMock.data[expectedCacheKey]).toBeDefined();
});

it('sets the environment on async init requests', async () => {
await client.initializeAsync();

const [, req] = fetchMock.mock.calls[0];
const body = JSON.parse(String(req?.body));

expect(body.user).toMatchObject(env);
expect(storageMock.data[expectedCacheKey]).toBeDefined();
});
});

describe('When triggered by DataAdapter', () => {
it('sets the environment on prefetch requests', async () => {
await client.dataAdapter.prefetchData(user);

const [, req] = fetchMock.mock.calls[0];
const body = JSON.parse(String(req?.body));

expect(body.user).toMatchObject(env);
expect(storageMock.data[expectedCacheKey]).toBeDefined();
});

it('sets the environment on getDataAsync requests', async () => {
await client.dataAdapter.getDataAsync(null, user, {});

const [, req] = fetchMock.mock.calls[0];
const body = JSON.parse(String(req?.body));

expect(body.user).toMatchObject(env);
expect(storageMock.data[expectedCacheKey]).toBeDefined();
});
});

it('includes env on DataAdapter reads', () => {
storageMock.data[expectedCacheKey] = JSON.stringify({
source: 'Network',
receivedAt: Date.now(),
data: InitResponseString,
stableID: null,
fullUserHash: null,
});

expect(client.dataAdapter.getDataSync(user)).toBeDefined();
});
});
Loading

0 comments on commit 35cc6fd

Please sign in to comment.