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:
parent
6f09bcdee1
commit
0ce49c789e
@ -17,7 +17,10 @@ import type { Db } from '../../db/db';
|
|||||||
import type { Knex } from 'knex';
|
import type { Knex } from 'knex';
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import { ADMIN_TOKEN_USER, SYSTEM_USER, SYSTEM_USER_ID } from '../../types';
|
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 type { IQueryParam } from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
import { applyGenericQueryParams } from '../feature-search/search-utils';
|
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(
|
async deprecatedSearchEvents(
|
||||||
search: DeprecatedSearchEventsSchema = {},
|
search: DeprecatedSearchEventsSchema = {},
|
||||||
): Promise<IEvent[]> {
|
): Promise<IEvent[]> {
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
import type { Db, IUnleashConfig } from '../../server-impl';
|
import type { Db, IUnleashConfig } from '../../server-impl';
|
||||||
import { ProjectStatusService } from './project-status-service';
|
import { ProjectStatusService } from './project-status-service';
|
||||||
|
import EventStore from '../events/event-store';
|
||||||
|
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||||
|
|
||||||
export const createProjectStatusService = (
|
export const createProjectStatusService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
): ProjectStatusService => {
|
): ProjectStatusService => {
|
||||||
return new ProjectStatusService();
|
const eventStore = new EventStore(db, config.getLogger);
|
||||||
|
return new ProjectStatusService({ eventStore });
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createFakeProjectStatusService = () => {
|
export const createFakeProjectStatusService = () => {
|
||||||
const projectStatusService = new ProjectStatusService();
|
const eventStore = new FakeEventStore();
|
||||||
|
const projectStatusService = new ProjectStatusService({
|
||||||
|
eventStore,
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
projectStatusService,
|
projectStatusService,
|
||||||
|
@ -39,7 +39,7 @@ export default class ProjectStatusController extends Controller {
|
|||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.openApiService.validPath({
|
||||||
tags: ['Projects'],
|
tags: ['Unstable'],
|
||||||
operationId: 'getProjectStatus',
|
operationId: 'getProjectStatus',
|
||||||
summary: 'Get project status',
|
summary: 'Get project status',
|
||||||
description:
|
description:
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
import type { ProjectStatusSchema } from '../../openapi';
|
import type { ProjectStatusSchema } from '../../openapi';
|
||||||
|
import type { IEventStore, IUnleashStores } from '../../types';
|
||||||
|
|
||||||
export class ProjectStatusService {
|
export class ProjectStatusService {
|
||||||
constructor() {}
|
private eventStore: IEventStore;
|
||||||
|
constructor({ eventStore }: Pick<IUnleashStores, 'eventStore'>) {
|
||||||
|
this.eventStore = eventStore;
|
||||||
|
}
|
||||||
|
|
||||||
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
||||||
return { activityCountByDate: [{ date: '2024-09-11', count: 0 }] };
|
return {
|
||||||
|
activityCountByDate:
|
||||||
|
await this.eventStore.getProjectEventActivity(projectId),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,26 @@ import {
|
|||||||
setupAppWithCustomConfig,
|
setupAppWithCustomConfig,
|
||||||
} from '../../../test/e2e/helpers/test-helper';
|
} from '../../../test/e2e/helpers/test-helper';
|
||||||
import getLogger from '../../../test/fixtures/no-logger';
|
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 app: IUnleashTest;
|
||||||
let db: ITestDb;
|
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 () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('projects_status', getLogger);
|
db = await dbInit('projects_status', getLogger);
|
||||||
@ -21,6 +38,7 @@ beforeAll(async () => {
|
|||||||
},
|
},
|
||||||
db.rawDatabase,
|
db.rawDatabase,
|
||||||
);
|
);
|
||||||
|
eventService = createEventsService(db.rawDatabase, config);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
@ -28,13 +46,55 @@ afterAll(async () => {
|
|||||||
await db.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
|
const { body } = await app.request
|
||||||
.get('/api/admin/projects/default/status')
|
.get('/api/admin/projects/default/status')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
|
const { todayString, yesterdayString } = getCurrentDateStrings();
|
||||||
|
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
activityCountByDate: [{ date: '2024-09-11', count: 0 }],
|
activityCountByDate: [
|
||||||
|
{ date: yesterdayString, count: 1 },
|
||||||
|
{ date: todayString, count: 2 },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
import type { IBaseEvent, IEvent } from '../events';
|
import type { IBaseEvent, IEvent } from '../events';
|
||||||
import type { Store } from './store';
|
import type { Store } from './store';
|
||||||
import type { DeprecatedSearchEventsSchema } from '../../openapi';
|
import type {
|
||||||
|
DeprecatedSearchEventsSchema,
|
||||||
|
ProjectActivitySchema,
|
||||||
|
} from '../../openapi';
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import type { IQueryOperations } from '../../features/events/event-store';
|
import type { IQueryOperations } from '../../features/events/event-store';
|
||||||
import type { IQueryParam } from '../../features/feature-toggle/types/feature-toggle-strategies-store-type';
|
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>;
|
queryCount(operations: IQueryOperations[]): Promise<number>;
|
||||||
setCreatedByUserId(batchSize: number): Promise<number | undefined>;
|
setCreatedByUserId(batchSize: number): Promise<number | undefined>;
|
||||||
getEventCreators(): Promise<Array<{ id: number; name: string }>>;
|
getEventCreators(): Promise<Array<{ id: number; name: string }>>;
|
||||||
|
getProjectEventActivity(project: string): Promise<ProjectActivitySchema>;
|
||||||
}
|
}
|
||||||
|
9
src/test/fixtures/fake-event-store.ts
vendored
9
src/test/fixtures/fake-event-store.ts
vendored
@ -2,7 +2,10 @@ import type { IEventStore } from '../../lib/types/stores/event-store';
|
|||||||
import type { IBaseEvent, IEvent } from '../../lib/types/events';
|
import type { IBaseEvent, IEvent } from '../../lib/types/events';
|
||||||
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
|
import { sharedEventEmitter } from '../../lib/util/anyEventEmitter';
|
||||||
import type { IQueryOperations } from '../../lib/features/events/event-store';
|
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';
|
import type EventEmitter from 'events';
|
||||||
|
|
||||||
class FakeEventStore implements IEventStore {
|
class FakeEventStore implements IEventStore {
|
||||||
@ -15,6 +18,10 @@ class FakeEventStore implements IEventStore {
|
|||||||
this.events = [];
|
this.events = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProjectEventActivity(project: string): Promise<ProjectActivitySchema> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
getEventCreators(): Promise<{ id: number; name: string }[]> {
|
getEventCreators(): Promise<{ id: number; name: string }[]> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user