import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { BackendAnalyticsClient } from './backend-client'; describe('BackendAnalyticsClient', () => { let fetchMock: ReturnType; let consoleErrorSpy: ReturnType; 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(); }); });