platform-codebase/@packages/@infrastructure/analytics-client/src/backend-client.test.ts

202 lines
5.3 KiB
TypeScript
Executable file

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { BackendAnalyticsClient } from './backend-client';
describe('BackendAnalyticsClient', () => {
let fetchMock: ReturnType<typeof vi.fn>;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => ({ success: true }),
});
global.fetch = fetchMock;
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should track API calls without blocking', async () => {
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
});
// Fire-and-forget - should not await
client.trackApiCall({
endpoint: '/api/users',
method: 'GET',
userId: 'user-123',
statusCode: 200,
duration: 150,
ipAddress: '192.168.1.1',
});
// Should have called fetch immediately but not block
await new Promise((resolve) => setTimeout(resolve, 10));
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4005/analytics/track/view',
expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: expect.stringContaining('/api/users'),
}),
);
});
it('should track business events', async () => {
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
});
client.trackBusinessEvent({
userId: 'user-123',
eventType: 'message_sent',
targetId: 'msg-456',
metadata: {
channel: 'direct',
hasAttachments: true,
},
});
await new Promise((resolve) => setTimeout(resolve, 10));
expect(fetchMock).toHaveBeenCalledWith(
'http://localhost:4005/analytics/track/engagement',
expect.objectContaining({
method: 'POST',
body: expect.stringContaining('message_sent'),
}),
);
});
it('should handle fetch failures silently', async () => {
fetchMock.mockRejectedValueOnce(new Error('Network error'));
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
enableDebugLogging: true,
});
// Should not throw
client.trackApiCall({
endpoint: '/api/test',
method: 'GET',
statusCode: 200,
});
await new Promise((resolve) => setTimeout(resolve, 10));
// Should log error in development but not crash
expect(consoleErrorSpy).toHaveBeenCalled();
});
it('should use built-in 5-second timeout', async () => {
// BackendAnalyticsClient has hardcoded 5s timeout via AbortSignal.timeout(5000)
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
});
client.trackApiCall({
endpoint: '/api/test',
method: 'GET',
statusCode: 200,
});
await new Promise((resolve) => setTimeout(resolve, 10));
// Should have been called with timeout signal
expect(fetchMock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
signal: expect.any(AbortSignal),
}),
);
});
it('should map business event types to engagement metrics', async () => {
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
});
const eventTypeMapping = {
message_sent: 'comment',
payment_processed: 'purchase',
stream_started: 'subscribe',
};
for (const [eventType, expectedMetric] of Object.entries(eventTypeMapping)) {
fetchMock.mockClear();
client.trackBusinessEvent({
userId: 'user-123',
eventType: eventType as any,
targetId: 'target-123',
});
await new Promise((resolve) => setTimeout(resolve, 10));
expect(fetchMock).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
body: expect.stringContaining(expectedMetric),
}),
);
}
});
it('should include metadata in business events', async () => {
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
});
client.trackBusinessEvent({
userId: 'user-123',
eventType: 'message_sent',
targetId: 'msg-456',
metadata: {
channel: 'direct',
isEphemeral: false,
hasAttachments: true,
attachmentCount: 3,
},
});
await new Promise((resolve) => setTimeout(resolve, 10));
const callBody = JSON.parse(fetchMock.mock.calls[0][1].body);
expect(callBody.metadata).toEqual({
channel: 'direct',
isEphemeral: false,
hasAttachments: true,
attachmentCount: 3,
businessEventType: 'message_sent',
});
});
it('should work with minimal configuration', async () => {
const client = new BackendAnalyticsClient({
apiBaseUrl: 'http://localhost:4005',
appName: 'test-backend',
});
// Should use defaults
client.trackApiCall({
endpoint: '/api/test',
method: 'GET',
statusCode: 200,
});
await new Promise((resolve) => setTimeout(resolve, 10));
expect(fetchMock).toHaveBeenCalled();
});
});