diff --git a/src/lib/features/onboarding/onboarding-read-model.test.ts b/src/lib/features/onboarding/onboarding-read-model.test.ts index 848c3752b5..c67abdd900 100644 --- a/src/lib/features/onboarding/onboarding-read-model.test.ts +++ b/src/lib/features/onboarding/onboarding-read-model.test.ts @@ -1,28 +1,19 @@ import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init'; import getLogger from '../../../test/fixtures/no-logger'; -import type { - IFeatureLifecycleStore, - IFeatureToggleStore, - IUserStore, -} from '../../types'; +import type { IOnboardingStore } from '../../types'; import { OnboardingReadModel } from './onboarding-read-model'; import type { IOnboardingReadModel } from './onboarding-read-model-type'; -import { minutesToMilliseconds } from 'date-fns'; let db: ITestDb; let onboardingReadModel: IOnboardingReadModel; -let userStore: IUserStore; -let lifecycleStore: IFeatureLifecycleStore; -let featureToggleStore: IFeatureToggleStore; +let onBoardingStore: IOnboardingStore; beforeAll(async () => { db = await dbInit('onboarding_read_model', getLogger, { experimental: { flags: { onboardingMetrics: true } }, }); onboardingReadModel = new OnboardingReadModel(db.rawDatabase); - userStore = db.stores.userStore; - lifecycleStore = db.stores.featureLifecycleStore; - featureToggleStore = db.stores.featureToggleStore; + onBoardingStore = db.stores.onboardingStore; }); afterAll(async () => { @@ -31,13 +22,9 @@ afterAll(async () => { } }); -beforeEach(async () => { - await userStore.deleteAll(); - jest.useRealTimers(); -}); +beforeEach(async () => {}); -test('can get onboarding durations', async () => { - jest.useFakeTimers(); +test('can get instance onboarding durations', async () => { const initialResult = await onboardingReadModel.getInstanceOnboardingMetrics(); expect(initialResult).toMatchObject({ @@ -48,8 +35,10 @@ test('can get onboarding durations', async () => { firstLive: null, }); - const firstUser = await userStore.insert({}); - await userStore.successfullyLogin(firstUser); + await onBoardingStore.insertInstanceEvent({ + type: 'first-user-login', + timeToEvent: 0, + }); const firstLoginResult = await onboardingReadModel.getInstanceOnboardingMetrics(); @@ -58,42 +47,25 @@ test('can get onboarding durations', async () => { secondLogin: null, }); - jest.advanceTimersByTime(minutesToMilliseconds(10)); - - const secondUser = await userStore.insert({}); - await userStore.successfullyLogin(secondUser); - - jest.advanceTimersByTime(minutesToMilliseconds(10)); - - await featureToggleStore.create('default', { - name: 'test', - createdByUserId: secondUser.id, + await onBoardingStore.insertInstanceEvent({ + type: 'second-user-login', + timeToEvent: 10, }); - await lifecycleStore.insert([ - { - feature: 'test', - stage: 'initial', - }, - ]); + await onBoardingStore.insertInstanceEvent({ + type: 'flag-created', + timeToEvent: 20, + }); - jest.advanceTimersByTime(minutesToMilliseconds(10)); + await onBoardingStore.insertInstanceEvent({ + type: 'pre-live', + timeToEvent: 30, + }); - await lifecycleStore.insert([ - { - feature: 'test', - stage: 'pre-live', - }, - ]); - - jest.advanceTimersByTime(minutesToMilliseconds(10)); - - await lifecycleStore.insert([ - { - feature: 'test', - stage: 'live', - }, - ]); + await onBoardingStore.insertInstanceEvent({ + type: 'live', + timeToEvent: 40, + }); const secondLoginResult = await onboardingReadModel.getInstanceOnboardingMetrics(); @@ -104,6 +76,26 @@ test('can get onboarding durations', async () => { firstPreLive: 30, firstLive: 40, }); +}); + +test('can get instance onboarding durations', async () => { + await onBoardingStore.insertProjectEvent({ + project: 'default', + type: 'flag-created', + timeToEvent: 20, + }); + + await onBoardingStore.insertProjectEvent({ + project: 'default', + type: 'pre-live', + timeToEvent: 30, + }); + + await onBoardingStore.insertProjectEvent({ + project: 'default', + type: 'live', + timeToEvent: 40, + }); const projectOnboardingResult = await onboardingReadModel.getProjectsOnboardingMetrics(); diff --git a/src/lib/features/onboarding/onboarding-read-model.ts b/src/lib/features/onboarding/onboarding-read-model.ts index 772cb4d104..251d5066ea 100644 --- a/src/lib/features/onboarding/onboarding-read-model.ts +++ b/src/lib/features/onboarding/onboarding-read-model.ts @@ -4,14 +4,19 @@ import type { InstanceOnboarding, ProjectOnboarding, } from './onboarding-read-model-type'; -import { millisecondsToMinutes } from 'date-fns'; -const calculateTimeDifferenceInMinutes = (date1?: Date, date2?: Date) => { - if (date1 && date2) { - const diffInMilliseconds = date2.getTime() - date1.getTime(); - return millisecondsToMinutes(diffInMilliseconds); - } - return null; +const instanceEventLookup = { + 'first-user-login': 'firstLogin', + 'second-user-login': 'secondLogin', + 'first-flag': 'firstFeatureFlag', + 'first-pre-live': 'firstPreLive', + 'first-live': 'firstLive', +}; + +const projectEventLookup = { + 'first-flag': 'firstFeatureFlag', + 'first-pre-live': 'firstPreLive', + 'first-live': 'firstLive', }; export class OnboardingReadModel implements IOnboardingReadModel { @@ -22,109 +27,55 @@ export class OnboardingReadModel implements IOnboardingReadModel { } async getInstanceOnboardingMetrics(): Promise { - const firstUserCreatedResult = await this.db('users') - .select('created_at') - .orderBy('created_at') - .first(); - const firstLoginResult = await this.db('users') - .select('first_seen_at') - .orderBy('first_seen_at') - .limit(2); - - const firstInitialResult = await this.db('feature_lifecycles') - .select('created_at') - .where('stage', 'initial') - .orderBy('created_at') - .first(); - const firstPreLiveResult = await this.db('feature_lifecycles') - .select('created_at') - .where('stage', 'pre-live') - .orderBy('created_at') - .first(); - const firstLiveResult = await this.db('feature_lifecycles') - .select('created_at') - .where('stage', 'live') - .orderBy('created_at') - .first(); - - const createdAt = firstUserCreatedResult?.created_at; - const firstLogin = firstLoginResult[0]?.first_seen_at; - const secondLogin = firstLoginResult[1]?.first_seen_at; - const firstInitial = firstInitialResult?.created_at; - const firstPreLive = firstPreLiveResult?.created_at; - const firstLive = firstLiveResult?.created_at; - - const firstLoginDiff = calculateTimeDifferenceInMinutes( - createdAt, - firstLogin, - ); - const secondLoginDiff = calculateTimeDifferenceInMinutes( - createdAt, - secondLogin, - ); - const firstFlagDiff = calculateTimeDifferenceInMinutes( - createdAt, - firstInitial, - ); - const firstPreLiveDiff = calculateTimeDifferenceInMinutes( - createdAt, - firstPreLive, - ); - const firstLiveDiff = calculateTimeDifferenceInMinutes( - createdAt, - firstLive, + const eventsResult = await this.db('onboarding_events_instance').select( + 'event', + 'time_to_event', ); - return { - firstLogin: firstLoginDiff, - secondLogin: secondLoginDiff, - firstFeatureFlag: firstFlagDiff, - firstPreLive: firstPreLiveDiff, - firstLive: firstLiveDiff, + const events: InstanceOnboarding = { + firstLogin: null, + secondLogin: null, + firstFeatureFlag: null, + firstPreLive: null, + firstLive: null, }; + + for (const event of eventsResult) { + const eventType = instanceEventLookup[event.event]; + if (eventType) { + events[eventType] = event.time_to_event; + } + } + + return events as InstanceOnboarding; } async getProjectsOnboardingMetrics(): Promise> { - const lifecycleResults = await this.db('projects') - .join('features', 'projects.id', 'features.project') - .join( - 'feature_lifecycles', - 'features.name', - 'feature_lifecycles.feature', - ) - .select('projects.id as project_id') - .select('projects.created_at as project_created_at') - .select( - this.db.raw( - ` MIN(CASE WHEN feature_lifecycles.stage = 'initial' THEN feature_lifecycles.created_at ELSE NULL END) AS first_initial`, - ), - ) - .select( - this.db.raw( - `MIN(CASE WHEN feature_lifecycles.stage = 'pre-live' THEN feature_lifecycles.created_at ELSE NULL END) AS first_pre_live`, - ), - ) - .select( - this.db.raw( - `MIN(CASE WHEN feature_lifecycles.stage = 'live' THEN feature_lifecycles.created_at ELSE NULL END) AS first_live`, - ), - ) - .groupBy('projects.id'); + const lifecycleResults = await this.db( + 'onboarding_events_project', + ).select('project', 'event', 'time_to_event'); - return lifecycleResults.map((result) => ({ - project: result.project_id, - firstFeatureFlag: calculateTimeDifferenceInMinutes( - result.project_created_at, - result.first_initial, - ), - firstPreLive: calculateTimeDifferenceInMinutes( - result.project_created_at, - result.first_pre_live, - ), - firstLive: calculateTimeDifferenceInMinutes( - result.project_created_at, - result.first_live, - ), - })); + const projects: Array = []; + + lifecycleResults.forEach((result) => { + let project = projects.find((p) => p.project === result.project); + + if (!project) { + project = { + project: result.project, + firstFeatureFlag: null, + firstPreLive: null, + firstLive: null, + }; + projects.push(project); + } + + const eventType = projectEventLookup[result.event]; + if (eventType) { + project[eventType] = result.time_to_event; + } + }); + + return projects; } }