1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

feat: onboarding table to prometheus (#8034)

Instead of lifecycle table, we take the metrics directly from onboarding
tables.
This commit is contained in:
Jaanus Sellin 2024-09-02 11:41:00 +03:00 committed by GitHub
parent eb55067983
commit 2a6b2e98e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 99 additions and 156 deletions

View File

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

View File

@ -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<InstanceOnboarding> {
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<Array<ProjectOnboarding>> {
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<ProjectOnboarding> = [];
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;
}
}