Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
} from '@eppo/js-client-sdk-common';
import { BanditParameters, BanditVariation } from '@eppo/js-client-sdk-common/dist/interfaces';
import { ContextAttributes } from '@eppo/js-client-sdk-common/dist/types';
import { Attributes } from '@eppo/js-client-sdk-common/src/types';
import * as td from 'testdouble';

import apiServer, { TEST_BANDIT_API_KEY, TEST_SERVER_PORT } from '../test/mockApiServer';
Expand All @@ -27,6 +28,8 @@

import { getInstance, IAssignmentEvent, IAssignmentLogger, init } from '.';

import SpyInstance = jest.SpyInstance;

const { DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } = constants;

const apiKey = 'zCsQuoHJxVPp895.ZWg9MTIzNDU2LmUudGVzdGluZy5lcHBvLmNsb3Vk';
Expand Down Expand Up @@ -313,6 +316,90 @@
// Ensure that this test case correctly checked some test assignments
expect(numAssignmentsChecked).toBeGreaterThan(0);
});

describe('Bandit assignment cache', () => {
const flagKey = 'banner_bandit_flag'; // piggyback off a shared test data flag
const bobKey = 'bob';
const bobAttributes: Attributes = { age: 25, country: 'USA', gender_identity: 'female' };
const bobActions: Record<string, Attributes> = {
nike: { brand_affinity: 1.5, loyalty_tier: 'silver' },
adidas: { brand_affinity: -1.0, loyalty_tier: 'bronze' },
reebok: { brand_affinity: 0.5, loyalty_tier: 'gold' },
};

const aliceKey = 'alice';
const aliceAttributes: Attributes = { age: 25, country: 'USA', gender_identity: 'female' };
const aliceActions: Record<string, Attributes> = {
nike: { brand_affinity: 1.5, loyalty_tier: 'silver' },
adidas: { brand_affinity: -1.0, loyalty_tier: 'bronze' },
reebok: { brand_affinity: 0.5, loyalty_tier: 'gold' },
};
const charlieKey = 'charlie';
const charlieAttributes: Attributes = { age: 25, country: 'USA', gender_identity: 'female' };
const charlieActions: Record<string, Attributes> = {
nike: { brand_affinity: 1.0, loyalty_tier: 'gold' },
adidas: { brand_affinity: 1.0, loyalty_tier: 'silver' },
puma: {},
};

let banditLoggerSpy: SpyInstance<void, [banditEvent: IBanditEvent]>;
const defaultBanditCacheTTL = 600_000;

beforeAll(async () => {
const dummyBanditLogger: IBanditLogger = {
logBanditAction(banditEvent: IBanditEvent) {
console.log(
`Bandit ${banditEvent.bandit} assigned ${banditEvent.subject} the action ${banditEvent.action}`,
);
},
};
banditLoggerSpy = jest.spyOn(dummyBanditLogger, 'logBanditAction');
await init({
apiKey: TEST_BANDIT_API_KEY, // Flag to dummy test server we want bandit-related files
baseUrl: `http://127.0.0.1:${TEST_SERVER_PORT}`,
assignmentLogger: mockLogger,
banditLogger: dummyBanditLogger,
});
});

it('Should not log bandit assignment if cached version is still valid', async () => {
const client = getInstance();
client.useExpiringInMemoryBanditAssignmentCache(2);

// Let's say someone is rage refreshing - we want to log assignment only once
for (const _ of Array(3).keys()) {

Check warning on line 370 in src/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (18)

'_' is assigned a value but never used

Check warning on line 370 in src/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (20)

'_' is assigned a value but never used

Check warning on line 370 in src/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (22)

'_' is assigned a value but never used

Check warning on line 370 in src/index.spec.ts

View workflow job for this annotation

GitHub Actions / lint-test-sdk (23)

'_' is assigned a value but never used
client.getBanditAction(flagKey, bobKey, bobAttributes, bobActions, 'default');
}
expect(banditLoggerSpy).toHaveBeenCalledTimes(1);
});
it('Should log bandit assignment if cached entry is expired', async () => {
jest.useFakeTimers();
banditLoggerSpy.mockReset();

const client = getInstance();
client.useExpiringInMemoryBanditAssignmentCache(2);

client.getBanditAction(flagKey, bobKey, bobAttributes, bobActions, 'default');
jest.advanceTimersByTime(defaultBanditCacheTTL);
client.getBanditAction(flagKey, bobKey, bobAttributes, bobActions, 'default');
expect(banditLoggerSpy).toHaveBeenCalledTimes(2);
});

it('Should invalidate least used cache entry if cache reaches max size', async () => {
banditLoggerSpy.mockReset();
const client = getInstance();
client.useExpiringInMemoryBanditAssignmentCache(2);

client.getBanditAction(flagKey, bobKey, bobAttributes, bobActions, 'default');
client.getBanditAction(flagKey, aliceKey, aliceAttributes, aliceActions, 'default');
client.getBanditAction(flagKey, charlieKey, charlieAttributes, charlieActions, 'default');
// even though bob was called 2nd time within cache validity time
// we expect assignment to be logged because max cache size is 2
// and currently storing alice and charlie
client.getBanditAction(flagKey, bobKey, bobAttributes, bobActions, 'default');
expect(banditLoggerSpy).toHaveBeenCalledTimes(4);
});
});
});

describe('initialization errors', () => {
Expand Down