From e5597185813c77b1c07b322228a748f678f630d8 Mon Sep 17 00:00:00 2001 From: Mateusz Kwasniewski Date: Mon, 13 Jan 2025 11:56:57 +0100 Subject: [PATCH] feat: unique connection counting (#9074) --- package.json | 1 + src/lib/db/index.ts | 2 + .../features/scheduler/schedule-services.ts | 7 + .../fake-unique-connection-store.ts | 27 +++ .../unique-connection-service.test.ts | 169 ++++++++++++++++++ .../unique-connection-service.ts | 99 ++++++++++ .../unique-connection-store-type.ts | 14 ++ .../unique-connection-store.e2e.test.ts | 58 ++++++ .../unique-connection-store.ts | 34 ++++ src/lib/metric-events.ts | 3 + .../middleware/response-time-metrics.test.ts | 5 + src/lib/middleware/response-time-metrics.ts | 7 +- src/lib/services/index.ts | 6 + src/lib/types/services.ts | 2 + src/lib/types/stores.ts | 3 + src/server-dev.ts | 1 + src/test/fixtures/store.ts | 2 + yarn.lock | 17 ++ 18 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 src/lib/features/unique-connection/fake-unique-connection-store.ts create mode 100644 src/lib/features/unique-connection/unique-connection-service.test.ts create mode 100644 src/lib/features/unique-connection/unique-connection-service.ts create mode 100644 src/lib/features/unique-connection/unique-connection-store-type.ts create mode 100644 src/lib/features/unique-connection/unique-connection-store.e2e.test.ts create mode 100644 src/lib/features/unique-connection/unique-connection-store.ts diff --git a/package.json b/package.json index ad39e457d898..bbe056855242 100644 --- a/package.json +++ b/package.json @@ -134,6 +134,7 @@ "hash-sum": "^2.0.0", "helmet": "^6.0.0", "http-errors": "^2.0.0", + "hyperloglog-lite": "^1.0.2", "ip-address": "^10.0.1", "joi": "^17.13.3", "js-sha256": "^0.11.0", diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 4657547a42df..6d2d41e4e0b4 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -56,6 +56,7 @@ import { OnboardingStore } from '../features/onboarding/onboarding-store'; import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel'; import { UserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store'; import { UserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model'; +import { UniqueConnectionStore } from '../features/unique-connection/unique-connection-store'; export const createStores = ( config: IUnleashConfig, @@ -185,6 +186,7 @@ export const createStores = ( ), userUnsubscribeStore: new UserUnsubscribeStore(db), userSubscriptionsReadModel: new UserSubscriptionsReadModel(db), + uniqueConnectionStore: new UniqueConnectionStore(db), }; }; diff --git a/src/lib/features/scheduler/schedule-services.ts b/src/lib/features/scheduler/schedule-services.ts index ad5a93bbe7ac..99d373bc29e2 100644 --- a/src/lib/features/scheduler/schedule-services.ts +++ b/src/lib/features/scheduler/schedule-services.ts @@ -32,6 +32,7 @@ export const scheduleServices = async ( frontendApiService, clientMetricsServiceV2, integrationEventsService, + uniqueConnectionService, } = services; schedulerService.schedule( @@ -179,4 +180,10 @@ export const scheduleServices = async ( minutesToMilliseconds(15), 'cleanUpIntegrationEvents', ); + + schedulerService.schedule( + uniqueConnectionService.sync.bind(uniqueConnectionService), + minutesToMilliseconds(10), + 'uniqueConnectionService', + ); }; diff --git a/src/lib/features/unique-connection/fake-unique-connection-store.ts b/src/lib/features/unique-connection/fake-unique-connection-store.ts new file mode 100644 index 000000000000..cad87bc22710 --- /dev/null +++ b/src/lib/features/unique-connection/fake-unique-connection-store.ts @@ -0,0 +1,27 @@ +import type { IUniqueConnectionStore } from '../../types'; +import type { + TimedUniqueConnections, + UniqueConnections, +} from './unique-connection-store-type'; + +export class FakeUniqueConnectionStore implements IUniqueConnectionStore { + private uniqueConnectionsRecord: Record = + {}; + + async insert(uniqueConnections: UniqueConnections): Promise { + this.uniqueConnectionsRecord[uniqueConnections.id] = { + ...uniqueConnections, + updatedAt: new Date(), + }; + } + + async get( + id: 'current' | 'previous', + ): Promise<(UniqueConnections & { updatedAt: Date }) | null> { + return this.uniqueConnectionsRecord[id] || null; + } + + async deleteAll(): Promise { + this.uniqueConnectionsRecord = {}; + } +} diff --git a/src/lib/features/unique-connection/unique-connection-service.test.ts b/src/lib/features/unique-connection/unique-connection-service.test.ts new file mode 100644 index 000000000000..6ab0676ffad2 --- /dev/null +++ b/src/lib/features/unique-connection/unique-connection-service.test.ts @@ -0,0 +1,169 @@ +import { UniqueConnectionService } from './unique-connection-service'; +import { FakeUniqueConnectionStore } from './fake-unique-connection-store'; +import getLogger from '../../../test/fixtures/no-logger'; +import type { IFlagResolver } from '../../types'; +import { SDK_CONNECTION_ID_RECEIVED } from '../../metric-events'; +import { addHours } from 'date-fns'; +import EventEmitter from 'events'; + +const alwaysOnFlagResolver = { + isEnabled() { + return true; + }, +} as unknown as IFlagResolver; + +test('sync first current bucket', async () => { + const eventBus = new EventEmitter(); + const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus }; + const uniqueConnectionStore = new FakeUniqueConnectionStore(); + const uniqueConnectionService = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + uniqueConnectionService.listen(); + + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection1'); + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection1'); + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2'); + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2'); + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2'); + + await uniqueConnectionService.sync(); + + const stats = await uniqueConnectionService.getStats(); + expect(stats).toEqual({ previous: 0, current: 2 }); +}); + +test('sync first previous bucket', async () => { + const eventBus = new EventEmitter(); + const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus }; + const uniqueConnectionStore = new FakeUniqueConnectionStore(); + const uniqueConnectionService = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + uniqueConnectionService.listen(); + + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection1'); + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection2'); + + await uniqueConnectionService.sync(); + + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, 'connection3'); + + await uniqueConnectionService.sync(addHours(new Date(), 1)); + + const stats = await uniqueConnectionService.getStats(); + expect(stats).toEqual({ previous: 3, current: 0 }); +}); + +test('sync to existing current bucket from the same service', async () => { + const eventBus = new EventEmitter(); + const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus }; + const uniqueConnectionStore = new FakeUniqueConnectionStore(); + const uniqueConnectionService = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + uniqueConnectionService.listen(); + + uniqueConnectionService.count('connection1'); + uniqueConnectionService.count('connection2'); + + await uniqueConnectionService.sync(); + + uniqueConnectionService.count('connection1'); + uniqueConnectionService.count('connection3'); + + const stats = await uniqueConnectionService.getStats(); + expect(stats).toEqual({ previous: 0, current: 3 }); +}); + +test('sync to existing current bucket from another service', async () => { + const eventBus = new EventEmitter(); + const config = { + flagResolver: alwaysOnFlagResolver, + getLogger, + eventBus: eventBus, + }; + const uniqueConnectionStore = new FakeUniqueConnectionStore(); + const uniqueConnectionService1 = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + const uniqueConnectionService2 = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + + uniqueConnectionService1.count('connection1'); + uniqueConnectionService1.count('connection2'); + await uniqueConnectionService1.sync(); + + uniqueConnectionService2.count('connection1'); + uniqueConnectionService2.count('connection3'); + await uniqueConnectionService2.sync(); + + const stats1 = await uniqueConnectionService1.getStats(); + expect(stats1).toEqual({ previous: 0, current: 3 }); + const stats2 = await uniqueConnectionService2.getStats(); + expect(stats2).toEqual({ previous: 0, current: 3 }); +}); + +test('sync to existing previous bucket from another service', async () => { + const eventBus = new EventEmitter(); + const config = { + flagResolver: alwaysOnFlagResolver, + getLogger, + eventBus: eventBus, + }; + const uniqueConnectionStore = new FakeUniqueConnectionStore(); + const uniqueConnectionService1 = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + const uniqueConnectionService2 = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + + uniqueConnectionService1.count('connection1'); + uniqueConnectionService1.count('connection2'); + await uniqueConnectionService1.sync(addHours(new Date(), 1)); + + uniqueConnectionService2.count('connection1'); + uniqueConnectionService2.count('connection3'); + await uniqueConnectionService2.sync(addHours(new Date(), 1)); + + const stats1 = await uniqueConnectionService1.getStats(); + expect(stats1).toEqual({ previous: 3, current: 0 }); + const stats2 = await uniqueConnectionService2.getStats(); + expect(stats2).toEqual({ previous: 3, current: 0 }); +}); + +test('populate previous and current', async () => { + const eventBus = new EventEmitter(); + const config = { flagResolver: alwaysOnFlagResolver, getLogger, eventBus }; + const uniqueConnectionStore = new FakeUniqueConnectionStore(); + const uniqueConnectionService = new UniqueConnectionService( + { uniqueConnectionStore }, + config, + ); + + uniqueConnectionService.count('connection1'); + uniqueConnectionService.count('connection2'); + await uniqueConnectionService.sync(); + await uniqueConnectionService.sync(); + + uniqueConnectionService.count('connection3'); + await uniqueConnectionService.sync(addHours(new Date(), 1)); + await uniqueConnectionService.sync(addHours(new Date(), 1)); // deliberate duplicate call + + uniqueConnectionService.count('connection3'); + uniqueConnectionService.count('connection4'); + await uniqueConnectionService.sync(addHours(new Date(), 1)); + await uniqueConnectionService.sync(addHours(new Date(), 1)); // deliberate duplicate call + + const stats = await uniqueConnectionService.getStats(); + expect(stats).toEqual({ previous: 3, current: 2 }); +}); diff --git a/src/lib/features/unique-connection/unique-connection-service.ts b/src/lib/features/unique-connection/unique-connection-service.ts new file mode 100644 index 000000000000..f49903bd45a6 --- /dev/null +++ b/src/lib/features/unique-connection/unique-connection-service.ts @@ -0,0 +1,99 @@ +import type { IUnleashConfig } from '../../types/option'; +import type { IFlagResolver, IUnleashStores } from '../../types'; +import type { Logger } from '../../logger'; +import type { IUniqueConnectionStore } from './unique-connection-store-type'; +import HyperLogLog from 'hyperloglog-lite'; +import type EventEmitter from 'events'; +import { SDK_CONNECTION_ID_RECEIVED } from '../../metric-events'; + +// HyperLogLog will create 2^n registers +const n = 12; + +export class UniqueConnectionService { + private logger: Logger; + + private uniqueConnectionStore: IUniqueConnectionStore; + + private flagResolver: IFlagResolver; + + private eventBus: EventEmitter; + + private activeHour: number; + + private hll = HyperLogLog(n); + + constructor( + { + uniqueConnectionStore, + }: Pick, + config: Pick, + ) { + this.uniqueConnectionStore = uniqueConnectionStore; + this.logger = config.getLogger('services/unique-connection-service.ts'); + this.flagResolver = config.flagResolver; + this.eventBus = config.eventBus; + this.activeHour = new Date().getHours(); + } + + listen() { + this.eventBus.on(SDK_CONNECTION_ID_RECEIVED, this.count.bind(this)); + } + + count(connectionId: string) { + if (!this.flagResolver.isEnabled('uniqueSdkTracking')) return; + this.hll.add(HyperLogLog.hash(connectionId)); + } + + async getStats() { + const [previous, current] = await Promise.all([ + this.uniqueConnectionStore.get('previous'), + this.uniqueConnectionStore.get('current'), + ]); + const previousHll = HyperLogLog(n); + if (previous) { + previousHll.merge({ n, buckets: previous.hll }); + } + const currentHll = HyperLogLog(n); + if (current) { + currentHll.merge({ n, buckets: current.hll }); + } + return { previous: previousHll.count(), current: currentHll.count() }; + } + + async sync(currentTime = new Date()): Promise { + if (!this.flagResolver.isEnabled('uniqueSdkTracking')) return; + + const currentHour = currentTime.getHours(); + const currentBucket = await this.uniqueConnectionStore.get('current'); + + if (this.activeHour !== currentHour && currentBucket) { + if (currentBucket.updatedAt.getHours() < currentHour) { + this.hll.merge({ n, buckets: currentBucket.hll }); + await this.uniqueConnectionStore.insert({ + hll: this.hll.output().buckets, + id: 'previous', + }); + } else { + const previousBucket = + await this.uniqueConnectionStore.get('previous'); + if (previousBucket) { + this.hll.merge({ n, buckets: previousBucket.hll }); + } + await this.uniqueConnectionStore.insert({ + hll: this.hll.output().buckets, + id: 'previous', + }); + } + + this.activeHour = currentHour; + this.hll = HyperLogLog(n); + } else if (currentBucket) { + this.hll.merge({ n, buckets: currentBucket.hll }); + } + + await this.uniqueConnectionStore.insert({ + hll: this.hll.output().buckets, + id: 'current', + }); + } +} diff --git a/src/lib/features/unique-connection/unique-connection-store-type.ts b/src/lib/features/unique-connection/unique-connection-store-type.ts new file mode 100644 index 000000000000..e691a794f82a --- /dev/null +++ b/src/lib/features/unique-connection/unique-connection-store-type.ts @@ -0,0 +1,14 @@ +export type UniqueConnections = { + hll: Buffer; + id: 'current' | 'previous'; +}; + +export type TimedUniqueConnections = UniqueConnections & { + updatedAt: Date; +}; + +export interface IUniqueConnectionStore { + insert(uniqueConnections: UniqueConnections): Promise; + get(id: 'current' | 'previous'): Promise; + deleteAll(): Promise; +} diff --git a/src/lib/features/unique-connection/unique-connection-store.e2e.test.ts b/src/lib/features/unique-connection/unique-connection-store.e2e.test.ts new file mode 100644 index 000000000000..29fca9e6914e --- /dev/null +++ b/src/lib/features/unique-connection/unique-connection-store.e2e.test.ts @@ -0,0 +1,58 @@ +import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; +import getLogger from '../../../test/fixtures/no-logger'; +import type { + IUniqueConnectionStore, + IUnleashStores, +} from '../../../lib/types'; +import HyperLogLog from 'hyperloglog-lite'; + +let stores: IUnleashStores; +let db: ITestDb; +let uniqueConnectionStore: IUniqueConnectionStore; + +beforeAll(async () => { + db = await dbInit('unique_connections_store', getLogger); + stores = db.stores; + uniqueConnectionStore = stores.uniqueConnectionStore; +}); + +afterAll(async () => { + await db.destroy(); +}); + +beforeEach(async () => { + await uniqueConnectionStore.deleteAll(); +}); + +test('should store empty HyperLogLog buffer', async () => { + const hll = HyperLogLog(12); + await uniqueConnectionStore.insert({ + id: 'current', + hll: hll.output().buckets, + }); + + const fetchedHll = await uniqueConnectionStore.get('current'); + hll.merge({ n: 12, buckets: fetchedHll!.hll }); + expect(hll.count()).toBe(0); +}); + +test('should store non empty HyperLogLog buffer', async () => { + const hll = HyperLogLog(12); + hll.add(HyperLogLog.hash('connection-1')); + hll.add(HyperLogLog.hash('connection-2')); + await uniqueConnectionStore.insert({ + id: 'current', + hll: hll.output().buckets, + }); + + const fetchedHll = await uniqueConnectionStore.get('current'); + const emptyHll = HyperLogLog(12); + emptyHll.merge({ n: 12, buckets: fetchedHll!.hll }); + expect(hll.count()).toBe(2); +}); + +test('should indicate when no entry', async () => { + const fetchedHll = await uniqueConnectionStore.get('current'); + + expect(fetchedHll).toBeNull(); +}); diff --git a/src/lib/features/unique-connection/unique-connection-store.ts b/src/lib/features/unique-connection/unique-connection-store.ts new file mode 100644 index 000000000000..aa3eab7c5c2d --- /dev/null +++ b/src/lib/features/unique-connection/unique-connection-store.ts @@ -0,0 +1,34 @@ +import type { Db } from '../../db/db'; +import type { IUniqueConnectionStore } from '../../types'; +import type { UniqueConnections } from './unique-connection-store-type'; + +export class UniqueConnectionStore implements IUniqueConnectionStore { + private db: Db; + + constructor(db: Db) { + this.db = db; + } + + async insert(uniqueConnections: UniqueConnections): Promise { + await this.db('unique_connections') + .insert({ id: uniqueConnections.id, hll: uniqueConnections.hll }) + .onConflict('id') + .merge(); + } + + async get( + id: 'current' | 'previous', + ): Promise<(UniqueConnections & { updatedAt: Date }) | null> { + const row = await this.db('unique_connections') + .select('id', 'hll', 'updated_at') + .where('id', id) + .first(); + return row + ? { id: row.id, hll: row.hll, updatedAt: row.updated_at } + : null; + } + + async deleteAll(): Promise { + await this.db('unique_connections').delete(); + } +} diff --git a/src/lib/metric-events.ts b/src/lib/metric-events.ts index 6591ea4c672f..29557f9b84de 100644 --- a/src/lib/metric-events.ts +++ b/src/lib/metric-events.ts @@ -1,6 +1,7 @@ import type EventEmitter from 'events'; const REQUEST_TIME = 'request_time'; +const SDK_CONNECTION_ID_RECEIVED = 'sdk_connection_id_received'; const DB_TIME = 'db_time'; const FUNCTION_TIME = 'function_time'; const SCHEDULER_JOB_TIME = 'scheduler_job_time'; @@ -21,6 +22,7 @@ const CLIENT_DELTA_MEMORY = 'client_delta_memory'; type MetricEvent = | typeof REQUEST_TIME + | typeof SDK_CONNECTION_ID_RECEIVED | typeof DB_TIME | typeof FUNCTION_TIME | typeof SCHEDULER_JOB_TIME @@ -71,6 +73,7 @@ const onMetricEvent = ( export { REQUEST_TIME, + SDK_CONNECTION_ID_RECEIVED, DB_TIME, SCHEDULER_JOB_TIME, FUNCTION_TIME, diff --git a/src/lib/middleware/response-time-metrics.test.ts b/src/lib/middleware/response-time-metrics.test.ts index 6e8dd541d951..db4456e50bb1 100644 --- a/src/lib/middleware/response-time-metrics.test.ts +++ b/src/lib/middleware/response-time-metrics.test.ts @@ -66,6 +66,7 @@ describe('responseTimeMetrics new behavior', () => { }, method: 'GET', path: 'should-not-be-used', + headers: {}, }; // @ts-expect-error req and res doesn't have all properties @@ -98,6 +99,7 @@ describe('responseTimeMetrics new behavior', () => { }; const reqWithoutRoute = { method: 'GET', + headers: {}, }; // @ts-expect-error req and res doesn't have all properties @@ -132,6 +134,7 @@ describe('responseTimeMetrics new behavior', () => { }; const reqWithoutRoute = { method: 'GET', + headers: {}, }; // @ts-expect-error req and res doesn't have all properties @@ -166,6 +169,7 @@ describe('responseTimeMetrics new behavior', () => { const reqWithoutRoute = { method: 'GET', path, + headers: {}, }; // @ts-expect-error req and res doesn't have all properties @@ -210,6 +214,7 @@ describe('responseTimeMetrics new behavior', () => { const reqWithoutRoute = { method: 'GET', path, + headers: {}, }; // @ts-expect-error req and res doesn't have all properties diff --git a/src/lib/middleware/response-time-metrics.ts b/src/lib/middleware/response-time-metrics.ts index 8ab50328ee03..5b19a5a3f1f8 100644 --- a/src/lib/middleware/response-time-metrics.ts +++ b/src/lib/middleware/response-time-metrics.ts @@ -1,6 +1,6 @@ import * as responseTime from 'response-time'; import type EventEmitter from 'events'; -import { REQUEST_TIME } from '../metric-events'; +import { REQUEST_TIME, SDK_CONNECTION_ID_RECEIVED } from '../metric-events'; import type { IFlagResolver } from '../types/experimental'; import type { InstanceStatsService } from '../services'; import type { RequestHandler } from 'express'; @@ -66,6 +66,11 @@ export function responseTimeMetrics( req.query.appName; } + const connectionId = req.headers['x-unleash-connection-id']; + if (connectionId && flagResolver.isEnabled('uniqueSdkTracking')) { + eventBus.emit(SDK_CONNECTION_ID_RECEIVED, connectionId); + } + const timingInfo = { path: pathname, method: req.method, diff --git a/src/lib/services/index.ts b/src/lib/services/index.ts index 3cdc3fe1ba02..cb9a067febf0 100644 --- a/src/lib/services/index.ts +++ b/src/lib/services/index.ts @@ -157,6 +157,7 @@ import { createContextService, createFakeContextService, } from '../features/context/createContextService'; +import { UniqueConnectionService } from '../features/unique-connection/unique-connection-service'; export const createServices = ( stores: IUnleashStores, @@ -403,6 +404,9 @@ export const createServices = ( const featureLifecycleService = transactionalFeatureLifecycleService; featureLifecycleService.listen(); + const uniqueConnectionService = new UniqueConnectionService(stores, config); + uniqueConnectionService.listen(); + const onboardingService = db ? createOnboardingService(config)(db) : createFakeOnboardingService(config).onboardingService; @@ -484,6 +488,7 @@ export const createServices = ( personalDashboardService, projectStatusService, transactionalUserSubscriptionsService, + uniqueConnectionService, }; }; @@ -537,4 +542,5 @@ export { PersonalDashboardService, ProjectStatusService, UserSubscriptionsService, + UniqueConnectionService, }; diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 5fa6d107c7fc..e37efac1a580 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -59,6 +59,7 @@ import type { OnboardingService } from '../features/onboarding/onboarding-servic import type { PersonalDashboardService } from '../features/personal-dashboard/personal-dashboard-service'; import type { ProjectStatusService } from '../features/project-status/project-status-service'; import type { UserSubscriptionsService } from '../features/user-subscriptions/user-subscriptions-service'; +import type { UniqueConnectionService } from '../features/unique-connection/unique-connection-service'; export interface IUnleashServices { transactionalAccessService: WithTransactional; @@ -131,4 +132,5 @@ export interface IUnleashServices { personalDashboardService: PersonalDashboardService; projectStatusService: ProjectStatusService; transactionalUserSubscriptionsService: WithTransactional; + uniqueConnectionService: UniqueConnectionService; } diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 9998009fe692..710b842d48e8 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -53,6 +53,7 @@ import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-mod import { IOnboardingStore } from '../features/onboarding/onboarding-store-type'; import type { IUserUnsubscribeStore } from '../features/user-subscriptions/user-unsubscribe-store-type'; import type { IUserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model-type'; +import { IUniqueConnectionStore } from '../features/unique-connection/unique-connection-store-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -110,6 +111,7 @@ export interface IUnleashStores { onboardingStore: IOnboardingStore; userUnsubscribeStore: IUserUnsubscribeStore; userSubscriptionsReadModel: IUserSubscriptionsReadModel; + uniqueConnectionStore: IUniqueConnectionStore; } export { @@ -165,4 +167,5 @@ export { type IProjectReadModel, IOnboardingStore, type IUserSubscriptionsReadModel, + IUniqueConnectionStore, }; diff --git a/src/server-dev.ts b/src/server-dev.ts index 22aea88451de..1e08a0a007f4 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -55,6 +55,7 @@ process.nextTick(async () => { flagOverviewRedesign: false, granularAdminPermissions: true, deltaApi: true, + uniqueSdkTracking: true, }, }, authentication: { diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index be96d9a380b7..21038c30f12a 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -56,6 +56,7 @@ import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboardi import { createFakeOnboardingReadModel } from '../../lib/features/onboarding/createOnboardingReadModel'; import { FakeUserUnsubscribeStore } from '../../lib/features/user-subscriptions/fake-user-unsubscribe-store'; import { FakeUserSubscriptionsReadModel } from '../../lib/features/user-subscriptions/fake-user-subscriptions-read-model'; +import { FakeUniqueConnectionStore } from '../../lib/features/unique-connection/fake-unique-connection-store'; const db = { select: () => ({ @@ -121,6 +122,7 @@ const createStores: () => IUnleashStores = () => { onboardingStore: new FakeOnboardingStore(), userUnsubscribeStore: new FakeUserUnsubscribeStore(), userSubscriptionsReadModel: new FakeUserSubscriptionsReadModel(), + uniqueConnectionStore: new FakeUniqueConnectionStore(), }; }; diff --git a/yarn.lock b/yarn.lock index 014f65b439c9..d8dbff984bbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4919,6 +4919,15 @@ __metadata: languageName: node linkType: hard +"hyperloglog-lite@npm:^1.0.2": + version: 1.0.2 + resolution: "hyperloglog-lite@npm:1.0.2" + dependencies: + murmurhash32-node: "npm:^1.0.1" + checksum: 10c0/3077b9dba1bac384b842a70d1b17da58449d3e633936ef7bd03a3386613e59c413f5f886d9383d14c3fe31eac524abe28a99025d43c446e57aa4175b17675450 + languageName: node + linkType: hard + "iconv-lite@npm:0.4.24": version: 0.4.24 resolution: "iconv-lite@npm:0.4.24" @@ -6831,6 +6840,13 @@ __metadata: languageName: node linkType: hard +"murmurhash32-node@npm:^1.0.1": + version: 1.0.1 + resolution: "murmurhash32-node@npm:1.0.1" + checksum: 10c0/06a36a2f0d0c6855ce131c2a5c225c3096f53bf36898eb2683b2200f782577cde07f07485792d6e85798ea74f4dd95e836058fbab07c49cfcbc0a79b168ab654 + languageName: node + linkType: hard + "murmurhash3js@npm:^3.0.1": version: 3.0.1 resolution: "murmurhash3js@npm:3.0.1" @@ -9311,6 +9327,7 @@ __metadata: helmet: "npm:^6.0.0" http-errors: "npm:^2.0.0" husky: "npm:^9.0.11" + hyperloglog-lite: "npm:^1.0.2" ip-address: "npm:^10.0.1" jest: "npm:29.7.0" jest-junit: "npm:^16.0.0"