From 7e85de8f6517862cdda96ec20978fdce66a7418d Mon Sep 17 00:00:00 2001 From: Jaanus Sellin Date: Wed, 2 Jul 2025 10:16:13 +0300 Subject: [PATCH] feat: now it is possible to search events by group id (#10275) Now when you put group ID as query param, it will filter based on transaction id. I am not sure if its best naming, whether it should be groupId or transactionId, I will leave as group for now, but its simple change later. ![image](https://github.com/user-attachments/assets/e0caaf57-f93f-40ee-a332-d3aed249c4ca) --- .../events/EventLog/useEventLogSearch.ts | 1 + src/lib/features/events/event-service.ts | 5 + .../spec/event-search-query-parameters.ts | 11 ++ src/lib/types/stores/event-store.ts | 1 + .../e2e/api/admin/event-search.e2e.test.ts | 139 ++++++++++++++++++ 5 files changed, 157 insertions(+) diff --git a/frontend/src/component/events/EventLog/useEventLogSearch.ts b/frontend/src/component/events/EventLog/useEventLogSearch.ts index 86483c87b8..cc98ffc7f6 100644 --- a/frontend/src/component/events/EventLog/useEventLogSearch.ts +++ b/frontend/src/component/events/EventLog/useEventLogSearch.ts @@ -73,6 +73,7 @@ export const useEventLogSearch = ( type: FilterItemParam, environment: FilterItemParam, id: StringParam, + groupId: StringParam, ...extraParameters(logType), }; diff --git a/src/lib/features/events/event-service.ts b/src/lib/features/events/event-service.ts index b55ddf0bd5..b6acb84642 100644 --- a/src/lib/features/events/event-service.ts +++ b/src/lib/features/events/event-service.ts @@ -222,6 +222,11 @@ export default class EventService { if (parsed) queryParams.push(parsed); } + if (params.groupId) { + const parsed = parseSearchOperatorValue('group_id', params.groupId); + if (parsed) queryParams.push(parsed); + } + ['project', 'type', 'environment', 'id'].forEach((field) => { if (params[field]) { const parsed = parseSearchOperatorValue(field, params[field]); diff --git a/src/lib/openapi/spec/event-search-query-parameters.ts b/src/lib/openapi/spec/event-search-query-parameters.ts index a36fc971c8..2c2ea1f891 100644 --- a/src/lib/openapi/spec/event-search-query-parameters.ts +++ b/src/lib/openapi/spec/event-search-query-parameters.ts @@ -22,6 +22,17 @@ export const eventSearchQueryParameters = [ 'Filter by event ID using supported operators: IS, IS_ANY_OF.', in: 'query', }, + { + name: 'groupId', + schema: { + type: 'string', + example: 'IS:123', + pattern: '^(IS|IS_ANY_OF):(.*?)(,([0-9]+))*$', + }, + description: + 'Filter by group ID using supported operators: IS, IS_ANY_OF.', + in: 'query', + }, { name: 'feature', schema: { diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts index 41331db1c1..d35f30809e 100644 --- a/src/lib/types/stores/event-store.ts +++ b/src/lib/types/stores/event-store.ts @@ -7,6 +7,7 @@ import type { IQueryParam } from '../../features/feature-toggle/types/feature-to export interface IEventSearchParams { id?: string; + groupId?: string; project?: string; query?: string; feature?: string; diff --git a/src/test/e2e/api/admin/event-search.e2e.test.ts b/src/test/e2e/api/admin/event-search.e2e.test.ts index 778ab0c9c1..80ec83f772 100644 --- a/src/test/e2e/api/admin/event-search.e2e.test.ts +++ b/src/test/e2e/api/admin/event-search.e2e.test.ts @@ -16,6 +16,8 @@ import { import { createEventsService } from '../../../../lib/features/index.js'; import { createTestConfig } from '../../../config/test-config.js'; import { FEATURE_CREATED, USER_CREATED } from '../../../../lib/events/index.js'; +import { withTransactional } from '../../../../lib/db/transaction.js'; +import { EventStore } from '../../../../lib/features/events/event-store.js'; let app: IUnleashTest; let db: ITestDb; @@ -718,3 +720,140 @@ test('should filter events by multiple IDs using IS_ANY_OF', async () => { ); expect(returnedIds).not.toContain(feature2Event.id); }); + +test('should filter events by group ID', async () => { + const eventStoreService = withTransactional( + (db) => new EventStore(db, getLogger), + db.rawDatabase, + ); + + const events = [ + { + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature1' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }, + { + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature2' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }, + ]; + + await eventStoreService.transactional( + async (transactionalEventStore) => { + await transactionalEventStore.batchStore(events); + }, + { type: 'transaction', id: '1' }, + ); + + await eventService.storeEvent({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature3' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + + const { body } = await searchEvents({ groupId: 'IS:1' }); + + expect(body.total).toBe(2); + expect(body.events).toHaveLength(2); + expect(body.events.every((e: any) => e.groupId === '1')).toBe(true); +}); + +test('should filter events by multiple group IDs using IS_ANY_OF', async () => { + const eventStoreService = withTransactional( + (db) => new EventStore(db, getLogger), + db.rawDatabase, + ); + + const event1 = { + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature1' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }; + const event2 = { + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature2' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }; + const event3 = { + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature3' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }; + + await eventStoreService.transactional( + async (transactionalEventStore) => { + await transactionalEventStore.store(event1); + }, + { type: 'transaction', id: '10' }, + ); + + await eventStoreService.transactional( + async (transactionalEventStore) => { + await transactionalEventStore.store(event2); + }, + { type: 'transaction', id: '20' }, + ); + + await eventStoreService.transactional( + async (transactionalEventStore) => { + await transactionalEventStore.store(event3); + }, + { type: 'transaction', id: '30' }, + ); + + const { body } = await searchEvents({ groupId: 'IS_ANY_OF:10,30' }); + + expect(body.total).toBe(2); + expect(body.events).toHaveLength(2); + + const returnedGroupIds = body.events.map((e: any) => e.groupId); + expect(returnedGroupIds).toContain('10'); + expect(returnedGroupIds).toContain('30'); + expect(returnedGroupIds).not.toContain('20'); +}); + +test('Should return empty result when filtering by non-existent group ID', async () => { + const eventStoreService = withTransactional( + (db) => new EventStore(db, getLogger), + db.rawDatabase, + ); + + await eventStoreService.transactional( + async (transactionalEventStore) => { + await transactionalEventStore.store({ + type: FEATURE_CREATED, + project: 'default', + data: { name: 'feature1' }, + createdBy: 'test-user', + createdByUserId: TEST_USER_ID, + ip: '127.0.0.1', + }); + }, + { type: 'transaction', id: '1' }, + ); + + const { body } = await searchEvents({ groupId: 'IS:999' }); + + expect(body.total).toBe(0); + expect(body.events).toHaveLength(0); +});