diff --git a/src/lib/features/events/event-store.ts b/src/lib/features/events/event-store.ts index bf8efaeed7da..8b522f30bf8b 100644 --- a/src/lib/features/events/event-store.ts +++ b/src/lib/features/events/event-store.ts @@ -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 { + 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 { diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index 00a30582ff94..3448baa31693 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -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, diff --git a/src/lib/features/project-status/project-status-controller.ts b/src/lib/features/project-status/project-status-controller.ts index 3abd8aa0228c..ba919a0e68ff 100644 --- a/src/lib/features/project-status/project-status-controller.ts +++ b/src/lib/features/project-status/project-status-controller.ts @@ -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: diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts index 26a2c444e2d2..b6d905b36c63 100644 --- a/src/lib/features/project-status/project-status-service.ts +++ b/src/lib/features/project-status/project-status-service.ts @@ -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) { + this.eventStore = eventStore; + } async getProjectStatus(projectId: string): Promise { - return { activityCountByDate: [{ date: '2024-09-11', count: 0 }] }; + return { + activityCountByDate: + await this.eventStore.getProjectEventActivity(projectId), + }; } } diff --git a/src/lib/features/project-status/projects-status.e2e.test.ts b/src/lib/features/project-status/projects-status.e2e.test.ts index 79e4992cfe36..cf757e20b5f3 100644 --- a/src/lib/features/project-status/projects-status.e2e.test.ts +++ b/src/lib/features/project-status/projects-status.e2e.test.ts @@ -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 }, + ], }); }); diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts index 57c36fcf0f3c..aada71d1dea3 100644 --- a/src/lib/types/stores/event-store.ts +++ b/src/lib/types/stores/event-store.ts @@ -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; setCreatedByUserId(batchSize: number): Promise; getEventCreators(): Promise>; + getProjectEventActivity(project: string): Promise; } diff --git a/src/test/fixtures/fake-event-store.ts b/src/test/fixtures/fake-event-store.ts index ff53577f0f14..ea4b63438bf4 100644 --- a/src/test/fixtures/fake-event-store.ts +++ b/src/test/fixtures/fake-event-store.ts @@ -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 { + throw new Error('Method not implemented.'); + } + getEventCreators(): Promise<{ id: number; name: string }[]> { throw new Error('Method not implemented.'); }