mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
feat: projects onboarding metrics (#8014)
This commit is contained in:
parent
d3b65a3105
commit
c5d6bdecac
@ -109,7 +109,7 @@ class UserStore implements IUserStore {
|
||||
|
||||
async insert(user: ICreateUser): Promise<User> {
|
||||
const rows = await this.db(TABLE)
|
||||
.insert(mapUserToColumns(user))
|
||||
.insert({ ...mapUserToColumns(user), created_at: new Date() })
|
||||
.returning(USER_COLUMNS);
|
||||
return rowToUser(rows[0]);
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
import type { IOnboardingReadModel } from '../../types';
|
||||
import type { InstanceOnboarding } from './onboarding-read-model-type';
|
||||
import type {
|
||||
InstanceOnboarding,
|
||||
ProjectOnboarding,
|
||||
} from './onboarding-read-model-type';
|
||||
|
||||
export class FakeOnboardingReadModel implements IOnboardingReadModel {
|
||||
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding> {
|
||||
@ -11,4 +14,7 @@ export class FakeOnboardingReadModel implements IOnboardingReadModel {
|
||||
firstLive: null,
|
||||
});
|
||||
}
|
||||
getProjectsOnboardingMetrics(): Promise<ProjectOnboarding[]> {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,17 @@ export type InstanceOnboarding = {
|
||||
firstLive: number | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* All the values are in minutes
|
||||
*/
|
||||
export type ProjectOnboarding = {
|
||||
project: string;
|
||||
firstFeatureFlag: number | null;
|
||||
firstPreLive: number | null;
|
||||
firstLive: number | null;
|
||||
};
|
||||
|
||||
export interface IOnboardingReadModel {
|
||||
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding>;
|
||||
getProjectsOnboardingMetrics(): Promise<Array<ProjectOnboarding>>;
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ beforeEach(async () => {
|
||||
});
|
||||
|
||||
test('can get onboarding durations', async () => {
|
||||
jest.useFakeTimers();
|
||||
const initialResult =
|
||||
await onboardingReadModel.getInstanceOnboardingMetrics();
|
||||
expect(initialResult).toMatchObject({
|
||||
@ -56,7 +57,7 @@ test('can get onboarding durations', async () => {
|
||||
firstLogin: 0,
|
||||
secondLogin: null,
|
||||
});
|
||||
jest.useFakeTimers();
|
||||
|
||||
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
||||
|
||||
const secondUser = await userStore.insert({});
|
||||
@ -103,4 +104,16 @@ test('can get onboarding durations', async () => {
|
||||
firstPreLive: 30,
|
||||
firstLive: 40,
|
||||
});
|
||||
|
||||
const projectOnboardingResult =
|
||||
await onboardingReadModel.getProjectsOnboardingMetrics();
|
||||
|
||||
expect(projectOnboardingResult).toMatchObject([
|
||||
{
|
||||
project: 'default',
|
||||
firstFeatureFlag: 20,
|
||||
firstPreLive: 30,
|
||||
firstLive: 40,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -2,16 +2,10 @@ import type { Db } from '../../db/db';
|
||||
import type {
|
||||
IOnboardingReadModel,
|
||||
InstanceOnboarding,
|
||||
ProjectOnboarding,
|
||||
} from './onboarding-read-model-type';
|
||||
import { millisecondsToMinutes } from 'date-fns';
|
||||
|
||||
interface IOnboardingUser {
|
||||
first_login: string;
|
||||
}
|
||||
const parseStringToNumber = (value: string): number | null => {
|
||||
return Number.isNaN(Number(value)) ? null : Number(value);
|
||||
};
|
||||
|
||||
const calculateTimeDifferenceInMinutes = (date1?: Date, date2?: Date) => {
|
||||
if (date1 && date2) {
|
||||
const diffInMilliseconds = date2.getTime() - date1.getTime();
|
||||
@ -89,4 +83,48 @@ export class OnboardingReadModel implements IOnboardingReadModel {
|
||||
firstLive: firstLiveDiff,
|
||||
};
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
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,
|
||||
),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -312,6 +312,11 @@ export default class MetricsMonitor {
|
||||
labelNames: ['event'],
|
||||
help: 'firstLogin, secondLogin, firstFeatureFlag, firstPreLive, firstLive from first user creation',
|
||||
});
|
||||
const projectOnboardingDuration = createGauge({
|
||||
name: 'project_onboarding_duration',
|
||||
labelNames: ['event', 'project'],
|
||||
help: 'firstFeatureFlag, firstPreLive, firstLive from project creation',
|
||||
});
|
||||
|
||||
const featureLifecycleStageCountByProject = createGauge({
|
||||
name: 'feature_lifecycle_stage_count_by_project',
|
||||
@ -394,7 +399,8 @@ export default class MetricsMonitor {
|
||||
largestProjectEnvironments,
|
||||
largestFeatureEnvironments,
|
||||
deprecatedTokens,
|
||||
onboardingMetrics,
|
||||
instanceOnboardingMetrics,
|
||||
projectsOnboardingMetrics,
|
||||
] = await Promise.all([
|
||||
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
||||
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
||||
@ -412,6 +418,9 @@ export default class MetricsMonitor {
|
||||
flagResolver.isEnabled('onboardingMetrics')
|
||||
? stores.onboardingReadModel.getInstanceOnboardingMetrics()
|
||||
: Promise.resolve({}),
|
||||
flagResolver.isEnabled('onboardingMetrics')
|
||||
? stores.onboardingReadModel.getProjectsOnboardingMetrics()
|
||||
: Promise.resolve([]),
|
||||
]);
|
||||
|
||||
featureFlagsTotal.reset();
|
||||
@ -539,15 +548,26 @@ export default class MetricsMonitor {
|
||||
.set(featureEnvironment.size);
|
||||
}
|
||||
|
||||
Object.keys(onboardingMetrics).forEach((key) => {
|
||||
if (Number.isInteger(onboardingMetrics[key])) {
|
||||
Object.keys(instanceOnboardingMetrics).forEach((key) => {
|
||||
if (Number.isInteger(instanceOnboardingMetrics[key])) {
|
||||
onboardingDuration
|
||||
.labels({
|
||||
event: key,
|
||||
})
|
||||
.set(onboardingMetrics[key]);
|
||||
.set(instanceOnboardingMetrics[key]);
|
||||
}
|
||||
});
|
||||
projectsOnboardingMetrics.forEach(
|
||||
({ project, ...projectMetrics }) => {
|
||||
Object.keys(projectMetrics).forEach((key) => {
|
||||
if (Number.isInteger(projectMetrics[key])) {
|
||||
projectOnboardingDuration
|
||||
.labels({ event: key, project })
|
||||
.set(projectMetrics[key]);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
for (const [resource, limit] of Object.entries(
|
||||
config.resourceLimits,
|
||||
|
Loading…
Reference in New Issue
Block a user