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:
parent
f6ab7460c6
commit
681ce3bfd9
@ -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 {
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -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}$/);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user