mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-28 17:55:15 +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;
|
id: number;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
|
groupType?: string;
|
||||||
|
groupId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IEnrichedEvent extends IEvent {
|
export interface IEnrichedEvent extends IEvent {
|
||||||
|
@ -41,6 +41,8 @@ const EVENT_COLUMNS = [
|
|||||||
'feature_name',
|
'feature_name',
|
||||||
'project',
|
'project',
|
||||||
'environment',
|
'environment',
|
||||||
|
'group_type',
|
||||||
|
'group_id',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type IQueryOperations =
|
export type IQueryOperations =
|
||||||
@ -97,6 +99,8 @@ export interface IEventTable {
|
|||||||
environment?: string;
|
environment?: string;
|
||||||
tags: ITag[];
|
tags: ITag[];
|
||||||
ip?: string;
|
ip?: string;
|
||||||
|
group_type: string | null;
|
||||||
|
group_id: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TABLE = 'events';
|
const TABLE = 'events';
|
||||||
@ -157,7 +161,9 @@ export class EventStore implements IEventStore {
|
|||||||
|
|
||||||
async batchStore(events: IBaseEvent[]): Promise<void> {
|
async batchStore(events: IBaseEvent[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.db(TABLE).insert(events.map(this.eventToDbRow));
|
await this.db(TABLE).insert(
|
||||||
|
events.map((event) => this.eventToDbRow(event)),
|
||||||
|
);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Failed to store events: ${JSON.stringify(events)}`,
|
`Failed to store events: ${JSON.stringify(events)}`,
|
||||||
@ -472,10 +478,14 @@ export class EventStore implements IEventStore {
|
|||||||
featureName: row.feature_name,
|
featureName: row.feature_name,
|
||||||
project: row.project,
|
project: row.project,
|
||||||
environment: row.environment,
|
environment: row.environment,
|
||||||
|
groupType: row.group_type || undefined,
|
||||||
|
groupId: row.group_id || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
|
eventToDbRow(e: IBaseEvent): Omit<IEventTable, 'id' | 'created_at'> {
|
||||||
|
const transactionContext = this.db.userParams;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: e.type,
|
type: e.type,
|
||||||
created_by: e.createdBy ?? 'admin',
|
created_by: e.createdBy ?? 'admin',
|
||||||
@ -490,6 +500,8 @@ export class EventStore implements IEventStore {
|
|||||||
project: e.project,
|
project: e.project,
|
||||||
environment: e.environment,
|
environment: e.environment,
|
||||||
ip: e.ip,
|
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.',
|
'The IP address of the user that created the event. Only available in Enterprise.',
|
||||||
example: '192.168.1.1',
|
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: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
@ -18,6 +18,11 @@ import dbInit, { type ITestDb } from '../helpers/database-init.js';
|
|||||||
import getLogger from '../../fixtures/no-logger.js';
|
import getLogger from '../../fixtures/no-logger.js';
|
||||||
import type { IEventStore } from '../../../lib/types/stores/event-store.js';
|
import type { IEventStore } from '../../../lib/types/stores/event-store.js';
|
||||||
import type { IAuditUser, IUnleashStores } from '../../../lib/types/index.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';
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
@ -472,3 +477,155 @@ test('Should return empty result when filtering by non-existent ID', async () =>
|
|||||||
|
|
||||||
expect(filteredEvents).toHaveLength(0);
|
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