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:
parent
eb55067983
commit
2a6b2e98e0
@ -1,28 +1,19 @@
|
|||||||
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
import dbInit, { type ITestDb } from '../../../test/e2e/helpers/database-init';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
import getLogger from '../../../test/fixtures/no-logger';
|
||||||
import type {
|
import type { IOnboardingStore } from '../../types';
|
||||||
IFeatureLifecycleStore,
|
|
||||||
IFeatureToggleStore,
|
|
||||||
IUserStore,
|
|
||||||
} from '../../types';
|
|
||||||
import { OnboardingReadModel } from './onboarding-read-model';
|
import { OnboardingReadModel } from './onboarding-read-model';
|
||||||
import type { IOnboardingReadModel } from './onboarding-read-model-type';
|
import type { IOnboardingReadModel } from './onboarding-read-model-type';
|
||||||
import { minutesToMilliseconds } from 'date-fns';
|
|
||||||
|
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
let onboardingReadModel: IOnboardingReadModel;
|
let onboardingReadModel: IOnboardingReadModel;
|
||||||
let userStore: IUserStore;
|
let onBoardingStore: IOnboardingStore;
|
||||||
let lifecycleStore: IFeatureLifecycleStore;
|
|
||||||
let featureToggleStore: IFeatureToggleStore;
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('onboarding_read_model', getLogger, {
|
db = await dbInit('onboarding_read_model', getLogger, {
|
||||||
experimental: { flags: { onboardingMetrics: true } },
|
experimental: { flags: { onboardingMetrics: true } },
|
||||||
});
|
});
|
||||||
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
|
onboardingReadModel = new OnboardingReadModel(db.rawDatabase);
|
||||||
userStore = db.stores.userStore;
|
onBoardingStore = db.stores.onboardingStore;
|
||||||
lifecycleStore = db.stores.featureLifecycleStore;
|
|
||||||
featureToggleStore = db.stores.featureToggleStore;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -31,13 +22,9 @@ afterAll(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {});
|
||||||
await userStore.deleteAll();
|
|
||||||
jest.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('can get onboarding durations', async () => {
|
test('can get instance onboarding durations', async () => {
|
||||||
jest.useFakeTimers();
|
|
||||||
const initialResult =
|
const initialResult =
|
||||||
await onboardingReadModel.getInstanceOnboardingMetrics();
|
await onboardingReadModel.getInstanceOnboardingMetrics();
|
||||||
expect(initialResult).toMatchObject({
|
expect(initialResult).toMatchObject({
|
||||||
@ -48,8 +35,10 @@ test('can get onboarding durations', async () => {
|
|||||||
firstLive: null,
|
firstLive: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const firstUser = await userStore.insert({});
|
await onBoardingStore.insertInstanceEvent({
|
||||||
await userStore.successfullyLogin(firstUser);
|
type: 'first-user-login',
|
||||||
|
timeToEvent: 0,
|
||||||
|
});
|
||||||
|
|
||||||
const firstLoginResult =
|
const firstLoginResult =
|
||||||
await onboardingReadModel.getInstanceOnboardingMetrics();
|
await onboardingReadModel.getInstanceOnboardingMetrics();
|
||||||
@ -58,42 +47,25 @@ test('can get onboarding durations', async () => {
|
|||||||
secondLogin: null,
|
secondLogin: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
await onBoardingStore.insertInstanceEvent({
|
||||||
|
type: 'second-user-login',
|
||||||
const secondUser = await userStore.insert({});
|
timeToEvent: 10,
|
||||||
await userStore.successfullyLogin(secondUser);
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
|
||||||
|
|
||||||
await featureToggleStore.create('default', {
|
|
||||||
name: 'test',
|
|
||||||
createdByUserId: secondUser.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await lifecycleStore.insert([
|
await onBoardingStore.insertInstanceEvent({
|
||||||
{
|
type: 'flag-created',
|
||||||
feature: 'test',
|
timeToEvent: 20,
|
||||||
stage: 'initial',
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
await onBoardingStore.insertInstanceEvent({
|
||||||
|
type: 'pre-live',
|
||||||
|
timeToEvent: 30,
|
||||||
|
});
|
||||||
|
|
||||||
await lifecycleStore.insert([
|
await onBoardingStore.insertInstanceEvent({
|
||||||
{
|
type: 'live',
|
||||||
feature: 'test',
|
timeToEvent: 40,
|
||||||
stage: 'pre-live',
|
});
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
|
||||||
|
|
||||||
await lifecycleStore.insert([
|
|
||||||
{
|
|
||||||
feature: 'test',
|
|
||||||
stage: 'live',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const secondLoginResult =
|
const secondLoginResult =
|
||||||
await onboardingReadModel.getInstanceOnboardingMetrics();
|
await onboardingReadModel.getInstanceOnboardingMetrics();
|
||||||
@ -104,6 +76,26 @@ test('can get onboarding durations', async () => {
|
|||||||
firstPreLive: 30,
|
firstPreLive: 30,
|
||||||
firstLive: 40,
|
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 =
|
const projectOnboardingResult =
|
||||||
await onboardingReadModel.getProjectsOnboardingMetrics();
|
await onboardingReadModel.getProjectsOnboardingMetrics();
|
||||||
|
@ -4,14 +4,19 @@ import type {
|
|||||||
InstanceOnboarding,
|
InstanceOnboarding,
|
||||||
ProjectOnboarding,
|
ProjectOnboarding,
|
||||||
} from './onboarding-read-model-type';
|
} from './onboarding-read-model-type';
|
||||||
import { millisecondsToMinutes } from 'date-fns';
|
|
||||||
|
|
||||||
const calculateTimeDifferenceInMinutes = (date1?: Date, date2?: Date) => {
|
const instanceEventLookup = {
|
||||||
if (date1 && date2) {
|
'first-user-login': 'firstLogin',
|
||||||
const diffInMilliseconds = date2.getTime() - date1.getTime();
|
'second-user-login': 'secondLogin',
|
||||||
return millisecondsToMinutes(diffInMilliseconds);
|
'first-flag': 'firstFeatureFlag',
|
||||||
}
|
'first-pre-live': 'firstPreLive',
|
||||||
return null;
|
'first-live': 'firstLive',
|
||||||
|
};
|
||||||
|
|
||||||
|
const projectEventLookup = {
|
||||||
|
'first-flag': 'firstFeatureFlag',
|
||||||
|
'first-pre-live': 'firstPreLive',
|
||||||
|
'first-live': 'firstLive',
|
||||||
};
|
};
|
||||||
|
|
||||||
export class OnboardingReadModel implements IOnboardingReadModel {
|
export class OnboardingReadModel implements IOnboardingReadModel {
|
||||||
@ -22,109 +27,55 @@ export class OnboardingReadModel implements IOnboardingReadModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getInstanceOnboardingMetrics(): Promise<InstanceOnboarding> {
|
async getInstanceOnboardingMetrics(): Promise<InstanceOnboarding> {
|
||||||
const firstUserCreatedResult = await this.db('users')
|
const eventsResult = await this.db('onboarding_events_instance').select(
|
||||||
.select('created_at')
|
'event',
|
||||||
.orderBy('created_at')
|
'time_to_event',
|
||||||
.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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
const events: InstanceOnboarding = {
|
||||||
firstLogin: firstLoginDiff,
|
firstLogin: null,
|
||||||
secondLogin: secondLoginDiff,
|
secondLogin: null,
|
||||||
firstFeatureFlag: firstFlagDiff,
|
firstFeatureFlag: null,
|
||||||
firstPreLive: firstPreLiveDiff,
|
firstPreLive: null,
|
||||||
firstLive: firstLiveDiff,
|
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>> {
|
async getProjectsOnboardingMetrics(): Promise<Array<ProjectOnboarding>> {
|
||||||
const lifecycleResults = await this.db('projects')
|
const lifecycleResults = await this.db(
|
||||||
.join('features', 'projects.id', 'features.project')
|
'onboarding_events_project',
|
||||||
.join(
|
).select('project', 'event', 'time_to_event');
|
||||||
'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');
|
|
||||||
|
|
||||||
return lifecycleResults.map((result) => ({
|
const projects: Array<ProjectOnboarding> = [];
|
||||||
project: result.project_id,
|
|
||||||
firstFeatureFlag: calculateTimeDifferenceInMinutes(
|
lifecycleResults.forEach((result) => {
|
||||||
result.project_created_at,
|
let project = projects.find((p) => p.project === result.project);
|
||||||
result.first_initial,
|
|
||||||
),
|
if (!project) {
|
||||||
firstPreLive: calculateTimeDifferenceInMinutes(
|
project = {
|
||||||
result.project_created_at,
|
project: result.project,
|
||||||
result.first_pre_live,
|
firstFeatureFlag: null,
|
||||||
),
|
firstPreLive: null,
|
||||||
firstLive: calculateTimeDifferenceInMinutes(
|
firstLive: null,
|
||||||
result.project_created_at,
|
};
|
||||||
result.first_live,
|
projects.push(project);
|
||||||
),
|
}
|
||||||
}));
|
|
||||||
|
const eventType = projectEventLookup[result.event];
|
||||||
|
if (eventType) {
|
||||||
|
project[eventType] = result.time_to_event;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return projects;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user