1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-31 13:47:02 +02:00

feat: start storing every transaction id in events table (#10236)

Every time an event gets inserted, we check if there is transaction
data, that we can include.
This commit is contained in:
Jaanus Sellin 2025-06-30 16:00:57 +03:00 committed by GitHub
parent f6ab7460c6
commit 681ce3bfd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 184 additions and 1 deletions

View File

@ -397,6 +397,8 @@ export interface IEvent extends Omit<IBaseEvent, 'ip'> {
id: number;
createdAt: Date;
ip?: string;
groupType?: string;
groupId?: string;
}
export interface IEnrichedEvent extends IEvent {

View File

@ -41,6 +41,8 @@ const EVENT_COLUMNS = [
'feature_name',
'project',
'environment',
'group_type',
'group_id',
] as const;
export type IQueryOperations =
@ -97,6 +99,8 @@ export interface IEventTable {
environment?: string;
tags: ITag[];
ip?: string;
group_type: string | null;
group_id: string | null;
}
const TABLE = 'events';
@ -157,7 +161,9 @@ export class EventStore implements IEventStore {
async batchStore(events: IBaseEvent[]): Promise<void> {
try {
await this.db(TABLE).insert(events.map(this.eventToDbRow));
await this.db(TABLE).insert(
events.map((event) => this.eventToDbRow(event)),
);
} catch (error: unknown) {
this.logger.warn(
`Failed to store events: ${JSON.stringify(events)}`,
@ -472,10 +478,14 @@ export class EventStore implements IEventStore {
featureName: row.feature_name,
project: row.project,
environment: row.environment,
groupType: row.group_type || undefined,
groupId: row.group_id || undefined,
};
}
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
const transactionContext = this.db.userParams;
return {
type: e.type,
created_by: e.createdBy ?? 'admin',
@ -490,6 +500,8 @@ export class EventStore implements IEventStore {
project: e.project,
environment: e.environment,
ip: e.ip,
group_type: transactionContext?.type || null,
group_id: transactionContext?.id || null,
};
}

View File

@ -109,6 +109,18 @@ export const eventSchema = {
'The IP address of the user that created the event. Only available in Enterprise.',
example: '192.168.1.1',
},
groupType: {
type: 'string',
description:
'The type of transaction group this event belongs to, if applicable.',
example: 'change-request',
},
groupId: {
type: 'string',
description:
'The unique identifier for the transaction group this event belongs to, if applicable.',
example: '01HQVX5K8P9EXAMPLE123456',
},
},
components: {
schemas: {

View File

@ -18,6 +18,11 @@ import dbInit, { type ITestDb } from '../helpers/database-init.js';
import getLogger from '../../fixtures/no-logger.js';
import type { IEventStore } from '../../../lib/types/stores/event-store.js';
import type { IAuditUser, IUnleashStores } from '../../../lib/types/index.js';
import {
withTransactional,
type TransactionUserParams,
} from '../../../lib/db/transaction.js';
import { EventStore } from '../../../lib/features/events/event-store.js';
import { vi } from 'vitest';
@ -472,3 +477,155 @@ test('Should return empty result when filtering by non-existent ID', async () =>
expect(filteredEvents).toHaveLength(0);
});
test('Should store and retrieve transaction context fields', async () => {
const mockTransactionContext: TransactionUserParams = {
type: 'change-request',
id: '01HQVX5K8P9EXAMPLE123456',
};
const eventStoreService = withTransactional(
(db) => new EventStore(db, getLogger),
db.rawDatabase,
);
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'test-feature-with-context',
project: 'test-project',
ip: '127.0.0.1',
data: {
name: 'test-feature-with-context',
enabled: true,
strategies: [{ name: 'default' }],
},
};
await eventStoreService.transactional(async (transactionalEventStore) => {
await transactionalEventStore.store(event);
}, mockTransactionContext);
const events = await eventStore.getAll();
const storedEvent = events.find(
(e) => e.featureName === 'test-feature-with-context',
);
expect(storedEvent).toBeTruthy();
expect(storedEvent!.groupType).toBe('change-request');
expect(storedEvent!.groupId).toBe('01HQVX5K8P9EXAMPLE123456');
});
test('Should handle missing transaction context gracefully', async () => {
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'test-feature-no-context',
project: 'test-project',
ip: '127.0.0.1',
data: {
name: 'test-feature-no-context',
enabled: true,
strategies: [{ name: 'default' }],
},
};
await eventStore.store(event);
const events = await eventStore.getAll();
const storedEvent = events.find(
(e) => e.featureName === 'test-feature-no-context',
);
expect(storedEvent).toBeTruthy();
expect(storedEvent!.groupType).toBeUndefined();
expect(storedEvent!.groupId).toBeUndefined();
});
test('Should store transaction context in batch operations', async () => {
const mockTransactionContext: TransactionUserParams = {
type: 'transaction',
id: '01HQVX5K8P9BATCH123456',
};
const eventStoreService = withTransactional(
(db) => new EventStore(db, getLogger),
db.rawDatabase,
);
const events = [
{
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'batch-feature-1',
project: 'test-project',
ip: '127.0.0.1',
data: { name: 'batch-feature-1' },
},
{
type: FEATURE_UPDATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'batch-feature-2',
project: 'test-project',
ip: '127.0.0.1',
data: { name: 'batch-feature-2' },
},
];
await eventStoreService.transactional(async (transactionalEventStore) => {
await transactionalEventStore.batchStore(events);
}, mockTransactionContext);
const allEvents = await eventStore.getAll();
const batchEvents = allEvents.filter(
(e) =>
e.featureName === 'batch-feature-1' ||
e.featureName === 'batch-feature-2',
);
expect(batchEvents).toHaveLength(2);
batchEvents.forEach((event) => {
expect(event.groupType).toBe('transaction');
expect(event.groupId).toBe('01HQVX5K8P9BATCH123456');
});
});
test('Should auto-generate transaction context when none provided', async () => {
const eventStoreService = withTransactional(
(db) => new EventStore(db, getLogger),
db.rawDatabase,
);
const event = {
type: FEATURE_CREATED,
createdBy: 'test-user',
createdByUserId: TEST_USER_ID,
featureName: 'test-feature-auto-context',
project: 'test-project',
ip: '127.0.0.1',
data: {
name: 'test-feature-auto-context',
enabled: true,
strategies: [{ name: 'default' }],
},
};
await eventStoreService.transactional(async (transactionalEventStore) => {
await transactionalEventStore.store(event);
});
const events = await eventStore.getAll();
const storedEvent = events.find(
(e) => e.featureName === 'test-feature-auto-context',
);
expect(storedEvent).toBeTruthy();
expect(storedEvent!.groupType).toBe('transaction');
expect(storedEvent!.groupId).toBeTruthy();
expect(typeof storedEvent!.groupId).toBe('string');
expect(storedEvent!.groupId).toMatch(/^[0-9A-HJKMNP-TV-Z]{26}$/);
});