fix: Prevent manual tracking of internal events that are automatically tracked

This commit is contained in:
Dries Augustyns
2025-12-12 20:30:49 +01:00
parent a555a12736
commit f34052ed73
4 changed files with 419 additions and 3 deletions
+34 -3
View File
@@ -5,6 +5,7 @@ import signale from 'signale';
import {prisma} from '../database/prisma.js';
import {redis} from '../database/redis.js';
import {ValidationError} from '../exceptions/index.js';
import {Keys} from './keys.js';
import {WorkflowExecutionService} from './WorkflowExecutionService.js';
@@ -289,9 +290,9 @@ export class EventService {
* @param eventName - The event name to delete
*/
public static async deleteEvent(projectId: string, eventName: string): Promise<{deletedCount: number}> {
// Prevent deletion of system events
if (eventName.startsWith('email.') || eventName.startsWith('segment.')) {
throw new Error('Cannot delete system events (email.* or segment.*)');
// Prevent deletion of reserved system events
if (this.isReservedEvent(eventName)) {
throw new Error(`Cannot delete reserved system event: ${eventName}`);
}
// Check if event is in use
@@ -313,6 +314,36 @@ export class EventService {
return {deletedCount: result.count};
}
/**
* Check if an event name is reserved for system use
* Reserved patterns:
* - email.* (email.sent, email.delivery, email.open, email.click, email.bounce, email.complaint)
* - contact.subscribed, contact.unsubscribed
* - segment.*.entry, segment.*.exit
*
* @param eventName - The event name to check
* @returns true if the event is reserved, false otherwise
*/
public static isReservedEvent(eventName: string): boolean {
// Email events: email.*
if (eventName.startsWith('email.')) {
return true;
}
// Contact events: contact.subscribed, contact.unsubscribed
if (eventName === 'contact.subscribed' || eventName === 'contact.unsubscribed') {
return true;
}
// Segment events: segment.*.entry, segment.*.exit
// Pattern: segment.<slug>.entry or segment.<slug>.exit
if (eventName.startsWith('segment.') && (eventName.endsWith('.entry') || eventName.endsWith('.exit'))) {
return true;
}
return false;
}
/**
* Trigger workflows based on an event
* Uses Redis caching for enabled workflows to improve performance
@@ -853,4 +853,90 @@ describe('EventService', () => {
});
});
});
// ========================================
// RESERVED EVENT VALIDATION
// ========================================
describe('isReservedEvent', () => {
describe('Email events (email.*)', () => {
it('should identify email.sent as reserved', () => {
expect(EventService.isReservedEvent('email.sent')).toBe(true);
});
it('should identify email.delivery as reserved', () => {
expect(EventService.isReservedEvent('email.delivery')).toBe(true);
});
it('should identify email.open as reserved', () => {
expect(EventService.isReservedEvent('email.open')).toBe(true);
});
it('should identify email.click as reserved', () => {
expect(EventService.isReservedEvent('email.click')).toBe(true);
});
it('should identify email.bounce as reserved', () => {
expect(EventService.isReservedEvent('email.bounce')).toBe(true);
});
it('should identify email.complaint as reserved', () => {
expect(EventService.isReservedEvent('email.complaint')).toBe(true);
});
it('should identify any email.* pattern as reserved', () => {
expect(EventService.isReservedEvent('email.custom')).toBe(true);
expect(EventService.isReservedEvent('email.anything')).toBe(true);
});
});
describe('Contact events', () => {
it('should identify contact.subscribed as reserved', () => {
expect(EventService.isReservedEvent('contact.subscribed')).toBe(true);
});
it('should identify contact.unsubscribed as reserved', () => {
expect(EventService.isReservedEvent('contact.unsubscribed')).toBe(true);
});
it('should not identify other contact.* events as reserved', () => {
expect(EventService.isReservedEvent('contact.created')).toBe(false);
expect(EventService.isReservedEvent('contact.updated')).toBe(false);
});
});
describe('Segment events (segment.*.entry, segment.*.exit)', () => {
it('should identify segment.*.entry as reserved', () => {
expect(EventService.isReservedEvent('segment.vip-users.entry')).toBe(true);
expect(EventService.isReservedEvent('segment.premium.entry')).toBe(true);
expect(EventService.isReservedEvent('segment.active-subscribers.entry')).toBe(true);
});
it('should identify segment.*.exit as reserved', () => {
expect(EventService.isReservedEvent('segment.vip-users.exit')).toBe(true);
expect(EventService.isReservedEvent('segment.premium.exit')).toBe(true);
expect(EventService.isReservedEvent('segment.active-subscribers.exit')).toBe(true);
});
it('should not identify other segment.* events as reserved', () => {
expect(EventService.isReservedEvent('segment.created')).toBe(false);
expect(EventService.isReservedEvent('segment.vip-users.updated')).toBe(false);
expect(EventService.isReservedEvent('segment.premium')).toBe(false);
});
});
describe('Non-reserved events', () => {
it('should not identify custom user events as reserved', () => {
expect(EventService.isReservedEvent('user.signup')).toBe(false);
expect(EventService.isReservedEvent('purchase.completed')).toBe(false);
expect(EventService.isReservedEvent('order.placed')).toBe(false);
expect(EventService.isReservedEvent('custom.event')).toBe(false);
});
it('should not identify events with similar prefixes as reserved', () => {
expect(EventService.isReservedEvent('emails.sent')).toBe(false);
expect(EventService.isReservedEvent('contacts.subscribed')).toBe(false);
expect(EventService.isReservedEvent('segments.entry')).toBe(false);
});
});
});
});