mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
chore: add connected environments to project status payload (#8645)
This PR adds connected environments to the project status payload. It's done by: - adding a new `getConnectedEnvironmentCountForProject` method to the project store (I opted for this approach instead of creating a new view model because it already has a `getEnvironmentsForProject` method) - adding the project store to the project status service - updating the schema For the schema, I opted for adding a `resources` property, under which I put `connectedEnvironments`. My thinking was that if we want to add the rest of the project resources (that go in the resources widget), it'd make sense to group those together inside an object. However, I'd also be happy to place the property on the top level. If you have opinions one way or the other, let me know. As for the count, we're currently only counting environments that have metrics and that are active for the current project.
This commit is contained in:
parent
6a8a75ce71
commit
1897f8a19d
@ -2,19 +2,29 @@ import type { Db, IUnleashConfig } from '../../server-impl';
|
||||
import { ProjectStatusService } from './project-status-service';
|
||||
import EventStore from '../events/event-store';
|
||||
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||
import ProjectStore from '../project/project-store';
|
||||
import FakeProjectStore from '../../../test/fixtures/fake-project-store';
|
||||
|
||||
export const createProjectStatusService = (
|
||||
db: Db,
|
||||
config: IUnleashConfig,
|
||||
): ProjectStatusService => {
|
||||
const eventStore = new EventStore(db, config.getLogger);
|
||||
return new ProjectStatusService({ eventStore });
|
||||
const projectStore = new ProjectStore(
|
||||
db,
|
||||
config.eventBus,
|
||||
config.getLogger,
|
||||
config.flagResolver,
|
||||
);
|
||||
return new ProjectStatusService({ eventStore, projectStore });
|
||||
};
|
||||
|
||||
export const createFakeProjectStatusService = () => {
|
||||
const eventStore = new FakeEventStore();
|
||||
const projectStore = new FakeProjectStore();
|
||||
const projectStatusService = new ProjectStatusService({
|
||||
eventStore,
|
||||
projectStore,
|
||||
});
|
||||
|
||||
return {
|
||||
|
@ -1,14 +1,26 @@
|
||||
import type { ProjectStatusSchema } from '../../openapi';
|
||||
import type { IEventStore, IUnleashStores } from '../../types';
|
||||
import type { IEventStore, IProjectStore, IUnleashStores } from '../../types';
|
||||
|
||||
export class ProjectStatusService {
|
||||
private eventStore: IEventStore;
|
||||
constructor({ eventStore }: Pick<IUnleashStores, 'eventStore'>) {
|
||||
private projectStore: IProjectStore;
|
||||
|
||||
constructor({
|
||||
eventStore,
|
||||
projectStore,
|
||||
}: Pick<IUnleashStores, 'eventStore' | 'projectStore'>) {
|
||||
this.eventStore = eventStore;
|
||||
this.projectStore = projectStore;
|
||||
}
|
||||
|
||||
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
||||
return {
|
||||
resources: {
|
||||
connectedEnvironments:
|
||||
await this.projectStore.getConnectedEnvironmentCountForProject(
|
||||
projectId,
|
||||
),
|
||||
},
|
||||
activityCountByDate:
|
||||
await this.eventStore.getProjectEventActivity(projectId),
|
||||
};
|
||||
|
@ -8,6 +8,7 @@ import { FEATURE_CREATED, type IUnleashConfig } from '../../types';
|
||||
import type { EventService } from '../../services';
|
||||
import { createEventsService } from '../events/createEventsService';
|
||||
import { createTestConfig } from '../../../test/config/test-config';
|
||||
import { randomId } from '../../util';
|
||||
|
||||
let app: IUnleashTest;
|
||||
let db: ITestDb;
|
||||
@ -99,3 +100,48 @@ test('project insights should return correct count for each day', async () => {
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
test('project status should return environments with connected SDKs', async () => {
|
||||
const flagName = randomId();
|
||||
await app.createFeature(flagName);
|
||||
|
||||
const envs =
|
||||
await app.services.environmentService.getProjectEnvironments('default');
|
||||
expect(envs.some((env) => env.name === 'default')).toBeTruthy();
|
||||
|
||||
const appName = 'blah';
|
||||
const environment = 'default';
|
||||
await db.stores.clientMetricsStoreV2.batchInsertMetrics([
|
||||
{
|
||||
featureName: `flag-doesnt-exist`,
|
||||
appName,
|
||||
environment,
|
||||
timestamp: new Date(),
|
||||
yes: 5,
|
||||
no: 2,
|
||||
},
|
||||
{
|
||||
featureName: flagName,
|
||||
appName: `web2`,
|
||||
environment,
|
||||
timestamp: new Date(),
|
||||
yes: 5,
|
||||
no: 2,
|
||||
},
|
||||
{
|
||||
featureName: flagName,
|
||||
appName,
|
||||
environment: 'not-a-real-env',
|
||||
timestamp: new Date(),
|
||||
yes: 2,
|
||||
no: 2,
|
||||
},
|
||||
]);
|
||||
|
||||
const { body } = await app.request
|
||||
.get('/api/admin/projects/default/status')
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(body.resources.connectedEnvironments).toBe(1);
|
||||
});
|
||||
|
@ -93,6 +93,8 @@ export interface IProjectStore extends Store<IProject, string> {
|
||||
|
||||
getEnvironmentsForProject(id: string): Promise<ProjectEnvironment[]>;
|
||||
|
||||
getConnectedEnvironmentCountForProject(id: string): Promise<number>;
|
||||
|
||||
getMembersCountByProject(projectId: string): Promise<number>;
|
||||
|
||||
getMembersCountByProjectAfterDate(
|
||||
|
@ -390,6 +390,22 @@ class ProjectStore implements IProjectStore {
|
||||
return rows.map(this.mapProjectEnvironmentRow);
|
||||
}
|
||||
|
||||
async getConnectedEnvironmentCountForProject(id: string): Promise<number> {
|
||||
const [{ count }] = (await this.db
|
||||
.countDistinct('cme.environment')
|
||||
.from('client_metrics_env as cme')
|
||||
.innerJoin('features', 'cme.feature_name', 'features.name')
|
||||
.innerJoin('projects', 'features.project', 'projects.id')
|
||||
.innerJoin(
|
||||
'project_environments',
|
||||
'cme.environment',
|
||||
'project_environments.environment_name',
|
||||
)
|
||||
.where('features.project', id)) as { count: string }[];
|
||||
|
||||
return Number(count);
|
||||
}
|
||||
|
||||
async getMembersCountByProject(projectId: string): Promise<number> {
|
||||
const members = await this.db
|
||||
.from((db) => {
|
||||
|
@ -7,6 +7,7 @@ test('projectStatusSchema', () => {
|
||||
{ date: '2022-12-14', count: 2 },
|
||||
{ date: '2022-12-15', count: 5 },
|
||||
],
|
||||
resources: { connectedEnvironments: 2 },
|
||||
};
|
||||
|
||||
expect(
|
||||
|
@ -5,7 +5,7 @@ export const projectStatusSchema = {
|
||||
$id: '#/components/schemas/projectStatusSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['activityCountByDate'],
|
||||
required: ['activityCountByDate', 'resources'],
|
||||
description:
|
||||
'Schema representing the overall status of a project, including an array of activity records. Each record in the activity array contains a date and a count, providing a snapshot of the project’s activity level over time.',
|
||||
properties: {
|
||||
@ -14,6 +14,19 @@ export const projectStatusSchema = {
|
||||
description:
|
||||
'Array of activity records with date and count, representing the project’s daily activity statistics.',
|
||||
},
|
||||
resources: {
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
required: ['connectedEnvironments'],
|
||||
description: 'Key resources within the project',
|
||||
properties: {
|
||||
connectedEnvironments: {
|
||||
type: 'number',
|
||||
description:
|
||||
'The number of environments that have received SDK traffic in this project.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
components: {
|
||||
schemas: {
|
||||
|
4
src/test/fixtures/fake-project-store.ts
vendored
4
src/test/fixtures/fake-project-store.ts
vendored
@ -214,4 +214,8 @@ export default class FakeProjectStore implements IProjectStore {
|
||||
project.id === id ? { ...project, archivedAt: null } : project,
|
||||
);
|
||||
}
|
||||
|
||||
async getConnectedEnvironmentCountForProject(): Promise<number> {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user