Skip to content

Commit

Permalink
feat: start returning onboarding status with project overview (#8058)
Browse files Browse the repository at this point in the history
To show/hide onboarding flow, we need to get extra info about onboarding
status. This PR adds it to project overview.
  • Loading branch information
sjaanus authored Sep 3, 2024
1 parent c865fd4 commit 037651c
Show file tree
Hide file tree
Showing 13 changed files with 168 additions and 13 deletions.
4 changes: 2 additions & 2 deletions src/lib/db/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ import { LargestResourcesReadModel } from '../features/metrics/sizes/largest-res
import { IntegrationEventsStore } from '../features/integration-events/integration-events-store';
import { FeatureCollaboratorsReadModel } from '../features/feature-toggle/feature-collaborators-read-model';
import { createProjectReadModel } from '../features/project/createProjectReadModel';
import { OnboardingReadModel } from '../features/onboarding/onboarding-read-model';
import { OnboardingStore } from '../features/onboarding/onboarding-store';
import { createOnboardingReadModel } from '../features/onboarding/createOnboardingReadModel';

export const createStores = (
config: IUnleashConfig,
Expand Down Expand Up @@ -173,7 +173,7 @@ export const createStores = (
projectFlagCreatorsReadModel: new ProjectFlagCreatorsReadModel(db),
featureLifecycleStore: new FeatureLifecycleStore(db),
featureStrategiesReadModel: new FeatureStrategiesReadModel(db),
onboardingReadModel: new OnboardingReadModel(db),
onboardingReadModel: createOnboardingReadModel(db),
onboardingStore: new OnboardingStore(db),
featureLifecycleReadModel: new FeatureLifecycleReadModel(
db,
Expand Down
12 changes: 12 additions & 0 deletions src/lib/features/onboarding/createOnboardingReadModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { Db } from '../../server-impl';
import type { IOnboardingReadModel } from '../../types';
import { OnboardingReadModel } from './onboarding-read-model';
import { FakeOnboardingReadModel } from './fake-onboarding-read-model';

export const createOnboardingReadModel = (db: Db): IOnboardingReadModel => {
return new OnboardingReadModel(db);
};

export const createFakeOnboardingReadModel = (): IOnboardingReadModel => {
return new FakeOnboardingReadModel();
};
7 changes: 7 additions & 0 deletions src/lib/features/onboarding/fake-onboarding-read-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IOnboardingReadModel } from '../../types';
import type {
InstanceOnboarding,
OnboardingStatus,
ProjectOnboarding,
} from './onboarding-read-model-type';

Expand All @@ -17,4 +18,10 @@ export class FakeOnboardingReadModel implements IOnboardingReadModel {
getProjectsOnboardingMetrics(): Promise<ProjectOnboarding[]> {
return Promise.resolve([]);
}

getOnboardingStatusForProject(
projectId: string,
): Promise<OnboardingStatus> {
throw new Error('Method not implemented.');
}
}
9 changes: 7 additions & 2 deletions src/lib/features/onboarding/onboarding-read-model-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import type { ProjectOverviewSchema } from '../../openapi';

export type OnboardingStatus = ProjectOverviewSchema['onboardingStatus'];

/**
* All the values are in minutes
* All the values are in seconds
*/
export type InstanceOnboarding = {
firstLogin: number | null;
Expand All @@ -10,7 +14,7 @@ export type InstanceOnboarding = {
};

/**
* All the values are in minutes
* All the values are in seconds
*/
export type ProjectOnboarding = {
project: string;
Expand All @@ -22,4 +26,5 @@ export type ProjectOnboarding = {
export interface IOnboardingReadModel {
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding>;
getProjectsOnboardingMetrics(): Promise<Array<ProjectOnboarding>>;
getOnboardingStatusForProject(projectId: string): Promise<OnboardingStatus>;
}
50 changes: 47 additions & 3 deletions src/lib/features/onboarding/onboarding-read-model.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
import getLogger from '../../../test/fixtures/no-logger';
import type { IOnboardingStore } from '../../types';
import { OnboardingReadModel } from './onboarding-read-model';
import {
type IFeatureToggleStore,
type ILastSeenStore,
type IOnboardingStore,
SYSTEM_USER,
} from '../../types';
import type { IOnboardingReadModel } from './onboarding-read-model-type';

let db: ITestDb;
let onboardingReadModel: IOnboardingReadModel;
let onBoardingStore: IOnboardingStore;
let featureToggleStore: IFeatureToggleStore;
let lastSeenStore: ILastSeenStore;

beforeAll(async () => {
db = await dbInit('onboarding_read_model', getLogger, {
experimental: { flags: { onboardingMetrics: true } },
});
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
onboardingReadModel = db.stores.onboardingReadModel;
onBoardingStore = db.stores.onboardingStore;
featureToggleStore = db.stores.featureToggleStore;
lastSeenStore = db.stores.lastSeenStore;
});

afterAll(async () => {
Expand Down Expand Up @@ -109,3 +117,39 @@ test('can get instance onboarding durations', async () => {
},
]);
});

test('can get project onboarding status', async () => {
const onboardingStartedResult =
await onboardingReadModel.getOnboardingStatusForProject('default');

expect(onboardingStartedResult).toMatchObject({
status: 'onboarding-started',
});

await featureToggleStore.create('default', {
name: 'my-flag',
createdByUserId: SYSTEM_USER.id,
});

const firstFlagResult =
await onboardingReadModel.getOnboardingStatusForProject('default');

expect(firstFlagResult).toMatchObject({
status: 'first-flag-created',
feature: 'my-flag',
});

await lastSeenStore.setLastSeen([
{
environment: 'default',
featureName: 'my-flag',
},
]);

const onboardedResult =
await onboardingReadModel.getOnboardingStatusForProject('default');

expect(onboardedResult).toMatchObject({
status: 'onboarded',
});
});
27 changes: 27 additions & 0 deletions src/lib/features/onboarding/onboarding-read-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
IOnboardingReadModel,
InstanceOnboarding,
ProjectOnboarding,
OnboardingStatus,
} from './onboarding-read-model-type';

const instanceEventLookup = {
Expand Down Expand Up @@ -78,4 +79,30 @@ export class OnboardingReadModel implements IOnboardingReadModel {

return projects;
}

async getOnboardingStatusForProject(
projectId: string,
): Promise<OnboardingStatus> {
const feature = await this.db('features')
.select('name')
.where('project', projectId)
.first();

if (!feature) {
return { status: 'onboarding-started' };
}

const lastSeen = await this.db('last_seen_at_metrics as lsm')
.select('lsm.feature_name')
.innerJoin('features as f', 'f.name', 'lsm.feature_name')
.innerJoin('projects as p', 'p.id', 'f.project')
.where('p.id', projectId)
.first();

if (lastSeen) {
return { status: 'onboarded' };
} else {
return { status: 'first-flag-created', feature: feature.name };
}
}
}
3 changes: 1 addition & 2 deletions src/lib/features/onboarding/onboarding-service.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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;
Expand All @@ -24,7 +23,7 @@ beforeAll(async () => {
eventBus = config.eventBus;
onboardingService = createOnboardingService(config)(db.rawDatabase);
onboardingService.listen();
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
onboardingReadModel = db.stores.onboardingReadModel;
});

afterAll(async () => {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/features/project/createProjectService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ import {
createFakeProjectReadModel,
createProjectReadModel,
} from './createProjectReadModel';
import {
createFakeOnboardingReadModel,
createOnboardingReadModel,
} from '../onboarding/createOnboardingReadModel';

export const createProjectService = (
db: Db,
Expand Down Expand Up @@ -130,6 +134,8 @@ export const createProjectService = (
config.flagResolver,
);

const onboardingReadModel = createOnboardingReadModel(db);

return new ProjectService(
{
projectStore,
Expand All @@ -142,6 +148,7 @@ export const createProjectService = (
projectOwnersReadModel,
projectFlagCreatorsReadModel,
projectReadModel,
onboardingReadModel,
},
config,
accessService,
Expand Down Expand Up @@ -197,6 +204,8 @@ export const createFakeProjectService = (

const projectReadModel = createFakeProjectReadModel();

const onboardingReadModel = createFakeOnboardingReadModel();

return new ProjectService(
{
projectStore,
Expand All @@ -209,6 +218,7 @@ export const createFakeProjectService = (
accountStore,
projectStatsStore,
projectReadModel,
onboardingReadModel,
},
config,
accessService,
Expand Down
9 changes: 9 additions & 0 deletions src/lib/features/project/project-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
RoleName,
SYSTEM_USER_ID,
type IProjectReadModel,
type IOnboardingReadModel,
} from '../../types';
import type {
IProjectAccessModel,
Expand Down Expand Up @@ -164,6 +165,8 @@ export default class ProjectService {

private projectReadModel: IProjectReadModel;

private onboardingReadModel: IOnboardingReadModel;

constructor(
{
projectStore,
Expand All @@ -176,6 +179,7 @@ export default class ProjectService {
accountStore,
projectStatsStore,
projectReadModel,
onboardingReadModel,
}: Pick<
IUnleashStores,
| 'projectStore'
Expand All @@ -188,6 +192,7 @@ export default class ProjectService {
| 'accountStore'
| 'projectStatsStore'
| 'projectReadModel'
| 'onboardingReadModel'
>,
config: IUnleashConfig,
accessService: AccessService,
Expand Down Expand Up @@ -220,6 +225,7 @@ export default class ProjectService {
this.resourceLimits = config.resourceLimits;
this.eventBus = config.eventBus;
this.projectReadModel = projectReadModel;
this.onboardingReadModel = onboardingReadModel;
}

async getProjects(
Expand Down Expand Up @@ -1491,6 +1497,7 @@ export default class ProjectService {
members,
favorite,
projectStats,
onboardingStatus,
] = await Promise.all([
this.projectStore.get(projectId),
this.projectStore.getEnvironmentsForProject(projectId),
Expand All @@ -1507,6 +1514,7 @@ export default class ProjectService {
})
: Promise.resolve(false),
this.projectStatsStore.getProjectStats(projectId),
this.onboardingReadModel.getOnboardingStatusForProject(projectId),
]);

return {
Expand All @@ -1524,6 +1532,7 @@ export default class ProjectService {
? { archivedAt: project.archivedAt }
: {}),
createdAt: project.createdAt,
onboardingStatus,
environments,
featureTypeCounts,
members,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/openapi/spec/project-overview-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ test('projectOverviewSchema', () => {
count: 1,
},
],
onboardingStatus: {
status: 'onboarding-started',
},
};

expect(
Expand Down
37 changes: 36 additions & 1 deletion src/lib/openapi/spec/project-overview-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export const projectOverviewSchema = {
$id: '#/components/schemas/projectOverviewSchema',
type: 'object',
additionalProperties: false,
required: ['version', 'name'],
required: ['version', 'name', 'onboardingStatus'],
description:
'A high-level overview of a project. It contains information such as project statistics, the name of the project, what members and what features it contains, etc.',
properties: {
Expand Down Expand Up @@ -135,6 +135,41 @@ export const projectOverviewSchema = {
description:
'`true` if the project was favorited, otherwise `false`.',
},
onboardingStatus: {
type: 'object',
oneOf: [
{
type: 'object',
properties: {
status: {
type: 'string',
enum: ['onboarding-started', 'onboarded'],
example: 'onboarding-started',
},
},
required: ['status'],
additionalProperties: false,
},
{
type: 'object',
properties: {
status: {
type: 'string',
enum: ['first-flag-created'],
example: 'first-flag-created',
},
feature: {
type: 'string',
description: 'The name of the feature flag',
example: 'my-feature-flag',
},
},
required: ['status', 'feature'],
additionalProperties: false,
},
],
description: 'The current onboarding status of the project.',
},
},
components: {
schemas: {
Expand Down
6 changes: 5 additions & 1 deletion src/lib/types/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ import type { IRole } from './stores/access-store';
import type { IUser } from './user';
import type { ALL_OPERATORS } from '../util';
import type { IProjectStats } from '../features/project/project-service';
import type { CreateFeatureStrategySchema } from '../openapi';
import type {
CreateFeatureStrategySchema,
ProjectOverviewSchema,
} from '../openapi';
import type { ProjectEnvironment } from '../features/project/project-store-type';
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
Expand Down Expand Up @@ -313,6 +316,7 @@ export interface IProjectOverview {
featureLimit?: number;
featureNaming?: IFeatureNaming;
defaultStickiness: string;
onboardingStatus: ProjectOverviewSchema['onboardingStatus'];
}

export interface IProjectHealthReport extends IProjectHealth {
Expand Down
Loading

0 comments on commit 037651c

Please sign in to comment.