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> {
|
async insert(user: ICreateUser): Promise<User> {
|
||||||
const rows = await this.db(TABLE)
|
const rows = await this.db(TABLE)
|
||||||
.insert(mapUserToColumns(user))
|
.insert({ ...mapUserToColumns(user), created_at: new Date() })
|
||||||
.returning(USER_COLUMNS);
|
.returning(USER_COLUMNS);
|
||||||
return rowToUser(rows[0]);
|
return rowToUser(rows[0]);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
import type { IOnboardingReadModel } from '../../types';
|
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 {
|
export class FakeOnboardingReadModel implements IOnboardingReadModel {
|
||||||
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding> {
|
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding> {
|
||||||
@ -11,4 +14,7 @@ export class FakeOnboardingReadModel implements IOnboardingReadModel {
|
|||||||
firstLive: null,
|
firstLive: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
getProjectsOnboardingMetrics(): Promise<ProjectOnboarding[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,17 @@ export type InstanceOnboarding = {
|
|||||||
firstLive: number | null;
|
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 {
|
export interface IOnboardingReadModel {
|
||||||
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding>;
|
getInstanceOnboardingMetrics(): Promise<InstanceOnboarding>;
|
||||||
|
getProjectsOnboardingMetrics(): Promise<Array<ProjectOnboarding>>;
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,7 @@ beforeEach(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('can get onboarding durations', async () => {
|
test('can get onboarding durations', async () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
const initialResult =
|
const initialResult =
|
||||||
await onboardingReadModel.getInstanceOnboardingMetrics();
|
await onboardingReadModel.getInstanceOnboardingMetrics();
|
||||||
expect(initialResult).toMatchObject({
|
expect(initialResult).toMatchObject({
|
||||||
@ -56,7 +57,7 @@ test('can get onboarding durations', async () => {
|
|||||||
firstLogin: 0,
|
firstLogin: 0,
|
||||||
secondLogin: null,
|
secondLogin: null,
|
||||||
});
|
});
|
||||||
jest.useFakeTimers();
|
|
||||||
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
jest.advanceTimersByTime(minutesToMilliseconds(10));
|
||||||
|
|
||||||
const secondUser = await userStore.insert({});
|
const secondUser = await userStore.insert({});
|
||||||
@ -103,4 +104,16 @@ test('can get onboarding durations', async () => {
|
|||||||
firstPreLive: 30,
|
firstPreLive: 30,
|
||||||
firstLive: 40,
|
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 {
|
import type {
|
||||||
IOnboardingReadModel,
|
IOnboardingReadModel,
|
||||||
InstanceOnboarding,
|
InstanceOnboarding,
|
||||||
|
ProjectOnboarding,
|
||||||
} from './onboarding-read-model-type';
|
} from './onboarding-read-model-type';
|
||||||
import { millisecondsToMinutes } from 'date-fns';
|
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) => {
|
const calculateTimeDifferenceInMinutes = (date1?: Date, date2?: Date) => {
|
||||||
if (date1 && date2) {
|
if (date1 && date2) {
|
||||||
const diffInMilliseconds = date2.getTime() - date1.getTime();
|
const diffInMilliseconds = date2.getTime() - date1.getTime();
|
||||||
@ -89,4 +83,48 @@ export class OnboardingReadModel implements IOnboardingReadModel {
|
|||||||
firstLive: firstLiveDiff,
|
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'],
|
labelNames: ['event'],
|
||||||
help: 'firstLogin, secondLogin, firstFeatureFlag, firstPreLive, firstLive from first user creation',
|
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({
|
const featureLifecycleStageCountByProject = createGauge({
|
||||||
name: 'feature_lifecycle_stage_count_by_project',
|
name: 'feature_lifecycle_stage_count_by_project',
|
||||||
@ -394,7 +399,8 @@ export default class MetricsMonitor {
|
|||||||
largestProjectEnvironments,
|
largestProjectEnvironments,
|
||||||
largestFeatureEnvironments,
|
largestFeatureEnvironments,
|
||||||
deprecatedTokens,
|
deprecatedTokens,
|
||||||
onboardingMetrics,
|
instanceOnboardingMetrics,
|
||||||
|
projectsOnboardingMetrics,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
|
||||||
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
|
||||||
@ -412,6 +418,9 @@ export default class MetricsMonitor {
|
|||||||
flagResolver.isEnabled('onboardingMetrics')
|
flagResolver.isEnabled('onboardingMetrics')
|
||||||
? stores.onboardingReadModel.getInstanceOnboardingMetrics()
|
? stores.onboardingReadModel.getInstanceOnboardingMetrics()
|
||||||
: Promise.resolve({}),
|
: Promise.resolve({}),
|
||||||
|
flagResolver.isEnabled('onboardingMetrics')
|
||||||
|
? stores.onboardingReadModel.getProjectsOnboardingMetrics()
|
||||||
|
: Promise.resolve([]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
featureFlagsTotal.reset();
|
featureFlagsTotal.reset();
|
||||||
@ -539,15 +548,26 @@ export default class MetricsMonitor {
|
|||||||
.set(featureEnvironment.size);
|
.set(featureEnvironment.size);
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.keys(onboardingMetrics).forEach((key) => {
|
Object.keys(instanceOnboardingMetrics).forEach((key) => {
|
||||||
if (Number.isInteger(onboardingMetrics[key])) {
|
if (Number.isInteger(instanceOnboardingMetrics[key])) {
|
||||||
onboardingDuration
|
onboardingDuration
|
||||||
.labels({
|
.labels({
|
||||||
event: key,
|
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(
|
for (const [resource, limit] of Object.entries(
|
||||||
config.resourceLimits,
|
config.resourceLimits,
|
||||||
|
Loading…
Reference in New Issue
Block a user