Skip to content

Commit

Permalink
feat: projects onboarding metrics (#8014)
Browse files Browse the repository at this point in the history
  • Loading branch information
kwasniew authored Aug 29, 2024
1 parent d3b65a3 commit c5d6bde
Show file tree
Hide file tree
Showing 6 changed files with 102 additions and 14 deletions.
2 changes: 1 addition & 1 deletion src/lib/db/user-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
}
Expand Down
8 changes: 7 additions & 1 deletion src/lib/features/onboarding/fake-onboarding-read-model.ts
Original file line number Diff line number Diff line change
@@ -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> {
Expand All @@ -11,4 +14,7 @@ export class FakeOnboardingReadModel implements IOnboardingReadModel {
firstLive: null,
});
}
getProjectsOnboardingMetrics(): Promise<ProjectOnboarding[]> {
return Promise.resolve([]);
}
}
11 changes: 11 additions & 0 deletions src/lib/features/onboarding/onboarding-read-model-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>>;
}
15 changes: 14 additions & 1 deletion src/lib/features/onboarding/onboarding-read-model.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ beforeEach(async () => {
});

test('can get onboarding durations', async () => {
jest.useFakeTimers();
const initialResult =
await onboardingReadModel.getInstanceOnboardingMetrics();
expect(initialResult).toMatchObject({
Expand All @@ -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({});
Expand Down Expand Up @@ -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,
},
]);
});
52 changes: 45 additions & 7 deletions src/lib/features/onboarding/onboarding-read-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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,
),
}));
}
}
28 changes: 24 additions & 4 deletions src/lib/metrics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -394,7 +399,8 @@ export default class MetricsMonitor {
largestProjectEnvironments,
largestFeatureEnvironments,
deprecatedTokens,
onboardingMetrics,
instanceOnboardingMetrics,
projectsOnboardingMetrics,
] = await Promise.all([
stores.featureStrategiesReadModel.getMaxFeatureStrategies(),
stores.featureStrategiesReadModel.getMaxFeatureEnvironmentStrategies(),
Expand All @@ -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();
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit c5d6bde

Please sign in to comment.