Skip to content

Commit

Permalink
feat: now backend returns event counts for activity chart (#8638)
Browse files Browse the repository at this point in the history
  • Loading branch information
sjaanus authored Nov 4, 2024
1 parent 6f09bcd commit 0ce49c7
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 10 deletions.
23 changes: 22 additions & 1 deletion src/lib/features/events/event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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[]> {
Expand Down
10 changes: 8 additions & 2 deletions src/lib/features/project-status/createProjectStatusService.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions src/lib/features/project-status/project-status-service.ts
Original file line number Diff line number Diff line change
@@ -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),
};
}
}
64 changes: 62 additions & 2 deletions src/lib/features/project-status/projects-status.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -21,20 +38,63 @@ beforeAll(async () => {
},
db.rawDatabase,
);
eventService = createEventsService(db.rawDatabase, config);
});

afterAll(async () => {
await app.destroy();
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 },
],
});
});
6 changes: 5 additions & 1 deletion src/lib/types/stores/event-store.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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>;
}
9 changes: 8 additions & 1 deletion src/test/fixtures/fake-event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.');
}
Expand Down

0 comments on commit 0ce49c7

Please sign in to comment.