202 lines
5.3 KiB
TypeScript
Executable file
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();
|
|
});
|
|
});
|