1
0
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:
Mateusz Kwasniewski 2024-09-02 11:39:47 +02:00 committed by GitHub
parent 0f5e4dc96b
commit 1bc0f97101
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 143 additions and 8 deletions

View 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 };
};

View File

@ -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.');
}
}

View File

@ -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,
},
]);
});

View File

@ -96,6 +96,7 @@ export class OnboardingService {
if ('flag' in event) {
await this.insertProjectEvent(event);
}
this.eventBus.emit('onboarding-event');
}
private async insertInstanceEvent(event: {

View File

@ -13,4 +13,6 @@ export interface IOnboardingStore {
insertProjectEvent(event: ProjectEvent): Promise<void>;
insertInstanceEvent(event: InstanceEvent): Promise<void>;
deleteAll(): Promise<void>;
}

View File

@ -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(),
]);
}
}

View File

@ -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,
};

View File

@ -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;
}