mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: onboarding service composition root (#8035)
This commit is contained in:
		
							parent
							
								
									0f5e4dc96b
								
							
						
					
					
						commit
						1bc0f97101
					
				
							
								
								
									
										48
									
								
								src/lib/features/onboarding/createOnboardingService.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/lib/features/onboarding/createOnboardingService.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import type { IUnleashConfig } from '../../types'; | ||||
| import type { Db } from '../../db/db'; | ||||
| import { OnboardingService } from './onboarding-service'; | ||||
| import { OnboardingStore } from './onboarding-store'; | ||||
| import { ProjectReadModel } from '../project/project-read-model'; | ||||
| import UserStore from '../../db/user-store'; | ||||
| import FakeUserStore from '../../../test/fixtures/fake-user-store'; | ||||
| import { FakeProjectReadModel } from '../project/fake-project-read-model'; | ||||
| import { FakeOnboardingStore } from './fake-onboarding-store'; | ||||
| 
 | ||||
| export const createOnboardingService = | ||||
|     (config: IUnleashConfig) => | ||||
|     (db: Db): OnboardingService => { | ||||
|         const { eventBus, flagResolver, getLogger } = config; | ||||
|         const onboardingStore = new OnboardingStore(db); | ||||
|         const projectReadModel = new ProjectReadModel( | ||||
|             db, | ||||
|             eventBus, | ||||
|             flagResolver, | ||||
|         ); | ||||
|         const userStore = new UserStore(db, getLogger, flagResolver); | ||||
|         const onboardingService = new OnboardingService( | ||||
|             { | ||||
|                 onboardingStore, | ||||
|                 projectReadModel, | ||||
|                 userStore, | ||||
|             }, | ||||
|             config, | ||||
|         ); | ||||
| 
 | ||||
|         return onboardingService; | ||||
|     }; | ||||
| 
 | ||||
| export const createFakeOnboardingService = (config: IUnleashConfig) => { | ||||
|     const onboardingStore = new FakeOnboardingStore(); | ||||
|     const projectReadModel = new FakeProjectReadModel(); | ||||
|     const userStore = new FakeUserStore(); | ||||
|     const onboardingService = new OnboardingService( | ||||
|         { | ||||
|             onboardingStore, | ||||
|             projectReadModel, | ||||
|             userStore, | ||||
|         }, | ||||
|         config, | ||||
|     ); | ||||
| 
 | ||||
|     return { onboardingService, projectReadModel, userStore, onboardingStore }; | ||||
| }; | ||||
| @ -8,7 +8,10 @@ export class FakeOnboardingStore implements IOnboardingStore { | ||||
|     insertProjectEvent(event: ProjectEvent): Promise<void> { | ||||
|         throw new Error('Method not implemented.'); | ||||
|     } | ||||
|     insertInstanceEvent(event: InstanceEvent): Promise<void> { | ||||
|     async insertInstanceEvent(event: InstanceEvent): Promise<void> { | ||||
|         throw new Error('Method not implemented.'); | ||||
|     } | ||||
|     async deleteAll(): Promise<void> { | ||||
|         throw new Error('Method not implemented.'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,13 +1,19 @@ | ||||
| import type { IUnleashStores } from '../../../lib/types'; | ||||
| import type { IOnboardingReadModel, IUnleashStores } from '../../../lib/types'; | ||||
| import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; | ||||
| import getLogger from '../../../test/fixtures/no-logger'; | ||||
| import { minutesToMilliseconds } from 'date-fns'; | ||||
| import { OnboardingService } from './onboarding-service'; | ||||
| import type { OnboardingService } from './onboarding-service'; | ||||
| import { createTestConfig } from '../../../test/config/test-config'; | ||||
| import { createOnboardingService } from './createOnboardingService'; | ||||
| import type EventEmitter from 'events'; | ||||
| import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events'; | ||||
| import { OnboardingReadModel } from './onboarding-read-model'; | ||||
| 
 | ||||
| let db: ITestDb; | ||||
| let stores: IUnleashStores; | ||||
| let onboardingService: OnboardingService; | ||||
| let eventBus: EventEmitter; | ||||
| let onboardingReadModel: IOnboardingReadModel; | ||||
| 
 | ||||
| beforeAll(async () => { | ||||
|     db = await dbInit('onboarding_store', getLogger); | ||||
| @ -15,11 +21,10 @@ beforeAll(async () => { | ||||
|         experimental: { flags: { onboardingMetrics: true } }, | ||||
|     }); | ||||
|     stores = db.stores; | ||||
|     const { userStore, onboardingStore, projectReadModel } = stores; | ||||
|     onboardingService = new OnboardingService( | ||||
|         { onboardingStore, userStore, projectReadModel }, | ||||
|         config, | ||||
|     ); | ||||
|     eventBus = config.eventBus; | ||||
|     onboardingService = createOnboardingService(config)(db.rawDatabase); | ||||
|     onboardingService.listen(); | ||||
|     onboardingReadModel = new OnboardingReadModel(db.rawDatabase); | ||||
| }); | ||||
| 
 | ||||
| afterAll(async () => { | ||||
| @ -27,6 +32,9 @@ afterAll(async () => { | ||||
| }); | ||||
| 
 | ||||
| beforeEach(async () => { | ||||
|     await stores.featureToggleStore.deleteAll(); | ||||
|     await stores.projectStore.deleteAll(); | ||||
|     await stores.onboardingStore.deleteAll(); | ||||
|     jest.useRealTimers(); | ||||
| }); | ||||
| 
 | ||||
| @ -85,3 +93,55 @@ test('Storing onboarding events', async () => { | ||||
|         { event: 'first-live', time_to_event: 300, project: 'test_project' }, | ||||
|     ]); | ||||
| }); | ||||
| 
 | ||||
| const reachedOnboardingEvents = (count: number) => { | ||||
|     let processedOnboardingEvents = 0; | ||||
|     return new Promise((resolve) => { | ||||
|         eventBus.on('onboarding-event', () => { | ||||
|             processedOnboardingEvents += 1; | ||||
|             if (processedOnboardingEvents === count) resolve('done'); | ||||
|         }); | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| test('Reacting to events', async () => { | ||||
|     jest.useFakeTimers(); | ||||
|     jest.setSystemTime(new Date()); | ||||
|     const { userStore, featureToggleStore, projectStore, projectReadModel } = | ||||
|         stores; | ||||
|     const user = await userStore.insert({}); | ||||
|     await projectStore.create({ id: 'test_project', name: 'irrelevant' }); | ||||
|     await featureToggleStore.create('test_project', { | ||||
|         name: 'test', | ||||
|         createdByUserId: user.id, | ||||
|     }); | ||||
|     jest.advanceTimersByTime(minutesToMilliseconds(1)); | ||||
| 
 | ||||
|     eventBus.emit(USER_LOGIN, { loginOrder: 0 }); | ||||
|     eventBus.emit(USER_LOGIN, { loginOrder: 1 }); | ||||
|     eventBus.emit(STAGE_ENTERED, { stage: 'initial', feature: 'test' }); | ||||
|     eventBus.emit(STAGE_ENTERED, { stage: 'pre-live', feature: 'test' }); | ||||
|     eventBus.emit(STAGE_ENTERED, { stage: 'live', feature: 'test' }); | ||||
|     await reachedOnboardingEvents(5); | ||||
| 
 | ||||
|     const instanceMetrics = | ||||
|         await onboardingReadModel.getInstanceOnboardingMetrics(); | ||||
|     const projectMetrics = | ||||
|         await onboardingReadModel.getProjectsOnboardingMetrics(); | ||||
| 
 | ||||
|     expect(instanceMetrics).toMatchObject({ | ||||
|         firstLogin: 60, | ||||
|         secondLogin: 60, | ||||
|         firstFeatureFlag: 60, | ||||
|         firstPreLive: 60, | ||||
|         firstLive: 60, | ||||
|     }); | ||||
|     expect(projectMetrics).toMatchObject([ | ||||
|         { | ||||
|             project: 'test_project', | ||||
|             firstFeatureFlag: 60, | ||||
|             firstPreLive: 60, | ||||
|             firstLive: 60, | ||||
|         }, | ||||
|     ]); | ||||
| }); | ||||
|  | ||||
| @ -96,6 +96,7 @@ export class OnboardingService { | ||||
|         if ('flag' in event) { | ||||
|             await this.insertProjectEvent(event); | ||||
|         } | ||||
|         this.eventBus.emit('onboarding-event'); | ||||
|     } | ||||
| 
 | ||||
|     private async insertInstanceEvent(event: { | ||||
|  | ||||
| @ -13,4 +13,6 @@ export interface IOnboardingStore { | ||||
|     insertProjectEvent(event: ProjectEvent): Promise<void>; | ||||
| 
 | ||||
|     insertInstanceEvent(event: InstanceEvent): Promise<void>; | ||||
| 
 | ||||
|     deleteAll(): Promise<void>; | ||||
| } | ||||
|  | ||||
| @ -69,4 +69,11 @@ export class OnboardingStore implements IOnboardingStore { | ||||
|             .onConflict() | ||||
|             .ignore(); | ||||
|     } | ||||
| 
 | ||||
|     async deleteAll(): Promise<void> { | ||||
|         await Promise.all([ | ||||
|             this.db('onboarding_events_project').del(), | ||||
|             this.db('onboarding_events_instance').del(), | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -141,6 +141,11 @@ import { | ||||
|     createFakePlaygroundService, | ||||
|     createPlaygroundService, | ||||
| } from '../features/playground/createPlaygroundService'; | ||||
| import { | ||||
|     createFakeOnboardingService, | ||||
|     createOnboardingService, | ||||
| } from '../features/onboarding/createOnboardingService'; | ||||
| import { OnboardingService } from '../features/onboarding/onboarding-service'; | ||||
| 
 | ||||
| export const createServices = ( | ||||
|     stores: IUnleashStores, | ||||
| @ -391,6 +396,11 @@ export const createServices = ( | ||||
|     const featureLifecycleService = transactionalFeatureLifecycleService; | ||||
|     featureLifecycleService.listen(); | ||||
| 
 | ||||
|     const onboardingService = db | ||||
|         ? createOnboardingService(config)(db) | ||||
|         : createFakeOnboardingService(config).onboardingService; | ||||
|     onboardingService.listen(); | ||||
| 
 | ||||
|     return { | ||||
|         accessService, | ||||
|         accountService, | ||||
| @ -453,6 +463,7 @@ export const createServices = ( | ||||
|         featureLifecycleService, | ||||
|         transactionalFeatureLifecycleService, | ||||
|         integrationEventsService, | ||||
|         onboardingService, | ||||
|     }; | ||||
| }; | ||||
| 
 | ||||
| @ -502,4 +513,5 @@ export { | ||||
|     JobService, | ||||
|     FeatureLifecycleService, | ||||
|     IntegrationEventsService, | ||||
|     OnboardingService, | ||||
| }; | ||||
|  | ||||
| @ -55,6 +55,7 @@ import type { ProjectInsightsService } from '../features/project-insights/projec | ||||
| import type { JobService } from '../features/scheduler/job-service'; | ||||
| import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service'; | ||||
| import type { IntegrationEventsService } from '../features/integration-events/integration-events-service'; | ||||
| import type { OnboardingService } from '../features/onboarding/onboarding-service'; | ||||
| 
 | ||||
| export interface IUnleashServices { | ||||
|     accessService: AccessService; | ||||
| @ -121,4 +122,5 @@ export interface IUnleashServices { | ||||
|     featureLifecycleService: FeatureLifecycleService; | ||||
|     transactionalFeatureLifecycleService: WithTransactional<FeatureLifecycleService>; | ||||
|     integrationEventsService: IntegrationEventsService; | ||||
|     onboardingService: OnboardingService; | ||||
| } | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user