mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +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