mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-19 01:17:18 +02: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> {
|
insertProjectEvent(event: ProjectEvent): Promise<void> {
|
||||||
throw new Error('Method not implemented.');
|
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.');
|
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 dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
import { minutesToMilliseconds } from 'date-fns';
|
import { minutesToMilliseconds } from 'date-fns';
|
||||||
import { OnboardingService } from './onboarding-service';
|
import type { OnboardingService } from './onboarding-service';
|
||||||
import { createTestConfig } from '../../../test/config/test-config';
|
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 db: ITestDb;
|
||||||
let stores: IUnleashStores;
|
let stores: IUnleashStores;
|
||||||
let onboardingService: OnboardingService;
|
let onboardingService: OnboardingService;
|
||||||
|
let eventBus: EventEmitter;
|
||||||
|
let onboardingReadModel: IOnboardingReadModel;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('onboarding_store', getLogger);
|
db = await dbInit('onboarding_store', getLogger);
|
||||||
@ -15,11 +21,10 @@ beforeAll(async () => {
|
|||||||
experimental: { flags: { onboardingMetrics: true } },
|
experimental: { flags: { onboardingMetrics: true } },
|
||||||
});
|
});
|
||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
const { userStore, onboardingStore, projectReadModel } = stores;
|
eventBus = config.eventBus;
|
||||||
onboardingService = new OnboardingService(
|
onboardingService = createOnboardingService(config)(db.rawDatabase);
|
||||||
{ onboardingStore, userStore, projectReadModel },
|
onboardingService.listen();
|
||||||
config,
|
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -27,6 +32,9 @@ afterAll(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await stores.featureToggleStore.deleteAll();
|
||||||
|
await stores.projectStore.deleteAll();
|
||||||
|
await stores.onboardingStore.deleteAll();
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -85,3 +93,55 @@ test('Storing onboarding events', async () => {
|
|||||||
{ event: 'first-live', time_to_event: 300, project: 'test_project' },
|
{ 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) {
|
if ('flag' in event) {
|
||||||
await this.insertProjectEvent(event);
|
await this.insertProjectEvent(event);
|
||||||
}
|
}
|
||||||
|
this.eventBus.emit('onboarding-event');
|
||||||
}
|
}
|
||||||
|
|
||||||
private async insertInstanceEvent(event: {
|
private async insertInstanceEvent(event: {
|
||||||
|
@ -13,4 +13,6 @@ export interface IOnboardingStore {
|
|||||||
insertProjectEvent(event: ProjectEvent): Promise<void>;
|
insertProjectEvent(event: ProjectEvent): Promise<void>;
|
||||||
|
|
||||||
insertInstanceEvent(event: InstanceEvent): Promise<void>;
|
insertInstanceEvent(event: InstanceEvent): Promise<void>;
|
||||||
|
|
||||||
|
deleteAll(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
@ -69,4 +69,11 @@ export class OnboardingStore implements IOnboardingStore {
|
|||||||
.onConflict()
|
.onConflict()
|
||||||
.ignore();
|
.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,
|
createFakePlaygroundService,
|
||||||
createPlaygroundService,
|
createPlaygroundService,
|
||||||
} from '../features/playground/createPlaygroundService';
|
} from '../features/playground/createPlaygroundService';
|
||||||
|
import {
|
||||||
|
createFakeOnboardingService,
|
||||||
|
createOnboardingService,
|
||||||
|
} from '../features/onboarding/createOnboardingService';
|
||||||
|
import { OnboardingService } from '../features/onboarding/onboarding-service';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -391,6 +396,11 @@ export const createServices = (
|
|||||||
const featureLifecycleService = transactionalFeatureLifecycleService;
|
const featureLifecycleService = transactionalFeatureLifecycleService;
|
||||||
featureLifecycleService.listen();
|
featureLifecycleService.listen();
|
||||||
|
|
||||||
|
const onboardingService = db
|
||||||
|
? createOnboardingService(config)(db)
|
||||||
|
: createFakeOnboardingService(config).onboardingService;
|
||||||
|
onboardingService.listen();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
accountService,
|
accountService,
|
||||||
@ -453,6 +463,7 @@ export const createServices = (
|
|||||||
featureLifecycleService,
|
featureLifecycleService,
|
||||||
transactionalFeatureLifecycleService,
|
transactionalFeatureLifecycleService,
|
||||||
integrationEventsService,
|
integrationEventsService,
|
||||||
|
onboardingService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -502,4 +513,5 @@ export {
|
|||||||
JobService,
|
JobService,
|
||||||
FeatureLifecycleService,
|
FeatureLifecycleService,
|
||||||
IntegrationEventsService,
|
IntegrationEventsService,
|
||||||
|
OnboardingService,
|
||||||
};
|
};
|
||||||
|
@ -55,6 +55,7 @@ import type { ProjectInsightsService } from '../features/project-insights/projec
|
|||||||
import type { JobService } from '../features/scheduler/job-service';
|
import type { JobService } from '../features/scheduler/job-service';
|
||||||
import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
|
import type { FeatureLifecycleService } from '../features/feature-lifecycle/feature-lifecycle-service';
|
||||||
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||||
|
import type { OnboardingService } from '../features/onboarding/onboarding-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -121,4 +122,5 @@ export interface IUnleashServices {
|
|||||||
featureLifecycleService: FeatureLifecycleService;
|
featureLifecycleService: FeatureLifecycleService;
|
||||||
transactionalFeatureLifecycleService: WithTransactional<FeatureLifecycleService>;
|
transactionalFeatureLifecycleService: WithTransactional<FeatureLifecycleService>;
|
||||||
integrationEventsService: IntegrationEventsService;
|
integrationEventsService: IntegrationEventsService;
|
||||||
|
onboardingService: OnboardingService;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user