autotel
Version:
Write Once, Observe Anywhere
1,105 lines (915 loc) • 33.5 kB
text/typescript
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
import { Event, getEvents, resetEvents } from './event';
import { type Logger } from './logger';
import { init } from './init';
import { shutdown } from './shutdown';
import { trace } from './functional';
import { context, propagation } from '@opentelemetry/api';
describe('Events', () => {
let mockLogger: Logger;
beforeEach(() => {
resetEvents();
mockLogger = {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
};
});
describe('trackEvent', () => {
it('should track business events', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackEvent('application.submitted', {
jobId: '123',
userId: '456',
});
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
event: 'application.submitted',
attributes: { service: 'test-service', jobId: '123', userId: '456' },
},
'Event tracked',
);
});
it('should track events without attributes', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackEvent('user.login');
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
event: 'user.login',
attributes: { service: 'test-service' },
},
'Event tracked',
);
});
});
describe('trackFunnelStep', () => {
it('should track funnel progression', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackFunnelStep('checkout', 'started', { cartValue: 99.99 });
event.trackFunnelStep('checkout', 'completed', { cartValue: 99.99 });
expect(mockLogger.info).toHaveBeenCalledTimes(2);
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
funnel: 'checkout',
status: 'started',
attributes: { service: 'test-service', cartValue: 99.99 },
},
'Funnel step tracked',
);
});
it('should track funnel abandonment', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackFunnelStep('checkout', 'abandoned', { reason: 'timeout' });
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
funnel: 'checkout',
status: 'abandoned',
attributes: { service: 'test-service', reason: 'timeout' },
},
'Funnel step tracked',
);
});
});
describe('trackOutcome', () => {
it('should track successful outcomes', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackOutcome('email.delivery', 'success', {
recipientType: 'school',
});
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
operation: 'email.delivery',
status: 'success',
attributes: { service: 'test-service', recipientType: 'school' },
},
'Outcome tracked',
);
});
it('should track failed outcomes', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackOutcome('email.delivery', 'failure', {
error: 'invalid_email',
});
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
operation: 'email.delivery',
status: 'failure',
attributes: { service: 'test-service', error: 'invalid_email' },
},
'Outcome tracked',
);
});
it('should track partial outcomes', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackOutcome('batch.process', 'partial', {
successCount: 8,
failureCount: 2,
});
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
{
operation: 'batch.process',
status: 'partial',
attributes: {
service: 'test-service',
successCount: 8,
failureCount: 2,
},
},
'Outcome tracked',
);
});
});
describe('trackValue', () => {
it('should track revenue metrics', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackValue('order.revenue', 149.99, {
currency: 'USD',
productCategory: 'electronics',
});
// Pino-native: (extra, message)
expect(mockLogger.debug).toHaveBeenCalledWith(
{
metric: 'order.revenue',
value: 149.99,
attributes: {
service: 'test-service',
metric: 'order.revenue',
currency: 'USD',
productCategory: 'electronics',
},
},
'Value tracked',
);
});
it('should track processing time', () => {
const event = new Event('test-service', { logger: mockLogger });
event.trackValue('application.processing_time', 2500, {
unit: 'ms',
});
// Pino-native: (extra, message)
expect(mockLogger.debug).toHaveBeenCalledWith(
{
metric: 'application.processing_time',
value: 2500,
attributes: {
service: 'test-service',
metric: 'application.processing_time',
unit: 'ms',
},
},
'Value tracked',
);
});
});
describe('getEvents', () => {
it('should return singleton instance', () => {
const events1 = getEvents('test-service');
const events2 = getEvents('test-service');
expect(events1).toBe(events2);
});
it('should return different instances for different services', () => {
const events1 = getEvents('service-1');
const events2 = getEvents('service-2');
expect(events1).not.toBe(events2);
});
it('should reset instances', () => {
const events1 = getEvents('test-service');
resetEvents();
const events2 = getEvents('test-service');
expect(events1).not.toBe(events2);
});
});
describe('real-world usage example', () => {
it('should track job application flow', () => {
const event = new Event('job-application', {
logger: mockLogger,
});
// User starts application
event.trackFunnelStep('application', 'started', { jobId: '123' });
// User submits application
event.trackEvent('application.submitted', {
jobId: '123',
userId: '456',
});
event.trackFunnelStep('application', 'completed', { jobId: '123' });
// Email sent successfully
event.trackOutcome('email.sent', 'success', {
recipientType: 'school',
jobId: '123',
});
expect(mockLogger.info).toHaveBeenCalledTimes(4);
});
it('should track email delivery failures', () => {
const event = new Event('email-service', { logger: mockLogger });
// Failed email delivery
event.trackOutcome('email.delivery', 'failure', {
error: 'invalid_email',
recipientEmail: 'redacted',
});
// Track event for alerting
event.trackEvent('email.bounce', {
bounceType: 'permanent',
});
expect(mockLogger.info).toHaveBeenCalledTimes(2);
});
});
describe('automatic telemetry context enrichment', () => {
beforeEach(() => {
resetEvents();
});
afterEach(async () => {
await shutdown();
});
// Test without config first (before any init() is called)
it('should still work without config (graceful degradation)', () => {
// Don't initialize - no config available
const event = new Event('test-service', { logger: mockLogger });
event.trackEvent('user.login');
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: {
service: 'test-service',
// No version/environment - gracefully omitted
},
}),
'Event tracked',
);
});
it('should auto-capture resource attributes (service.version, deployment.environment)', () => {
// Initialize with config
init({
service: 'test-service',
version: '2.1.0',
environment: 'production',
});
const event = new Event('test-service', { logger: mockLogger });
event.trackEvent('user.signup', { userId: '123' });
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
service: 'test-service',
'service.version': '2.1.0',
'deployment.environment': 'production',
userId: '123',
}),
}),
'Event tracked',
);
});
it('should auto-capture trace context (traceId, spanId, correlationId) when inside a trace', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const tracedOperation = trace('test.operation', async () => {
event.trackEvent('operation.started', { step: 1 });
});
await tracedOperation();
// Pino-native: first arg is the extra object
const capturedCall = (mockLogger.info as ReturnType<typeof vi.fn>).mock
.calls[0];
const attributes = capturedCall[0].attributes;
expect(attributes).toHaveProperty('traceId');
expect(attributes).toHaveProperty('spanId');
expect(attributes).toHaveProperty('correlationId');
expect(typeof attributes.traceId).toBe('string');
expect(typeof attributes.spanId).toBe('string');
expect(typeof attributes.correlationId).toBe('string');
// Correlation ID should be first 16 chars of traceId
expect(attributes.correlationId).toBe(attributes.traceId.slice(0, 16));
});
it('should enrich trackFunnelStep with telemetry context', async () => {
init({
service: 'test-service',
version: '1.5.0',
environment: 'staging',
});
const event = new Event('test-service', { logger: mockLogger });
const tracedOperation = trace('checkout.flow', async () => {
event.trackFunnelStep('checkout', 'started', { cartValue: 99.99 });
});
await tracedOperation();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
service: 'test-service',
'service.version': '1.5.0',
'deployment.environment': 'staging',
cartValue: 99.99,
traceId: expect.any(String),
spanId: expect.any(String),
correlationId: expect.any(String),
}),
}),
'Funnel step tracked',
);
});
it('should enrich trackOutcome with telemetry context', async () => {
init({
service: 'test-service',
version: '3.0.0',
environment: 'development',
});
const event = new Event('test-service', { logger: mockLogger });
const tracedOperation = trace('email.send', async () => {
event.trackOutcome('email.delivery', 'success', {
recipientType: 'user',
});
});
await tracedOperation();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
service: 'test-service',
'service.version': '3.0.0',
'deployment.environment': 'development',
recipientType: 'user',
traceId: expect.any(String),
spanId: expect.any(String),
correlationId: expect.any(String),
}),
}),
'Outcome tracked',
);
});
it('should enrich trackValue with telemetry context', async () => {
init({
service: 'test-service',
version: '4.2.1',
environment: 'production',
});
const event = new Event('test-service', { logger: mockLogger });
const tracedOperation = trace('order.process', async () => {
event.trackValue('order.revenue', 149.99, { currency: 'USD' });
});
await tracedOperation();
// Pino-native: (extra, message)
expect(mockLogger.debug).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
service: 'test-service',
metric: 'order.revenue',
'service.version': '4.2.1',
'deployment.environment': 'production',
currency: 'USD',
traceId: expect.any(String),
spanId: expect.any(String),
correlationId: expect.any(String),
}),
}),
'Value tracked',
);
});
it('should still work outside a trace (no trace context)', () => {
init({
service: 'test-service',
version: '1.0.0',
environment: 'test',
});
const event = new Event('test-service', { logger: mockLogger });
// Call outside a trace
event.trackEvent('background.job.completed', { jobId: 'job-123' });
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: {
service: 'test-service',
'service.version': '1.0.0',
'deployment.environment': 'test',
jobId: 'job-123',
// No traceId/spanId/correlationId - gracefully omitted
},
}),
'Event tracked',
);
});
});
describe('automatic operation context enrichment', () => {
beforeEach(() => {
resetEvents();
});
afterEach(async () => {
await shutdown();
});
it('should auto-capture operation.name when inside trace() with string name', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const operation = trace('user.create', async () => {
event.trackEvent('user.created', { userId: '123' });
});
await operation();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'user.create',
userId: '123',
}),
}),
'Event tracked',
);
});
it('should auto-capture operation.name when inside trace() with named function', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const createUser = trace(async function createUser() {
event.trackEvent('user.created', { userId: '456' });
});
await createUser();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
// Function name might be inferred with slight variations (e.g., 'createUser2')
// The important thing is that operation.name is auto-captured
'operation.name': expect.stringMatching(/createUser/),
userId: '456',
}),
}),
'Event tracked',
);
});
it('should reliably infer function names in both factory and non-factory patterns', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
// Test 1: Named function declaration (non-factory pattern)
// Should infer name from function declaration
const updateUser = trace(async function updateUser(userId: string) {
event.trackEvent('user.updated', { userId });
});
await updateUser('user-123');
// Test 2: Named function with factory pattern (ctx parameter)
// Explicit name should take precedence
const deleteUser = trace(
'user.delete',
(ctx) =>
async function deleteUser(userId: string) {
ctx.setAttribute('user.id', userId);
event.trackEvent('user.deleted', { userId });
},
);
await deleteUser('user-456');
// Test 3: Named function in factory pattern (should infer inner function name)
const createOrder = trace(
(ctx) =>
async function createOrder(orderId: string) {
ctx.setAttribute('order.id', orderId);
event.trackEvent('order.created', { orderId });
},
);
await createOrder('order-789');
// Verify all operations captured correct names
// Pino-native: first arg is the extra object
const calls = (mockLogger.info as ReturnType<typeof vi.fn>).mock.calls;
// First call: updateUser - should infer from named function declaration
expect(calls[0][0].attributes['operation.name']).toMatch(/updateUser/);
// Second call: user.delete - explicit name takes precedence
expect(calls[1][0].attributes['operation.name']).toBe('user.delete');
// Third call: createOrder - should infer from inner named function in factory pattern
expect(calls[2][0].attributes['operation.name']).toMatch(/createOrder/);
});
it('should auto-capture operation.name in nested spans', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const { span } = await import('./functional');
const operation = trace('order.process', async () => {
span({ name: 'order.validate' }, () => {
// Should capture the innermost operation name
event.trackEvent('order.validated', { orderId: 'ord_123' });
});
});
await operation();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'order.validate',
orderId: 'ord_123',
}),
}),
'Event tracked',
);
});
it('should auto-capture operation.name in trackFunnelStep', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const checkout = trace('checkout.flow', async () => {
event.trackFunnelStep('checkout', 'started', {
cartValue: 99.99,
});
});
await checkout();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'checkout.flow',
cartValue: 99.99,
}),
}),
'Funnel step tracked',
);
});
it('should auto-capture operation.name in trackOutcome', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const sendEmail = trace('email.send', async () => {
event.trackOutcome('email.delivery', 'success', {
recipientType: 'user',
});
});
await sendEmail();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'email.send',
recipientType: 'user',
}),
}),
'Outcome tracked',
);
});
it('should auto-capture operation.name in trackValue', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const processOrder = trace('order.process', async () => {
event.trackValue('order.revenue', 149.99, { currency: 'USD' });
});
await processOrder();
// Pino-native: (extra, message)
expect(mockLogger.debug).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'order.process',
currency: 'USD',
}),
}),
'Value tracked',
);
});
it('should handle missing operation.name gracefully (outside trace)', () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
// Call outside any trace
event.trackEvent('background.job', { jobId: 'job-123' });
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: {
service: 'test-service',
'service.version': undefined,
'deployment.environment': undefined,
jobId: 'job-123',
// No operation.name - gracefully omitted
},
}),
'Event tracked',
);
});
it('should capture parent operation.name when not in nested span', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const parentOperation = trace('parent.operation', async () => {
// Track event in parent context (not in a nested span)
event.trackEvent('parent.event', { step: 1 });
// Then create a nested span
const { span } = await import('./functional');
span({ name: 'child.operation' }, () => {
event.trackEvent('child.event', { step: 2 });
});
});
await parentOperation();
// Check parent event has parent operation name
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'parent.operation',
step: 1,
}),
}),
'Event tracked',
);
// Check child event has child operation name
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenNthCalledWith(
2,
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'child.operation',
step: 2,
}),
}),
'Event tracked',
);
});
it('should work with trace() factory pattern', async () => {
init({ service: 'test-service' });
const event = new Event('test-service', { logger: mockLogger });
const operation = trace('factory.operation', (ctx) => async () => {
ctx.setAttribute('custom', 'attribute');
event.trackEvent('factory.event', { data: 'test' });
});
await operation();
// Pino-native: (extra, message)
expect(mockLogger.info).toHaveBeenCalledWith(
expect.objectContaining({
attributes: expect.objectContaining({
'operation.name': 'factory.operation',
data: 'test',
}),
}),
'Event tracked',
);
});
});
describe('autotel trace context', () => {
beforeEach(() => {
resetEvents();
});
afterEach(async () => {
await shutdown();
});
it('should always include correlation_id even without includeTraceContext', async () => {
init({
service: 'test-service',
// events.includeTraceContext is false by default
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
event.trackEvent('test.event', { userId: '123' });
// Wait for async subscriber notification
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockSubscriber.trackEvent).toHaveBeenCalledWith(
'test.event',
expect.any(Object),
expect.objectContaining({
autotel: expect.objectContaining({
correlation_id: expect.any(String),
}),
}),
);
// Correlation ID should be 16 hex chars
const call = mockSubscriber.trackEvent.mock.calls[0];
const autotelContext = call[2].autotel;
expect(autotelContext.correlation_id).toHaveLength(16);
expect(/^[0-9a-f]{16}$/.test(autotelContext.correlation_id)).toBe(true);
});
it('should include full trace context when includeTraceContext is enabled', async () => {
init({
service: 'test-service',
events: {
includeTraceContext: true,
},
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
// Track event inside a trace
const tracedOperation = trace('test.operation', async () => {
event.trackEvent('traced.event', { data: 'test' });
});
await tracedOperation();
// Wait for async subscriber notification
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockSubscriber.trackEvent).toHaveBeenCalledWith(
'traced.event',
expect.any(Object),
expect.objectContaining({
autotel: expect.objectContaining({
trace_id: expect.any(String),
span_id: expect.any(String),
trace_flags: expect.any(String),
correlation_id: expect.any(String),
}),
}),
);
// Verify trace_id is 32 hex chars
const call = mockSubscriber.trackEvent.mock.calls[0];
const autotelContext = call[2].autotel;
expect(autotelContext.trace_id).toHaveLength(32);
expect(/^[0-9a-f]{32}$/.test(autotelContext.trace_id)).toBe(true);
// Verify span_id is 16 hex chars
expect(autotelContext.span_id).toHaveLength(16);
expect(/^[0-9a-f]{16}$/.test(autotelContext.span_id)).toBe(true);
// Verify trace_flags is 2 hex chars
expect(autotelContext.trace_flags).toHaveLength(2);
});
it('should call traceUrl function when configured', async () => {
const traceUrlFn = vi
.fn()
.mockReturnValue('https://traces.example.com/trace/abc123');
init({
service: 'test-service',
events: {
includeTraceContext: true,
traceUrl: traceUrlFn,
},
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
// Track event inside a trace
const tracedOperation = trace('test.operation', async () => {
event.trackEvent('traced.event', {});
});
await tracedOperation();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(traceUrlFn).toHaveBeenCalledWith(
expect.objectContaining({
traceId: expect.any(String),
spanId: expect.any(String),
correlationId: expect.any(String),
serviceName: 'test-service',
}),
);
expect(mockSubscriber.trackEvent).toHaveBeenCalledWith(
'traced.event',
expect.any(Object),
expect.objectContaining({
autotel: expect.objectContaining({
trace_url: 'https://traces.example.com/trace/abc123',
}),
}),
);
});
it('should include autotel context in trackFunnelStep', async () => {
init({
service: 'test-service',
events: {
includeTraceContext: true,
},
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
const tracedOperation = trace('checkout.flow', async () => {
event.trackFunnelStep('checkout', 'started', { cartValue: 99.99 });
});
await tracedOperation();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockSubscriber.trackFunnelStep).toHaveBeenCalledWith(
'checkout',
'started',
expect.any(Object),
expect.objectContaining({
autotel: expect.objectContaining({
trace_id: expect.any(String),
correlation_id: expect.any(String),
}),
}),
);
});
it('should include autotel context in trackOutcome', async () => {
init({
service: 'test-service',
events: {
includeTraceContext: true,
},
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
const tracedOperation = trace('payment.process', async () => {
event.trackOutcome('payment', 'success', { amount: 99.99 });
});
await tracedOperation();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockSubscriber.trackOutcome).toHaveBeenCalledWith(
'payment',
'success',
expect.any(Object),
expect.objectContaining({
autotel: expect.objectContaining({
trace_id: expect.any(String),
correlation_id: expect.any(String),
}),
}),
);
});
it('should include autotel context in trackValue', async () => {
init({
service: 'test-service',
events: {
includeTraceContext: true,
},
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
const tracedOperation = trace('order.process', async () => {
event.trackValue('revenue', 149.99, { currency: 'USD' });
});
await tracedOperation();
await new Promise((resolve) => setTimeout(resolve, 50));
expect(mockSubscriber.trackValue).toHaveBeenCalledWith(
'revenue',
149.99,
expect.any(Object),
expect.objectContaining({
autotel: expect.objectContaining({
trace_id: expect.any(String),
correlation_id: expect.any(String),
}),
}),
);
});
});
describe('baggage enrichment', () => {
afterEach(async () => {
await shutdown();
});
it('should apply maxBytes after hashing baggage values', async () => {
init({
service: 'test-service',
events: {
enrichFromBaggage: {
allow: ['user.id'],
maxBytes: 16,
transform: {
'user.id': 'hash',
},
},
},
});
const mockSubscriber = {
name: 'MockSubscriber',
trackEvent: vi.fn().mockResolvedValue(undefined),
trackFunnelStep: vi.fn().mockResolvedValue(undefined),
trackOutcome: vi.fn().mockResolvedValue(undefined),
trackValue: vi.fn().mockResolvedValue(undefined),
};
const event = new Event('test-service', {
subscribers: [mockSubscriber],
});
const largeValue = 'x'.repeat(2048);
const baggage = propagation
.createBaggage()
.setEntry('user.id', { value: largeValue });
const ctx = propagation.setBaggage(context.active(), baggage);
context.with(ctx, () => {
event.trackEvent('test.event', { foo: 'bar' });
});
await new Promise((resolve) => setTimeout(resolve, 50));
const call = mockSubscriber.trackEvent.mock.calls[0];
const attributes = call[1] as Record<string, unknown>;
expect(attributes['user.id']).toBeDefined();
expect(String(attributes['user.id'])).toHaveLength(8);
});
});
});