From 2213b01fc17e9af8ff91ffb769fcbb08f5ade2f3 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 13:07:48 +0200 Subject: [PATCH 1/8] feat: onboarding store --- src/lib/db/index.ts | 2 + .../onboarding/fake-onboarding-store.ts | 0 .../features/onboarding/onboarding-service.ts | 63 ++++++++ .../onboarding/onboarding-store-type.ts | 12 ++ .../onboarding/onboarding-store.test.ts | 72 ++++++++++ .../features/onboarding/onboarding-store.ts | 136 ++++++++++++++++++ src/lib/features/project/project-store.ts | 2 +- src/lib/types/stores.ts | 3 + 8 files changed, 289 insertions(+), 1 deletion(-) create mode 100644 src/lib/features/onboarding/fake-onboarding-store.ts create mode 100644 src/lib/features/onboarding/onboarding-service.ts create mode 100644 src/lib/features/onboarding/onboarding-store-type.ts create mode 100644 src/lib/features/onboarding/onboarding-store.test.ts create mode 100644 src/lib/features/onboarding/onboarding-store.ts diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 0d576a191ef4..87b905d3e265 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -53,6 +53,7 @@ import { IntegrationEventsStore } from '../features/integration-events/integrati 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'; export const createStores = ( config: IUnleashConfig, @@ -173,6 +174,7 @@ export const createStores = ( featureLifecycleStore: new FeatureLifecycleStore(db), featureStrategiesReadModel: new FeatureStrategiesReadModel(db), onboardingReadModel: new OnboardingReadModel(db), + onboardingStore: new OnboardingStore(db), featureLifecycleReadModel: new FeatureLifecycleReadModel( db, config.flagResolver, diff --git a/src/lib/features/onboarding/fake-onboarding-store.ts b/src/lib/features/onboarding/fake-onboarding-store.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/lib/features/onboarding/onboarding-service.ts b/src/lib/features/onboarding/onboarding-service.ts new file mode 100644 index 000000000000..6c28b18231ec --- /dev/null +++ b/src/lib/features/onboarding/onboarding-service.ts @@ -0,0 +1,63 @@ +import type { IFlagResolver, IUnleashConfig } from '../../types'; +import type EventEmitter from 'events'; +import type { Logger } from '../../logger'; +import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events'; +import type { NewStage } from '../feature-lifecycle/feature-lifecycle-store-type'; +import type { IOnboardingStore } from './onboarding-store-type'; + +export class OnboardingService { + private flagResolver: IFlagResolver; + + private eventBus: EventEmitter; + + private logger: Logger; + + private onboardingStore: IOnboardingStore; + + constructor( + { onboardingStore }: { onboardingStore: IOnboardingStore }, + { + flagResolver, + eventBus, + getLogger, + }: Pick, + ) { + this.onboardingStore = onboardingStore; + this.flagResolver = flagResolver; + this.eventBus = eventBus; + this.logger = getLogger('onboarding/onboarding-service.ts'); + } + + listen() { + this.eventBus.on(USER_LOGIN, async (event: { loginOrder: number }) => { + if (!this.flagResolver.isEnabled('onboardingMetrics')) return; + + if (event.loginOrder === 0) { + await this.onboardingStore.insert({ type: 'firstUserLogin' }); + } + if (event.loginOrder === 1) { + await this.onboardingStore.insert({ type: 'secondUserLogin' }); + } + }); + this.eventBus.on(STAGE_ENTERED, async (stage: NewStage) => { + if (!this.flagResolver.isEnabled('onboardingMetrics')) return; + + if (stage.stage === 'initial') { + await this.onboardingStore.insert({ + type: 'flagCreated', + flag: stage.feature, + }); + } else if (stage.stage === 'pre-live') { + await this.onboardingStore.insert({ + type: 'preLive', + flag: stage.feature, + }); + } else if (stage.stage === 'live') { + await this.onboardingStore.insert({ + type: 'live', + flag: stage.feature, + }); + } + }); + } +} diff --git a/src/lib/features/onboarding/onboarding-store-type.ts b/src/lib/features/onboarding/onboarding-store-type.ts new file mode 100644 index 000000000000..394a905e6ec8 --- /dev/null +++ b/src/lib/features/onboarding/onboarding-store-type.ts @@ -0,0 +1,12 @@ +export type SharedEvent = + | { type: 'flagCreated'; flag: string } + | { type: 'preLive'; flag: string } + | { type: 'live'; flag: string }; +export type InstanceEvent = + | { type: 'firstUserLogin' } + | { type: 'secondUserLogin' }; +export type OnboardingEvent = SharedEvent | InstanceEvent; + +export interface IOnboardingStore { + insert(event: OnboardingEvent): Promise; +} diff --git a/src/lib/features/onboarding/onboarding-store.test.ts b/src/lib/features/onboarding/onboarding-store.test.ts new file mode 100644 index 000000000000..bc395c7bfd4a --- /dev/null +++ b/src/lib/features/onboarding/onboarding-store.test.ts @@ -0,0 +1,72 @@ +import type { 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'; + +let db: ITestDb; +let stores: IUnleashStores; + +beforeAll(async () => { + db = await dbInit('onboarding_store', getLogger); + stores = db.stores; +}); + +afterAll(async () => { + await db.destroy(); +}); + +beforeEach(async () => { + jest.useRealTimers(); +}); + +test('Storing onboarding events', async () => { + jest.useFakeTimers(); + jest.setSystemTime(new Date()); + const { userStore, onboardingStore, featureToggleStore, projectStore } = + 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)); + await onboardingStore.insert({ type: 'firstUserLogin' }); + jest.advanceTimersByTime(minutesToMilliseconds(1)); + await onboardingStore.insert({ type: 'secondUserLogin' }); + jest.advanceTimersByTime(minutesToMilliseconds(1)); + await onboardingStore.insert({ type: 'flagCreated', flag: 'test' }); + await onboardingStore.insert({ type: 'flagCreated', flag: 'test' }); + await onboardingStore.insert({ type: 'flagCreated', flag: 'invalid' }); + jest.advanceTimersByTime(minutesToMilliseconds(1)); + await onboardingStore.insert({ type: 'preLive', flag: 'test' }); + await onboardingStore.insert({ type: 'preLive', flag: 'test' }); + await onboardingStore.insert({ type: 'preLive', flag: 'invalid' }); + jest.advanceTimersByTime(minutesToMilliseconds(1)); + await onboardingStore.insert({ type: 'live', flag: 'test' }); + jest.advanceTimersByTime(minutesToMilliseconds(1)); + await onboardingStore.insert({ type: 'live', flag: 'test' }); + jest.advanceTimersByTime(minutesToMilliseconds(1)); + await onboardingStore.insert({ type: 'live', flag: 'invalid' }); + + const { rows: instanceEvents } = await db.rawDatabase.raw( + 'SELECT * FROM onboarding_events_instance', + ); + expect(instanceEvents).toMatchObject([ + { event: 'firstUserLogin', time_to_event: 60 }, + { event: 'secondUserLogin', time_to_event: 120 }, + { event: 'firstFlag', time_to_event: 180 }, + { event: 'firstPreLive', time_to_event: 240 }, + { event: 'firstLive', time_to_event: 300 }, + ]); + + const { rows: projectEvents } = await db.rawDatabase.raw( + 'SELECT * FROM onboarding_events_project', + ); + expect(projectEvents).toMatchObject([ + { event: 'firstFlag', time_to_event: 180, project: 'test_project' }, + { event: 'firstPreLive', time_to_event: 240, project: 'test_project' }, + { event: 'firstLive', time_to_event: 300, project: 'test_project' }, + ]); +}); diff --git a/src/lib/features/onboarding/onboarding-store.ts b/src/lib/features/onboarding/onboarding-store.ts new file mode 100644 index 000000000000..767c2899a8fa --- /dev/null +++ b/src/lib/features/onboarding/onboarding-store.ts @@ -0,0 +1,136 @@ +import type { Db } from '../../db/db'; +import type { + IOnboardingStore, + OnboardingEvent, + SharedEvent, +} from './onboarding-store-type'; +import { millisecondsToSeconds } from 'date-fns'; + +type DBInstanceType = { + event: + | 'firstUserLogin' + | 'secondUserLogin' + | 'firstFlag' + | 'firstPreLive' + | 'firstLive'; + time_to_event: number; +}; + +type DBProjectType = { + event: 'firstFlag' | 'firstPreLive' | 'firstLive'; + time_to_event: number; + project: string; +}; + +const translateEvent = ( + event: OnboardingEvent['type'], +): DBInstanceType['event'] => { + if (event === 'flagCreated') { + return 'firstFlag'; + } + if (event === 'preLive') { + return 'firstPreLive'; + } + if (event === 'live') { + return 'firstLive'; + } + return event as DBInstanceType['event']; +}; + +const calculateTimeDifferenceInSeconds = (date1?: Date, date2?: Date) => { + if (date1 && date2) { + const diffInMilliseconds = date2.getTime() - date1.getTime(); + return millisecondsToSeconds(diffInMilliseconds); + } + return null; +}; + +export class OnboardingStore implements IOnboardingStore { + private db: Db; + + constructor(db: Db) { + this.db = db; + } + + async insert(event: OnboardingEvent): Promise { + await this.insertInstanceEvent(event); + if ( + event.type !== 'firstUserLogin' && + event.type !== 'secondUserLogin' + ) { + await this.insertProjectEvent(event); + } + } + + private async insertInstanceEvent(event: OnboardingEvent): Promise { + const dbEvent = translateEvent(event.type); + const exists = await this.db( + 'onboarding_events_instance', + ) + .where({ event: dbEvent }) + .first(); + + if (exists) return; + + const firstInstanceUser = await this.db('users') + .select('created_at') + .orderBy('created_at', 'asc') + .first(); + + if (!firstInstanceUser) return; + + const timeToEvent = calculateTimeDifferenceInSeconds( + firstInstanceUser.created_at, + new Date(), + ); + if (timeToEvent === null) return; + + await this.db('onboarding_events_instance').insert({ + event: dbEvent, + time_to_event: timeToEvent, + }); + } + + private async insertProjectEvent(event: SharedEvent): Promise { + const dbEvent = translateEvent(event.type) as DBProjectType['event']; + + const projectRow = await this.db<{ project: string; name: string }>( + 'features', + ) + .select('project') + .where({ name: event.flag }) + .first(); + + if (!projectRow) return; + + const project = projectRow.project; + + const exists = await this.db('onboarding_events_project') + .where({ event: dbEvent, project }) + .first(); + + if (exists) return; + + const projectCreatedAt = await this.db<{ + created_at: Date; + id: string; + }>('projects') + .select('created_at') + .where({ id: project }) + .first(); + + if (!projectCreatedAt) return; + + const timeToEvent = calculateTimeDifferenceInSeconds( + projectCreatedAt.created_at, + new Date(), + ); + if (timeToEvent === null) return; + + await this.db('onboarding_events_project').insert({ + event: dbEvent, + time_to_event: timeToEvent, + project: project, + }); + } +} diff --git a/src/lib/features/project/project-store.ts b/src/lib/features/project/project-store.ts index f926d5cb045c..4dfb65e778ea 100644 --- a/src/lib/features/project/project-store.ts +++ b/src/lib/features/project/project-store.ts @@ -302,7 +302,7 @@ class ProjectStore implements IProjectStore { project: IProjectInsert & IProjectSettings, ): Promise { const row = await this.db(TABLE) - .insert(this.fieldToRow(project)) + .insert({ ...this.fieldToRow(project), created_at: new Date() }) .returning('*'); const settingsRow = await this.db(SETTINGS_TABLE) .insert({ diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 38228a5e46ac..634bfedca46d 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -50,6 +50,7 @@ import type { IntegrationEventsStore } from '../features/integration-events/inte import { IFeatureCollaboratorsReadModel } from '../features/feature-toggle/types/feature-collaborators-read-model-type'; import type { IProjectReadModel } from '../features/project/project-read-model-type'; import { IOnboardingReadModel } from '../features/onboarding/onboarding-read-model-type'; +import { IOnboardingStore } from '../features/onboarding/onboarding-store-type'; export interface IUnleashStores { accessStore: IAccessStore; @@ -104,6 +105,7 @@ export interface IUnleashStores { featureCollaboratorsReadModel: IFeatureCollaboratorsReadModel; projectReadModel: IProjectReadModel; onboardingReadModel: IOnboardingReadModel; + onboardingStore: IOnboardingStore; } export { @@ -157,4 +159,5 @@ export { IOnboardingReadModel, type IntegrationEventsStore, type IProjectReadModel, + IOnboardingStore, }; From bdad753ac50fabb842bfd444d53c9284393e91bd Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 13:11:50 +0200 Subject: [PATCH 2/8] feat: onboarding store --- src/lib/features/onboarding/fake-onboarding-store.ts | 10 ++++++++++ src/test/fixtures/store.ts | 2 ++ 2 files changed, 12 insertions(+) diff --git a/src/lib/features/onboarding/fake-onboarding-store.ts b/src/lib/features/onboarding/fake-onboarding-store.ts index e69de29bb2d1..0fcd3c3983e9 100644 --- a/src/lib/features/onboarding/fake-onboarding-store.ts +++ b/src/lib/features/onboarding/fake-onboarding-store.ts @@ -0,0 +1,10 @@ +import type { + IOnboardingStore, + OnboardingEvent, +} from './onboarding-store-type'; + +export class FakeOnboardingStore implements IOnboardingStore { + async insert(event: OnboardingEvent): Promise { + return; + } +} diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts index 9cf6167dcd49..fa3d60eb052e 100644 --- a/src/test/fixtures/store.ts +++ b/src/test/fixtures/store.ts @@ -53,6 +53,7 @@ import { FakeLargestResourcesReadModel } from '../../lib/features/metrics/sizes/ import { FakeFeatureCollaboratorsReadModel } from '../../lib/features/feature-toggle/fake-feature-collaborators-read-model'; import { createFakeProjectReadModel } from '../../lib/features/project/createProjectReadModel'; import { FakeOnboardingReadModel } from '../../lib/features/onboarding/fake-onboarding-read-model'; +import { FakeOnboardingStore } from '../../lib/features/onboarding/fake-onboarding-store'; const db = { select: () => ({ @@ -115,6 +116,7 @@ const createStores: () => IUnleashStores = () => { integrationEventsStore: {} as IntegrationEventsStore, featureCollaboratorsReadModel: new FakeFeatureCollaboratorsReadModel(), projectReadModel: createFakeProjectReadModel(), + onboardingStore: new FakeOnboardingStore(), }; }; From 4e552557122a8ac66293ff9b62fd921f99647fdd Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 13:16:39 +0200 Subject: [PATCH 3/8] feat: onboarding store --- .../features/onboarding/onboarding-service.ts | 10 +++--- .../onboarding/onboarding-store-type.ts | 8 ++--- .../onboarding/onboarding-store.test.ts | 36 ++++++++++--------- .../features/onboarding/onboarding-store.ts | 24 ++++++------- 4 files changed, 42 insertions(+), 36 deletions(-) diff --git a/src/lib/features/onboarding/onboarding-service.ts b/src/lib/features/onboarding/onboarding-service.ts index 6c28b18231ec..3c4bf2570306 100644 --- a/src/lib/features/onboarding/onboarding-service.ts +++ b/src/lib/features/onboarding/onboarding-service.ts @@ -33,10 +33,12 @@ export class OnboardingService { if (!this.flagResolver.isEnabled('onboardingMetrics')) return; if (event.loginOrder === 0) { - await this.onboardingStore.insert({ type: 'firstUserLogin' }); + await this.onboardingStore.insert({ type: 'first-user-login' }); } if (event.loginOrder === 1) { - await this.onboardingStore.insert({ type: 'secondUserLogin' }); + await this.onboardingStore.insert({ + type: 'second-user-login', + }); } }); this.eventBus.on(STAGE_ENTERED, async (stage: NewStage) => { @@ -44,12 +46,12 @@ export class OnboardingService { if (stage.stage === 'initial') { await this.onboardingStore.insert({ - type: 'flagCreated', + type: 'flag-created', flag: stage.feature, }); } else if (stage.stage === 'pre-live') { await this.onboardingStore.insert({ - type: 'preLive', + type: 'pre-live', flag: stage.feature, }); } else if (stage.stage === 'live') { diff --git a/src/lib/features/onboarding/onboarding-store-type.ts b/src/lib/features/onboarding/onboarding-store-type.ts index 394a905e6ec8..9d72c0ff06e3 100644 --- a/src/lib/features/onboarding/onboarding-store-type.ts +++ b/src/lib/features/onboarding/onboarding-store-type.ts @@ -1,10 +1,10 @@ export type SharedEvent = - | { type: 'flagCreated'; flag: string } - | { type: 'preLive'; flag: string } + | { type: 'flag-created'; flag: string } + | { type: 'pre-live'; flag: string } | { type: 'live'; flag: string }; export type InstanceEvent = - | { type: 'firstUserLogin' } - | { type: 'secondUserLogin' }; + | { type: 'first-user-login' } + | { type: 'second-user-login' }; export type OnboardingEvent = SharedEvent | InstanceEvent; export interface IOnboardingStore { diff --git a/src/lib/features/onboarding/onboarding-store.test.ts b/src/lib/features/onboarding/onboarding-store.test.ts index bc395c7bfd4a..7233179b6206 100644 --- a/src/lib/features/onboarding/onboarding-store.test.ts +++ b/src/lib/features/onboarding/onboarding-store.test.ts @@ -32,17 +32,17 @@ test('Storing onboarding events', async () => { }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'firstUserLogin' }); + await onboardingStore.insert({ type: 'first-user-login' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'secondUserLogin' }); + await onboardingStore.insert({ type: 'second-user-login' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'flagCreated', flag: 'test' }); - await onboardingStore.insert({ type: 'flagCreated', flag: 'test' }); - await onboardingStore.insert({ type: 'flagCreated', flag: 'invalid' }); + await onboardingStore.insert({ type: 'flag-created', flag: 'test' }); + await onboardingStore.insert({ type: 'flag-created', flag: 'test' }); + await onboardingStore.insert({ type: 'flag-created', flag: 'invalid' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'preLive', flag: 'test' }); - await onboardingStore.insert({ type: 'preLive', flag: 'test' }); - await onboardingStore.insert({ type: 'preLive', flag: 'invalid' }); + await onboardingStore.insert({ type: 'pre-live', flag: 'test' }); + await onboardingStore.insert({ type: 'pre-live', flag: 'test' }); + await onboardingStore.insert({ type: 'pre-live', flag: 'invalid' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); await onboardingStore.insert({ type: 'live', flag: 'test' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); @@ -54,19 +54,23 @@ test('Storing onboarding events', async () => { 'SELECT * FROM onboarding_events_instance', ); expect(instanceEvents).toMatchObject([ - { event: 'firstUserLogin', time_to_event: 60 }, - { event: 'secondUserLogin', time_to_event: 120 }, - { event: 'firstFlag', time_to_event: 180 }, - { event: 'firstPreLive', time_to_event: 240 }, - { event: 'firstLive', time_to_event: 300 }, + { event: 'first-user-login', time_to_event: 60 }, + { event: 'second-user-login', time_to_event: 120 }, + { event: 'first-flag', time_to_event: 180 }, + { event: 'first-pre-live', time_to_event: 240 }, + { event: 'first-live', time_to_event: 300 }, ]); const { rows: projectEvents } = await db.rawDatabase.raw( 'SELECT * FROM onboarding_events_project', ); expect(projectEvents).toMatchObject([ - { event: 'firstFlag', time_to_event: 180, project: 'test_project' }, - { event: 'firstPreLive', time_to_event: 240, project: 'test_project' }, - { event: 'firstLive', time_to_event: 300, project: 'test_project' }, + { event: 'first-flag', time_to_event: 180, project: 'test_project' }, + { + event: 'first-pre-live', + time_to_event: 240, + project: 'test_project', + }, + { event: 'first-live', time_to_event: 300, project: 'test_project' }, ]); }); diff --git a/src/lib/features/onboarding/onboarding-store.ts b/src/lib/features/onboarding/onboarding-store.ts index 767c2899a8fa..f635a31e4cd0 100644 --- a/src/lib/features/onboarding/onboarding-store.ts +++ b/src/lib/features/onboarding/onboarding-store.ts @@ -8,11 +8,11 @@ import { millisecondsToSeconds } from 'date-fns'; type DBInstanceType = { event: - | 'firstUserLogin' - | 'secondUserLogin' - | 'firstFlag' - | 'firstPreLive' - | 'firstLive'; + | 'first-user-login' + | 'second-user-login' + | 'first-flag' + | 'first-pre-live' + | 'first-live'; time_to_event: number; }; @@ -25,14 +25,14 @@ type DBProjectType = { const translateEvent = ( event: OnboardingEvent['type'], ): DBInstanceType['event'] => { - if (event === 'flagCreated') { - return 'firstFlag'; + if (event === 'flag-created') { + return 'first-flag'; } - if (event === 'preLive') { - return 'firstPreLive'; + if (event === 'pre-live') { + return 'first-pre-live'; } if (event === 'live') { - return 'firstLive'; + return 'first-live'; } return event as DBInstanceType['event']; }; @@ -55,8 +55,8 @@ export class OnboardingStore implements IOnboardingStore { async insert(event: OnboardingEvent): Promise { await this.insertInstanceEvent(event); if ( - event.type !== 'firstUserLogin' && - event.type !== 'secondUserLogin' + event.type !== 'first-user-login' && + event.type !== 'second-user-login' ) { await this.insertProjectEvent(event); } From a6831ce39d599a009e0c8f4ab5c378468d34ad19 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 17:09:36 +0200 Subject: [PATCH 4/8] feat: onboarding store --- src/lib/db/user-store.ts | 9 + ...test.ts => onboarding-service.e2e.test.ts} | 35 ++-- .../features/onboarding/onboarding-service.ts | 86 +++++++++- .../onboarding/onboarding-store-type.ts | 20 ++- .../features/onboarding/onboarding-store.ts | 160 ++++++------------ .../project/fake-project-read-model.ts | 6 + .../project/project-read-model-type.ts | 4 + .../features/project/project-read-model.ts | 39 +++++ src/lib/types/stores/user-store.ts | 1 + src/test/fixtures/fake-user-store.ts | 16 ++ 10 files changed, 236 insertions(+), 140 deletions(-) rename src/lib/features/onboarding/{onboarding-store.test.ts => onboarding-service.e2e.test.ts} (62%) diff --git a/src/lib/db/user-store.ts b/src/lib/db/user-store.ts index f5a2f8acdbc4..2be081ca3b39 100644 --- a/src/lib/db/user-store.ts +++ b/src/lib/db/user-store.ts @@ -298,6 +298,15 @@ class UserStore implements IUserStore { const row = await this.activeUsers().where({ id }).first(); return rowToUser(row); } + + async getFirstUserDate(): Promise { + const firstInstanceUser = await this.db('users') + .select('created_at') + .orderBy('created_at', 'asc') + .first(); + + return firstInstanceUser ? firstInstanceUser.created_at : null; + } } module.exports = UserStore; diff --git a/src/lib/features/onboarding/onboarding-store.test.ts b/src/lib/features/onboarding/onboarding-service.e2e.test.ts similarity index 62% rename from src/lib/features/onboarding/onboarding-store.test.ts rename to src/lib/features/onboarding/onboarding-service.e2e.test.ts index 7233179b6206..a0768700c70c 100644 --- a/src/lib/features/onboarding/onboarding-store.test.ts +++ b/src/lib/features/onboarding/onboarding-service.e2e.test.ts @@ -2,13 +2,24 @@ import type { 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 { createTestConfig } from '../../../test/config/test-config'; let db: ITestDb; let stores: IUnleashStores; +let onboardService: OnboardingService; beforeAll(async () => { db = await dbInit('onboarding_store', getLogger); + const config = createTestConfig({ + experimental: { flags: { onboardingMetrics: true } }, + }); stores = db.stores; + const { userStore, onboardingStore, projectReadModel } = stores; + onboardService = new OnboardingService( + { onboardingStore, userStore, projectReadModel }, + config, + ); }); afterAll(async () => { @@ -22,7 +33,7 @@ beforeEach(async () => { test('Storing onboarding events', async () => { jest.useFakeTimers(); jest.setSystemTime(new Date()); - const { userStore, onboardingStore, featureToggleStore, projectStore } = + const { userStore, featureToggleStore, projectStore, projectReadModel } = stores; const user = await userStore.insert({}); await projectStore.create({ id: 'test_project', name: 'irrelevant' }); @@ -32,23 +43,23 @@ test('Storing onboarding events', async () => { }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'first-user-login' }); + await onboardService.insert({ type: 'first-user-login' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'second-user-login' }); + await onboardService.insert({ type: 'second-user-login' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'flag-created', flag: 'test' }); - await onboardingStore.insert({ type: 'flag-created', flag: 'test' }); - await onboardingStore.insert({ type: 'flag-created', flag: 'invalid' }); + await onboardService.insert({ type: 'flag-created', flag: 'test' }); + await onboardService.insert({ type: 'flag-created', flag: 'test' }); + await onboardService.insert({ type: 'flag-created', flag: 'invalid' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'pre-live', flag: 'test' }); - await onboardingStore.insert({ type: 'pre-live', flag: 'test' }); - await onboardingStore.insert({ type: 'pre-live', flag: 'invalid' }); + await onboardService.insert({ type: 'pre-live', flag: 'test' }); + await onboardService.insert({ type: 'pre-live', flag: 'test' }); + await onboardService.insert({ type: 'pre-live', flag: 'invalid' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'live', flag: 'test' }); + await onboardService.insert({ type: 'live', flag: 'test' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'live', flag: 'test' }); + await onboardService.insert({ type: 'live', flag: 'test' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardingStore.insert({ type: 'live', flag: 'invalid' }); + await onboardService.insert({ type: 'live', flag: 'invalid' }); const { rows: instanceEvents } = await db.rawDatabase.raw( 'SELECT * FROM onboarding_events_instance', diff --git a/src/lib/features/onboarding/onboarding-service.ts b/src/lib/features/onboarding/onboarding-service.ts index 3c4bf2570306..bb88262909dc 100644 --- a/src/lib/features/onboarding/onboarding-service.ts +++ b/src/lib/features/onboarding/onboarding-service.ts @@ -1,9 +1,19 @@ -import type { IFlagResolver, IUnleashConfig } from '../../types'; +import type { + IFlagResolver, + IProjectReadModel, + IUnleashConfig, + IUserStore, +} from '../../types'; import type EventEmitter from 'events'; import type { Logger } from '../../logger'; import { STAGE_ENTERED, USER_LOGIN } from '../../metric-events'; import type { NewStage } from '../feature-lifecycle/feature-lifecycle-store-type'; -import type { IOnboardingStore } from './onboarding-store-type'; +import type { + InstanceEvent, + IOnboardingStore, + ProjectEvent, +} from './onboarding-store-type'; +import { millisecondsToSeconds } from 'date-fns'; export class OnboardingService { private flagResolver: IFlagResolver; @@ -14,8 +24,20 @@ export class OnboardingService { private onboardingStore: IOnboardingStore; + private projectReadModel: IProjectReadModel; + + private userStore: IUserStore; + constructor( - { onboardingStore }: { onboardingStore: IOnboardingStore }, + { + onboardingStore, + projectReadModel, + userStore, + }: { + onboardingStore: IOnboardingStore; + projectReadModel: IProjectReadModel; + userStore: IUserStore; + }, { flagResolver, eventBus, @@ -23,6 +45,8 @@ export class OnboardingService { }: Pick, ) { this.onboardingStore = onboardingStore; + this.projectReadModel = projectReadModel; + this.userStore = userStore; this.flagResolver = flagResolver; this.eventBus = eventBus; this.logger = getLogger('onboarding/onboarding-service.ts'); @@ -33,10 +57,10 @@ export class OnboardingService { if (!this.flagResolver.isEnabled('onboardingMetrics')) return; if (event.loginOrder === 0) { - await this.onboardingStore.insert({ type: 'first-user-login' }); + await this.insert({ type: 'first-user-login' }); } if (event.loginOrder === 1) { - await this.onboardingStore.insert({ + await this.insert({ type: 'second-user-login', }); } @@ -45,21 +69,67 @@ export class OnboardingService { if (!this.flagResolver.isEnabled('onboardingMetrics')) return; if (stage.stage === 'initial') { - await this.onboardingStore.insert({ + await this.insert({ type: 'flag-created', flag: stage.feature, }); } else if (stage.stage === 'pre-live') { - await this.onboardingStore.insert({ + await this.insert({ type: 'pre-live', flag: stage.feature, }); } else if (stage.stage === 'live') { - await this.onboardingStore.insert({ + await this.insert({ type: 'live', flag: stage.feature, }); } }); } + + async insert( + event: + | { flag: string; type: ProjectEvent['type'] } + | { type: 'first-user-login' | 'second-user-login' }, + ): Promise { + await this.insertInstanceEvent(event); + if ('flag' in event) { + await this.insertProjectEvent(event); + } + } + + private async insertInstanceEvent(event: { + flag?: string; + type: InstanceEvent['type']; + }): Promise { + const firstInstanceUserDate = await this.userStore.getFirstUserDate(); + if (!firstInstanceUserDate) return; + + const timeToEvent = millisecondsToSeconds( + new Date().getTime() - firstInstanceUserDate.getTime(), + ); + await this.onboardingStore.insertInstanceEvent({ + type: event.type, + timeToEvent, + }); + } + + private async insertProjectEvent(event: { + flag: string; + type: ProjectEvent['type']; + }): Promise { + const project = await this.projectReadModel.getFeatureProject( + event.flag, + ); + if (!project) return; + + const timeToEvent = millisecondsToSeconds( + new Date().getTime() - project.createdAt.getTime(), + ); + await this.onboardingStore.insertProjectEvent({ + type: event.type, + timeToEvent, + project: project.project, + }); + } } diff --git a/src/lib/features/onboarding/onboarding-store-type.ts b/src/lib/features/onboarding/onboarding-store-type.ts index 9d72c0ff06e3..bd48b74e3f7f 100644 --- a/src/lib/features/onboarding/onboarding-store-type.ts +++ b/src/lib/features/onboarding/onboarding-store-type.ts @@ -1,12 +1,16 @@ -export type SharedEvent = - | { type: 'flag-created'; flag: string } - | { type: 'pre-live'; flag: string } - | { type: 'live'; flag: string }; +export type ProjectEvent = + | { type: 'flag-created'; project: string; timeToEvent: number } + | { type: 'pre-live'; project: string; timeToEvent: number } + | { type: 'live'; project: string; timeToEvent: number }; export type InstanceEvent = - | { type: 'first-user-login' } - | { type: 'second-user-login' }; -export type OnboardingEvent = SharedEvent | InstanceEvent; + | { type: 'flag-created'; timeToEvent: number } + | { type: 'pre-live'; timeToEvent: number } + | { type: 'live'; timeToEvent: number } + | { type: 'first-user-login'; timeToEvent: number } + | { type: 'second-user-login'; timeToEvent: number }; export interface IOnboardingStore { - insert(event: OnboardingEvent): Promise; + insertProjectEvent(event: ProjectEvent): Promise; + + insertInstanceEvent(event: InstanceEvent): Promise; } diff --git a/src/lib/features/onboarding/onboarding-store.ts b/src/lib/features/onboarding/onboarding-store.ts index f635a31e4cd0..af132bdb87ca 100644 --- a/src/lib/features/onboarding/onboarding-store.ts +++ b/src/lib/features/onboarding/onboarding-store.ts @@ -1,48 +1,45 @@ import type { Db } from '../../db/db'; import type { + InstanceEvent, IOnboardingStore, - OnboardingEvent, - SharedEvent, + ProjectEvent, } from './onboarding-store-type'; -import { millisecondsToSeconds } from 'date-fns'; -type DBInstanceType = { - event: - | 'first-user-login' - | 'second-user-login' - | 'first-flag' - | 'first-pre-live' - | 'first-live'; - time_to_event: number; -}; - -type DBProjectType = { - event: 'firstFlag' | 'firstPreLive' | 'firstLive'; +export type DBProjectEvent = { + event: 'first-flag' | 'first-pre-live' | 'first-live'; time_to_event: number; project: string; }; -const translateEvent = ( - event: OnboardingEvent['type'], -): DBInstanceType['event'] => { - if (event === 'flag-created') { - return 'first-flag'; - } - if (event === 'pre-live') { - return 'first-pre-live'; - } - if (event === 'live') { - return 'first-live'; - } - return event as DBInstanceType['event']; +export type DBInstanceEvent = + | { + event: 'first-flag' | 'first-pre-live' | 'first-live'; + time_to_event: number; + project?: string; + } + | { + event: 'first-user-login' | 'second-user-login'; + time_to_event: number; + }; + +const projectEventLookup: Record< + ProjectEvent['type'], + DBProjectEvent['event'] +> = { + 'flag-created': 'first-flag', + 'pre-live': 'first-pre-live', + live: 'first-live', }; -const calculateTimeDifferenceInSeconds = (date1?: Date, date2?: Date) => { - if (date1 && date2) { - const diffInMilliseconds = date2.getTime() - date1.getTime(); - return millisecondsToSeconds(diffInMilliseconds); - } - return null; +const instanceEventLookup: Record< + InstanceEvent['type'], + DBInstanceEvent['event'] +> = { + 'flag-created': 'first-flag', + 'pre-live': 'first-pre-live', + live: 'first-live', + 'first-user-login': 'first-user-login', + 'second-user-login': 'second-user-login', }; export class OnboardingStore implements IOnboardingStore { @@ -52,85 +49,24 @@ export class OnboardingStore implements IOnboardingStore { this.db = db; } - async insert(event: OnboardingEvent): Promise { - await this.insertInstanceEvent(event); - if ( - event.type !== 'first-user-login' && - event.type !== 'second-user-login' - ) { - await this.insertProjectEvent(event); - } + async insertInstanceEvent(event: InstanceEvent): Promise { + await this.db('onboarding_events_instance') + .insert({ + event: instanceEventLookup[event.type], + time_to_event: event.timeToEvent, + }) + .onConflict() + .ignore(); } - private async insertInstanceEvent(event: OnboardingEvent): Promise { - const dbEvent = translateEvent(event.type); - const exists = await this.db( - 'onboarding_events_instance', - ) - .where({ event: dbEvent }) - .first(); - - if (exists) return; - - const firstInstanceUser = await this.db('users') - .select('created_at') - .orderBy('created_at', 'asc') - .first(); - - if (!firstInstanceUser) return; - - const timeToEvent = calculateTimeDifferenceInSeconds( - firstInstanceUser.created_at, - new Date(), - ); - if (timeToEvent === null) return; - - await this.db('onboarding_events_instance').insert({ - event: dbEvent, - time_to_event: timeToEvent, - }); - } - - private async insertProjectEvent(event: SharedEvent): Promise { - const dbEvent = translateEvent(event.type) as DBProjectType['event']; - - const projectRow = await this.db<{ project: string; name: string }>( - 'features', - ) - .select('project') - .where({ name: event.flag }) - .first(); - - if (!projectRow) return; - - const project = projectRow.project; - - const exists = await this.db('onboarding_events_project') - .where({ event: dbEvent, project }) - .first(); - - if (exists) return; - - const projectCreatedAt = await this.db<{ - created_at: Date; - id: string; - }>('projects') - .select('created_at') - .where({ id: project }) - .first(); - - if (!projectCreatedAt) return; - - const timeToEvent = calculateTimeDifferenceInSeconds( - projectCreatedAt.created_at, - new Date(), - ); - if (timeToEvent === null) return; - - await this.db('onboarding_events_project').insert({ - event: dbEvent, - time_to_event: timeToEvent, - project: project, - }); + async insertProjectEvent(event: ProjectEvent): Promise { + await this.db('onboarding_events_project') + .insert({ + event: projectEventLookup[event.type], + time_to_event: event.timeToEvent, + project: event.project, + }) + .onConflict() + .ignore(); } } diff --git a/src/lib/features/project/fake-project-read-model.ts b/src/lib/features/project/fake-project-read-model.ts index 9b7b81287a8d..203d7484a4e5 100644 --- a/src/lib/features/project/fake-project-read-model.ts +++ b/src/lib/features/project/fake-project-read-model.ts @@ -5,6 +5,12 @@ import type { } from './project-read-model-type'; export class FakeProjectReadModel implements IProjectReadModel { + getFeatureProject(): Promise { + return Promise.resolve(null); + } + getProjectCreationTime(): Promise { + return Promise.resolve(null); + } getProjectsForAdminUi(): Promise { return Promise.resolve([]); } diff --git a/src/lib/features/project/project-read-model-type.ts b/src/lib/features/project/project-read-model-type.ts index ff5e31284c72..ef657ad70f8b 100644 --- a/src/lib/features/project/project-read-model-type.ts +++ b/src/lib/features/project/project-read-model-type.ts @@ -37,4 +37,8 @@ export interface IProjectReadModel { getProjectsForInsights( query?: IProjectQuery, ): Promise; + getProjectCreationTime(project: string): Promise; + getFeatureProject( + featureName: string, + ): Promise<{ project: string; createdAt: Date } | null>; } diff --git a/src/lib/features/project/project-read-model.ts b/src/lib/features/project/project-read-model.ts index 4bdd703e067a..47b7d6c621d5 100644 --- a/src/lib/features/project/project-read-model.ts +++ b/src/lib/features/project/project-read-model.ts @@ -62,6 +62,45 @@ export class ProjectReadModel implements IProjectReadModel { this.flagResolver = flagResolver; } + async getProjectCreationTime(project: string): Promise { + const projectCreatedAt = await this.db<{ + created_at: Date; + id: string; + }>('projects') + .select('created_at') + .where({ id: project }) + .first(); + + return projectCreatedAt ? projectCreatedAt.created_at : null; + } + + async getFeatureProject( + featureName: string, + ): Promise<{ project: string; createdAt: Date } | null> { + const projectRow = await this.db<{ project: string; name: string }>( + 'features', + ) + .select('project') + .where({ name: featureName }) + .first(); + + if (!projectRow) return null; + + const project = projectRow.project; + + const projectCreatedAt = await this.db<{ + created_at: Date; + id: string; + }>('projects') + .select('created_at') + .where({ id: project }) + .first(); + + if (!projectCreatedAt) return null; + + return { project, createdAt: projectCreatedAt.created_at }; + } + async getProjectsForAdminUi( query?: IProjectQuery, userId?: number, diff --git a/src/lib/types/stores/user-store.ts b/src/lib/types/stores/user-store.ts index 73f5484d6c38..2ca1f39a1e32 100644 --- a/src/lib/types/stores/user-store.ts +++ b/src/lib/types/stores/user-store.ts @@ -34,6 +34,7 @@ export interface IUserStore extends Store { disallowNPreviousPasswords: number, ): Promise; getPasswordsPreviouslyUsed(userId: number): Promise; + getFirstUserDate(): Promise; incLoginAttempts(user: IUser): Promise; successfullyLogin(user: IUser): Promise; count(): Promise; diff --git a/src/test/fixtures/fake-user-store.ts b/src/test/fixtures/fake-user-store.ts index 0dda7cbe8142..a201d70c4029 100644 --- a/src/test/fixtures/fake-user-store.ts +++ b/src/test/fixtures/fake-user-store.ts @@ -17,6 +17,22 @@ class UserStoreMock implements IUserStore { this.previousPasswords = new Map(); } + async getFirstUserDate(): Promise { + if (this.data.length === 0) { + return null; + } + const oldestUser = this.data.reduce((oldest, user) => { + if (!user.createdAt) { + return oldest; + } + return !oldest.createdAt || user.createdAt < oldest.createdAt + ? user + : oldest; + }, this.data[0]); + + return oldestUser.createdAt || null; + } + getPasswordsPreviouslyUsed(userId: number): Promise { return Promise.resolve(this.previousPasswords.get(userId) || []); } From 2af57f00ab04470caba98044786609fcc6c761cb Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 17:12:26 +0200 Subject: [PATCH 5/8] feat: onboarding store --- .../features/project/project-read-model.ts | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/lib/features/project/project-read-model.ts b/src/lib/features/project/project-read-model.ts index 47b7d6c621d5..52abc4112b94 100644 --- a/src/lib/features/project/project-read-model.ts +++ b/src/lib/features/project/project-read-model.ts @@ -77,28 +77,17 @@ export class ProjectReadModel implements IProjectReadModel { async getFeatureProject( featureName: string, ): Promise<{ project: string; createdAt: Date } | null> { - const projectRow = await this.db<{ project: string; name: string }>( + const result = await this.db<{ project: string; created_at: Date }>( 'features', ) - .select('project') - .where({ name: featureName }) - .first(); - - if (!projectRow) return null; - - const project = projectRow.project; - - const projectCreatedAt = await this.db<{ - created_at: Date; - id: string; - }>('projects') - .select('created_at') - .where({ id: project }) + .join('projects', 'features.project', '=', 'projects.id') + .select('features.project', 'projects.created_at') + .where('features.name', featureName) .first(); - if (!projectCreatedAt) return null; + if (!result) return null; - return { project, createdAt: projectCreatedAt.created_at }; + return { project: result.project, createdAt: result.created_at }; } async getProjectsForAdminUi( From f7005f56aff8da23116e08b74c2255d8550034de Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 17:13:40 +0200 Subject: [PATCH 6/8] feat: onboarding store --- .../onboarding/onboarding-service.e2e.test.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/lib/features/onboarding/onboarding-service.e2e.test.ts b/src/lib/features/onboarding/onboarding-service.e2e.test.ts index a0768700c70c..3e751c57ac12 100644 --- a/src/lib/features/onboarding/onboarding-service.e2e.test.ts +++ b/src/lib/features/onboarding/onboarding-service.e2e.test.ts @@ -7,7 +7,7 @@ import { createTestConfig } from '../../../test/config/test-config'; let db: ITestDb; let stores: IUnleashStores; -let onboardService: OnboardingService; +let onboardingService: OnboardingService; beforeAll(async () => { db = await dbInit('onboarding_store', getLogger); @@ -16,7 +16,7 @@ beforeAll(async () => { }); stores = db.stores; const { userStore, onboardingStore, projectReadModel } = stores; - onboardService = new OnboardingService( + onboardingService = new OnboardingService( { onboardingStore, userStore, projectReadModel }, config, ); @@ -43,23 +43,23 @@ test('Storing onboarding events', async () => { }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'first-user-login' }); + await onboardingService.insert({ type: 'first-user-login' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'second-user-login' }); + await onboardingService.insert({ type: 'second-user-login' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'flag-created', flag: 'test' }); - await onboardService.insert({ type: 'flag-created', flag: 'test' }); - await onboardService.insert({ type: 'flag-created', flag: 'invalid' }); + await onboardingService.insert({ type: 'flag-created', flag: 'test' }); + await onboardingService.insert({ type: 'flag-created', flag: 'test' }); + await onboardingService.insert({ type: 'flag-created', flag: 'invalid' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'pre-live', flag: 'test' }); - await onboardService.insert({ type: 'pre-live', flag: 'test' }); - await onboardService.insert({ type: 'pre-live', flag: 'invalid' }); + await onboardingService.insert({ type: 'pre-live', flag: 'test' }); + await onboardingService.insert({ type: 'pre-live', flag: 'test' }); + await onboardingService.insert({ type: 'pre-live', flag: 'invalid' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'live', flag: 'test' }); + await onboardingService.insert({ type: 'live', flag: 'test' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'live', flag: 'test' }); + await onboardingService.insert({ type: 'live', flag: 'test' }); jest.advanceTimersByTime(minutesToMilliseconds(1)); - await onboardService.insert({ type: 'live', flag: 'invalid' }); + await onboardingService.insert({ type: 'live', flag: 'invalid' }); const { rows: instanceEvents } = await db.rawDatabase.raw( 'SELECT * FROM onboarding_events_instance', From b3201168e8568bc93267c3df07fc704ff13dd755 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 17:15:51 +0200 Subject: [PATCH 7/8] feat: onboarding store --- src/lib/features/project/fake-project-read-model.ts | 5 +---- src/lib/features/project/project-read-model-type.ts | 1 - src/lib/features/project/project-read-model.ts | 12 ------------ 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/lib/features/project/fake-project-read-model.ts b/src/lib/features/project/fake-project-read-model.ts index 203d7484a4e5..1d9e40b8f7cd 100644 --- a/src/lib/features/project/fake-project-read-model.ts +++ b/src/lib/features/project/fake-project-read-model.ts @@ -5,10 +5,7 @@ import type { } from './project-read-model-type'; export class FakeProjectReadModel implements IProjectReadModel { - getFeatureProject(): Promise { - return Promise.resolve(null); - } - getProjectCreationTime(): Promise { + getFeatureProject(): Promise<{ project: string; createdAt: Date } | null> { return Promise.resolve(null); } getProjectsForAdminUi(): Promise { diff --git a/src/lib/features/project/project-read-model-type.ts b/src/lib/features/project/project-read-model-type.ts index ef657ad70f8b..4487d31f0839 100644 --- a/src/lib/features/project/project-read-model-type.ts +++ b/src/lib/features/project/project-read-model-type.ts @@ -37,7 +37,6 @@ export interface IProjectReadModel { getProjectsForInsights( query?: IProjectQuery, ): Promise; - getProjectCreationTime(project: string): Promise; getFeatureProject( featureName: string, ): Promise<{ project: string; createdAt: Date } | null>; diff --git a/src/lib/features/project/project-read-model.ts b/src/lib/features/project/project-read-model.ts index 52abc4112b94..c793543d81ba 100644 --- a/src/lib/features/project/project-read-model.ts +++ b/src/lib/features/project/project-read-model.ts @@ -62,18 +62,6 @@ export class ProjectReadModel implements IProjectReadModel { this.flagResolver = flagResolver; } - async getProjectCreationTime(project: string): Promise { - const projectCreatedAt = await this.db<{ - created_at: Date; - id: string; - }>('projects') - .select('created_at') - .where({ id: project }) - .first(); - - return projectCreatedAt ? projectCreatedAt.created_at : null; - } - async getFeatureProject( featureName: string, ): Promise<{ project: string; createdAt: Date } | null> { From 5a2f441a617f113a93fb36f626f31103734f0a83 Mon Sep 17 00:00:00 2001 From: kwasniew Date: Fri, 30 Aug 2024 17:18:10 +0200 Subject: [PATCH 8/8] feat: onboarding store --- src/lib/features/onboarding/fake-onboarding-store.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/features/onboarding/fake-onboarding-store.ts b/src/lib/features/onboarding/fake-onboarding-store.ts index 0fcd3c3983e9..979d15de0a49 100644 --- a/src/lib/features/onboarding/fake-onboarding-store.ts +++ b/src/lib/features/onboarding/fake-onboarding-store.ts @@ -1,10 +1,14 @@ import type { + InstanceEvent, IOnboardingStore, - OnboardingEvent, + ProjectEvent, } from './onboarding-store-type'; export class FakeOnboardingStore implements IOnboardingStore { - async insert(event: OnboardingEvent): Promise { - return; + insertProjectEvent(event: ProjectEvent): Promise { + throw new Error('Method not implemented.'); + } + insertInstanceEvent(event: InstanceEvent): Promise { + throw new Error('Method not implemented.'); } }