1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-05-17 01:17:29 +02:00

feat: now backend returns event counts for activity chart (#8638)

This commit is contained in:
Jaanus Sellin 2024-11-04 14:29:10 +02:00 committed by GitHub
parent 6f09bcdee1
commit 0ce49c789e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 115 additions and 10 deletions

View File

@ -17,7 +17,10 @@ import type { Db } from '../../db/db';
import type { Knex } from 'knex';
import type EventEmitter from 'events';
import { ADMIN_TOKEN_USER, SYSTEM_USER, SYSTEM_USER_ID } from '../../types';
import type { DeprecatedSearchEventsSchema } from '../../openapi';
import type {
DeprecatedSearchEventsSchema,
ProjectActivitySchema,
} from '../../openapi';
import type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
import { applyGenericQueryParams } from '../feature-search/search-utils';
@ -406,6 +409,24 @@ class EventStore implements IEventStore {
}));
}
async getProjectEventActivity(
project: string,
): Promise<ProjectActivitySchema> {
const result = await this.db('events')
.select(
this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD') AS date"),
)
.count('* AS count')
.where('project', project)
.groupBy(this.db.raw("TO_CHAR(created_at::date, 'YYYY-MM-DD')"))
.orderBy('date', 'asc');
return result.map((row) => ({
date: row.date,
count: Number(row.count),
}));
}
async deprecatedSearchEvents(
search: DeprecatedSearchEventsSchema = {},
): Promise<IEvent[]> {

View File

@ -1,15 +1,21 @@
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';
export const createProjectStatusService = (
db: Db,
config: IUnleashConfig,
): ProjectStatusService => {
return new ProjectStatusService();
const eventStore = new EventStore(db, config.getLogger);
return new ProjectStatusService({ eventStore });
};
export const createFakeProjectStatusService = () => {
const projectStatusService = new ProjectStatusService();
const eventStore = new FakeEventStore();
const projectStatusService = new ProjectStatusService({
eventStore,
});
return {
projectStatusService,

View File

@ -39,7 +39,7 @@ export default class ProjectStatusController extends Controller {
permission: NONE,
middleware: [
this.openApiService.validPath({
tags: ['Projects'],
tags: ['Unstable'],
operationId: 'getProjectStatus',
summary: 'Get project status',
description:

View File

@ -1,9 +1,16 @@
import type { ProjectStatusSchema } from '../../openapi';
import type { IEventStore, IUnleashStores } from '../../types';
export class ProjectStatusService {
constructor() {}
private eventStore: IEventStore;
constructor({ eventStore }: Pick<IUnleashStores, 'eventStore'>) {
this.eventStore = eventStore;
}
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
return { activityCountByDate: [{ date: '2024-09-11', count: 0 }] };
return {
activityCountByDate:
await this.eventStore.getProjectEventActivity(projectId),
};
}
}

View File

@ -4,9 +4,26 @@ import {
setupAppWithCustomConfig,
} from '../../../test/e2e/helpers/test-helper';
import getLogger from '../../../test/fixtures/no-logger';
import { FEATURE_CREATED, type IUnleashConfig } from '../../types';
import type { EventService } from '../../services';
import { createEventsService } from '../events/createEventsService';
import { createTestConfig } from '../../../test/config/test-config';
let app: IUnleashTest;
let db: ITestDb;
let eventService: EventService;
const TEST_USER_ID = -9999;
const config: IUnleashConfig = createTestConfig();
const getCurrentDateStrings = () => {
const today = new Date();
const todayString = today.toISOString().split('T')[0];
const yesterday = new Date(today);
yesterday.setDate(today.getDate() - 1);
const yesterdayString = yesterday.toISOString().split('T')[0];
return { todayString, yesterdayString };
};
beforeAll(async () => {
db = await dbInit('projects_status', getLogger);
@ -21,6 +38,7 @@ beforeAll(async () => {
},
db.rawDatabase,
);
eventService = createEventsService(db.rawDatabase, config);
});
afterAll(async () => {
@ -28,13 +46,55 @@ afterAll(async () => {
await db.destroy();
});
test('project insights happy path', async () => {
test('project insights should return correct count for each day', async () => {
await eventService.storeEvent({
type: FEATURE_CREATED,
project: 'default',
data: { featureName: 'today-event' },
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
});
await eventService.storeEvent({
type: FEATURE_CREATED,
project: 'default',
data: { featureName: 'today-event-two' },
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
});
await eventService.storeEvent({
type: FEATURE_CREATED,
project: 'default',
data: { featureName: 'yesterday-event' },
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
ip: '127.0.0.1',
});
const { events } = await eventService.getEvents();
const yesterdayEvent = events.find(
(e) => e.data.featureName === 'yesterday-event',
);
await db.rawDatabase.raw(
`UPDATE events SET created_at = '2024-11-03' where id = ?`,
[yesterdayEvent?.id],
);
const { body } = await app.request
.get('/api/admin/projects/default/status')
.expect('Content-Type', /json/)
.expect(200);
const { todayString, yesterdayString } = getCurrentDateStrings();
expect(body).toMatchObject({
activityCountByDate: [{ date: '2024-09-11', count: 0 }],
activityCountByDate: [
{ date: yesterdayString, count: 1 },
{ date: todayString, count: 2 },
],
});
});

View File

@ -1,6 +1,9 @@
import type { IBaseEvent, IEvent } from '../events';
import type { Store } from './store';
import type { DeprecatedSearchEventsSchema } from '../../openapi';
import type {
DeprecatedSearchEventsSchema,
ProjectActivitySchema,
} from '../../openapi';
import type EventEmitter from 'events';
import type { IQueryOperations } from '../../features/events/event-store';
import type { IQueryParam } from '../../features/feature-toggle/types/feature-toggle-strategies-store-type';
@ -44,4 +47,5 @@ export interface IEventStore
queryCount(operations: IQueryOperations[]): Promise<number>;
setCreatedByUserId(batchSize: number): Promise<number | undefined>;
getEventCreators(): Promise<Array<{ id: number; name: string }>>;
getProjectEventActivity(project: string): Promise<ProjectActivitySchema>;
}

View File

@ -2,7 +2,10 @@ import type { IEventStore } from '../../lib/types/stores/event-store';
import type { IBaseEvent, IEvent } from '../../lib/types/events';
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
import type { IQueryOperations } from '../../lib/features/events/event-store';
import type { DeprecatedSearchEventsSchema } from '../../lib/openapi';
import type {
DeprecatedSearchEventsSchema,
ProjectActivitySchema,
} from '../../lib/openapi';
import type EventEmitter from 'events';
class FakeEventStore implements IEventStore {
@ -15,6 +18,10 @@ class FakeEventStore implements IEventStore {
this.events = [];
}
getProjectEventActivity(project: string): Promise<ProjectActivitySchema> {
throw new Error('Method not implemented.');
}
getEventCreators(): Promise<{ id: number; name: string }[]> {
throw new Error('Method not implemented.');
}