diff --git a/src/index.spec.ts b/src/index.spec.ts index 1f9bb4d..afa3a82 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -11,6 +11,7 @@ import { } 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'; @@ -27,6 +28,8 @@ import { import { getInstance, IAssignmentEvent, IAssignmentLogger, init } from '.'; +import SpyInstance = jest.SpyInstance; + const { DEFAULT_POLL_INTERVAL_MS, POLL_JITTER_PCT } = constants; const apiKey = 'zCsQuoHJxVPp895.ZWg9MTIzNDU2LmUudGVzdGluZy5lcHBvLmNsb3Vk'; @@ -313,6 +316,90 @@ describe('EppoClient E2E test', () => { // 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 = { + 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 = { + 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 = { + nike: { brand_affinity: 1.0, loyalty_tier: 'gold' }, + adidas: { brand_affinity: 1.0, loyalty_tier: 'silver' }, + puma: {}, + }; + + let banditLoggerSpy: SpyInstance; + 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()) { + 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', () => {