@oxy.so/telemetry 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -12
- package/dist/cjs/browser.js +8 -2
- package/dist/cjs/collector.js +180 -0
- package/dist/cjs/edge.js +137 -0
- package/dist/cjs/socket.js +102 -0
- package/dist/esm/browser.js +7 -1
- package/dist/esm/collector.js +171 -0
- package/dist/esm/edge.js +134 -0
- package/dist/esm/socket.js +97 -0
- package/dist/types/browser.d.ts +2 -0
- package/dist/types/collector.d.ts +50 -0
- package/dist/types/edge.d.ts +32 -0
- package/dist/types/socket.d.ts +50 -0
- package/package.json +41 -2
- package/src/__tests__/browser.test.ts +21 -0
- package/src/__tests__/collector.test.ts +95 -0
- package/src/__tests__/edge.test.ts +110 -0
- package/src/__tests__/socket.test.ts +118 -0
- package/src/browser.ts +8 -1
- package/src/collector.ts +192 -0
- package/src/edge.ts +117 -0
- package/src/socket.ts +131 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { createTrafficCollector, instrumentTrafficFetch, trafficMiddleware, trafficType, type TrafficAggregate } from '../collector';
|
|
4
|
+
|
|
5
|
+
describe('ecosystem activity collector', () => {
|
|
6
|
+
it('reports all products independently of dashboard viewers and counts errors too', async () => {
|
|
7
|
+
const batches: TrafficAggregate[][] = [];
|
|
8
|
+
const collector = createTrafficCollector(async events => { batches.push(events); });
|
|
9
|
+
for (const service of ['website', 'mention', 'mercaria', 'alia', 'kaana']) {
|
|
10
|
+
const response = new EventEmitter();
|
|
11
|
+
trafficMiddleware(collector, service, 'us-west-2')({ path: '/api/files/private-id', headers: { 'cf-ray': 'example-NRT', authorization: 'secret' } }, response, () => {});
|
|
12
|
+
response.emit('finish');
|
|
13
|
+
response.emit('close');
|
|
14
|
+
}
|
|
15
|
+
await collector.flush();
|
|
16
|
+
expect(batches[0]).toHaveLength(10);
|
|
17
|
+
expect(batches[0].filter(event => event.direction === 'inbound')).toHaveLength(5);
|
|
18
|
+
expect(batches[0].every(event => event.activityType === 'media')).toBe(true);
|
|
19
|
+
expect(JSON.stringify(batches)).not.toMatch(/private-id|secret|authorization/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('keeps scope, direction and category independent for co-located internal services', async () => {
|
|
23
|
+
const batches: TrafficAggregate[][] = [];
|
|
24
|
+
const collector = createTrafficCollector(async events => { batches.push(events); });
|
|
25
|
+
const response = new EventEmitter();
|
|
26
|
+
const request = { path: '/v1/chat/completions', headers: { 'x-oxy-source-region': 'us-west-2' }, serviceApp: undefined as undefined | { appName: string } };
|
|
27
|
+
trafficMiddleware(collector, 'kaana', 'us-west-2')(request, response, () => { request.serviceApp = { appName: 'oxy-api' }; });
|
|
28
|
+
response.emit('finish');
|
|
29
|
+
await collector.flush();
|
|
30
|
+
expect(batches[0]).toEqual([
|
|
31
|
+
expect.objectContaining({ sourceRegion: 'us-west-2', targetRegion: 'us-west-2', sourceService: 'oxy-api', targetService: 'kaana', direction: 'inbound', scope: 'internal', activityType: 'ai' }),
|
|
32
|
+
expect.objectContaining({ sourceService: 'kaana', targetService: 'oxy-api', direction: 'outbound', scope: 'internal', activityType: 'ai' }),
|
|
33
|
+
]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('does not fabricate a response on abort and excludes telemetry control traffic', async () => {
|
|
37
|
+
const batches: TrafficAggregate[][] = [];
|
|
38
|
+
const collector = createTrafficCollector(async events => { batches.push(events); });
|
|
39
|
+
for (const path of ['/health', '/api/ready', '/platform-stats/stream', '/internal/activity']) {
|
|
40
|
+
const response = new EventEmitter();
|
|
41
|
+
trafficMiddleware(collector, 'website', 'us-west-2')({ path, headers: {} }, response, () => {});
|
|
42
|
+
response.emit('finish');
|
|
43
|
+
}
|
|
44
|
+
const aborted = new EventEmitter();
|
|
45
|
+
trafficMiddleware(collector, 'mention', 'us-west-2')({ path: '/messages', headers: {} }, aborted, () => {});
|
|
46
|
+
aborted.emit('close');
|
|
47
|
+
await collector.flush();
|
|
48
|
+
expect(batches[0]).toHaveLength(1);
|
|
49
|
+
expect(batches[0][0].direction).toBe('inbound');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('tracks real outgoing calls and only reports the response when one arrived', async () => {
|
|
53
|
+
const batches: TrafficAggregate[][] = [];
|
|
54
|
+
const collector = createTrafficCollector(async events => { batches.push(events); });
|
|
55
|
+
const fetcher = instrumentTrafficFetch(jest.fn(async () => new Response('ok', { headers: { 'X-Oxy-Region': 'eu-central-1' } })) as unknown as typeof fetch, collector, 'alia', 'us-west-2', url => url.hostname === 'api.oxy.so' ? { service: 'oxy-api', region: 'us-west-2' } : undefined);
|
|
56
|
+
await fetcher('https://api.oxy.so/v1/chat/completions');
|
|
57
|
+
await collector.flush();
|
|
58
|
+
expect(batches[0].map(event => event.direction)).toEqual(['outbound', 'inbound']);
|
|
59
|
+
expect(batches[0].every(event => event.scope === 'internal')).toBe(true);
|
|
60
|
+
expect(batches[0][0].targetRegion).toBe('eu-central-1');
|
|
61
|
+
expect(batches[0][1].sourceRegion).toBe('eu-central-1');
|
|
62
|
+
const failed = instrumentTrafficFetch(jest.fn(async () => { throw new Error('offline'); }) as unknown as typeof fetch, collector, 'alia', 'us-west-2', () => undefined);
|
|
63
|
+
await expect(failed('https://example.com/files')).rejects.toThrow('offline');
|
|
64
|
+
await collector.flush();
|
|
65
|
+
expect(batches[1]).toHaveLength(1);
|
|
66
|
+
expect(batches[1][0]).toMatchObject({ scope: 'external', direction: 'outbound', activityType: 'media' });
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('chunks a busy fleet into ingestible batches and preserves fetch extensions', async () => {
|
|
70
|
+
const batches: TrafficAggregate[][] = [];
|
|
71
|
+
const collector = createTrafficCollector(async events => { batches.push(events); });
|
|
72
|
+
for (let index = 0; index < 300; index++) collector.record({ region: 'us-west-2', service: `service-${index}`, scope: 'internal', direction: 'inbound', activityType: 'platform' });
|
|
73
|
+
await collector.flush();
|
|
74
|
+
expect(batches.map(batch => batch.length)).toEqual([256, 44]);
|
|
75
|
+
const preconnect = jest.fn();
|
|
76
|
+
const baseFetch = Object.assign(jest.fn(async () => new Response()), { preconnect }) as unknown as typeof fetch;
|
|
77
|
+
const wrapped = instrumentTrafficFetch(baseFetch, collector, 'website', 'us-west-2', () => undefined);
|
|
78
|
+
expect((wrapped as typeof baseFetch & { preconnect: unknown }).preconnect).toBe(preconnect);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('keeps verified internal attribution when a request aborts', async () => {
|
|
82
|
+
const batches: TrafficAggregate[][] = [];
|
|
83
|
+
const collector = createTrafficCollector(async events => { batches.push(events); });
|
|
84
|
+
const response = new EventEmitter();
|
|
85
|
+
trafficMiddleware(collector, 'oxy-api', 'us-west-2')({ path: '/messages', headers: { 'x-oxy-source-region': 'eu-west-1' }, serviceApp: { appName: 'mention' } }, response, () => {});
|
|
86
|
+
response.emit('close');
|
|
87
|
+
await collector.flush();
|
|
88
|
+
expect(batches[0]).toEqual([expect.objectContaining({ scope: 'internal', direction: 'inbound', sourceRegion: 'eu-west-1', sourceService: 'mention' })]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('classifies versioned media and inference endpoints without retaining the path', () => {
|
|
92
|
+
expect(trafficType('/api/v1/uploads/private-id?token=secret')).toBe('media');
|
|
93
|
+
expect(trafficType('/v1/chat/completions')).toBe('ai');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
import { observeEdgeRequest, type EdgeActivityEnv } from '../edge';
|
|
3
|
+
|
|
4
|
+
function setup(path = '/private-name.png?email=secret@example.com') {
|
|
5
|
+
const request = new Request(`https://example.com${path}`, { headers: { 'CF-Connecting-IP': '203.0.113.8', 'X-Oxy-Source-Region': 'eu-west-1', Authorization: 'private' } });
|
|
6
|
+
Object.assign(request, { cf: { colo: 'MAD', country: 'ES' } });
|
|
7
|
+
const tasks: Promise<unknown>[] = [];
|
|
8
|
+
const env: EdgeActivityEnv = { OXY_EDGE_ACTIVITY_ENABLED: 'true', OXY_EDGE_ACTIVITY_API_KEY: 'key', OXY_EDGE_ACTIVITY_API_SECRET: 'secret' };
|
|
9
|
+
const fetcher = jest.fn(async (input: RequestInfo | URL) => String(input).endsWith('service-token') ? Response.json({ token: 'token', expiresIn: 3600 }) : new Response(null, { status: 204 }));
|
|
10
|
+
return { request, env, tasks, fetcher, ctx: { waitUntil: (task: Promise<unknown>) => tasks.push(task) }, service: 'noted', onError: jest.fn() };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
test('preserves the response stream, cache headers and media type without personal metadata', async () => {
|
|
14
|
+
const state = setup();
|
|
15
|
+
const response = new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode('image')); c.close(); } }), { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public,max-age=3600' } });
|
|
16
|
+
expect(await observeEdgeRequest({ ...state, next: () => response })).toBe(response);
|
|
17
|
+
expect(response.bodyUsed).toBe(false);
|
|
18
|
+
await Promise.all(state.tasks);
|
|
19
|
+
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
20
|
+
expect(JSON.parse(String(calls[0][1].body))).toEqual({ apiKey: 'key', apiSecret: 'secret' });
|
|
21
|
+
const events = JSON.parse(String(calls[1][1].body));
|
|
22
|
+
expect(events).toHaveLength(2);
|
|
23
|
+
expect(events[0]).toMatchObject({ region: 'edge-mad', scope: 'external', direction: 'inbound', activityType: 'media', targetRegion: 'edge-mad' });
|
|
24
|
+
expect(events[1]).toMatchObject({ direction: 'outbound', sourceRegion: 'edge-mad' });
|
|
25
|
+
expect(events[0].sourceRegion).toBeUndefined();
|
|
26
|
+
expect(JSON.stringify(events)).not.toMatch(/private|secret|203\.0|example|email|eu-west/);
|
|
27
|
+
expect(await response.text()).toBe('image');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('preserves handler errors and only reports received activity', async () => {
|
|
31
|
+
const state = setup(); const error = new Error('upstream');
|
|
32
|
+
await expect(observeEdgeRequest({ ...state, next: () => { throw error; } })).rejects.toBe(error);
|
|
33
|
+
await Promise.all(state.tasks);
|
|
34
|
+
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
35
|
+
expect(JSON.parse(String(calls[1][1].body)).map((x: {direction:string}) => x.direction)).toEqual(['inbound']);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test.each(['/health', '/api/ready', '/internal/activity', '/auth/service-token', '/cdn-cgi/trace'])('excludes control route %s', async (path) => {
|
|
39
|
+
const state = setup(path); const response = new Response(null);
|
|
40
|
+
expect(await observeEdgeRequest({ ...state, next: () => response })).toBe(response);
|
|
41
|
+
expect(state.tasks).toHaveLength(0);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('disabled bindings bypass publishing and missing enabled credentials report a fixed error', async () => {
|
|
45
|
+
const state = setup(); state.env = {};
|
|
46
|
+
await observeEdgeRequest({ ...state, next: () => new Response(null) });
|
|
47
|
+
expect(state.tasks).toHaveLength(0);
|
|
48
|
+
state.env.OXY_EDGE_ACTIVITY_ENABLED = 'true';
|
|
49
|
+
await observeEdgeRequest({ ...state, next: () => new Response(null) });
|
|
50
|
+
await Promise.all(state.tasks);
|
|
51
|
+
expect(state.onError).toHaveBeenCalledWith('Edge activity publication failed');
|
|
52
|
+
expect(state.fetcher).not.toHaveBeenCalled();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('verified peer is internal independently of inbound/outbound', async () => {
|
|
56
|
+
const state = setup();
|
|
57
|
+
await observeEdgeRequest({ ...state, peer: {service:'mention', region:'us-west-2'}, next: () => new Response(null) });
|
|
58
|
+
await Promise.all(state.tasks);
|
|
59
|
+
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
60
|
+
expect(JSON.parse(String(calls[1][1].body))).toEqual([
|
|
61
|
+
expect.objectContaining({scope:'internal', direction:'inbound', sourceRegion:'us-west-2', sourceService:'mention'}),
|
|
62
|
+
expect.objectContaining({scope:'internal', direction:'outbound', targetRegion:'us-west-2', targetService:'mention'}),
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('caches token values and invalidates on ingestion 401 without retrying a batch', async () => {
|
|
67
|
+
const state = setup();
|
|
68
|
+
const fetcher = jest.fn(async (input: RequestInfo | URL) => String(input).endsWith('service-token') ? Response.json({token:'token',expiresIn:3600}) : new Response(null, {status:401}));
|
|
69
|
+
for (let i=0;i<2;i++) { await observeEdgeRequest({...state, fetcher, next:()=>new Response(null)}); await Promise.all(state.tasks); }
|
|
70
|
+
expect(fetcher.mock.calls.map(([url])=>String(url))).toEqual(['https://api.oxy.so/auth/service-token','https://api.oxy.so/internal/activity','https://api.oxy.so/auth/service-token','https://api.oxy.so/internal/activity']);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('unknown colo stays unknown even when caller spoofs geo headers', async () => {
|
|
74
|
+
const state = setup(); Object.assign(state.request, {cf:undefined});
|
|
75
|
+
await observeEdgeRequest({...state, credential:async()=>'injected', next:()=>new Response(null)}); await Promise.all(state.tasks);
|
|
76
|
+
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
77
|
+
expect(calls).toHaveLength(1);
|
|
78
|
+
expect(JSON.parse(String(calls[0][1].body))[0].region).toBe('unknown');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('reuses only a token value on later requests and refreshes after credential rotation', async () => {
|
|
82
|
+
const state = setup();
|
|
83
|
+
for (let i=0;i<2;i++) { await observeEdgeRequest({...state,next:()=>new Response(null)}); await Promise.all(state.tasks); }
|
|
84
|
+
expect(state.fetcher.mock.calls).toHaveLength(3);
|
|
85
|
+
state.env.OXY_EDGE_ACTIVITY_API_SECRET = 'rotated';
|
|
86
|
+
await observeEdgeRequest({...state,next:()=>new Response(null)}); await Promise.all(state.tasks);
|
|
87
|
+
expect(state.fetcher.mock.calls).toHaveLength(5);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('bounds an injected credential that never resolves without delaying the response', async () => {
|
|
91
|
+
jest.useFakeTimers();
|
|
92
|
+
try {
|
|
93
|
+
const state = setup(); const response = new Response(null);
|
|
94
|
+
expect(await observeEdgeRequest({...state,credential:()=>new Promise(()=>{}),next:()=>response})).toBe(response);
|
|
95
|
+
await jest.advanceTimersByTimeAsync(4_000);
|
|
96
|
+
await Promise.all(state.tasks);
|
|
97
|
+
expect(state.onError).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(state.fetcher).not.toHaveBeenCalled();
|
|
99
|
+
expect(jest.getTimerCount()).toBe(0);
|
|
100
|
+
} finally { jest.useRealTimers(); }
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('bounds token response size and never publishes credentials in diagnostics', async () => {
|
|
104
|
+
const state = setup();
|
|
105
|
+
const fetcher = jest.fn(async()=>new Response('secret'.repeat(4000)));
|
|
106
|
+
await observeEdgeRequest({...state,fetcher,next:()=>new Response(null)});
|
|
107
|
+
await Promise.all(state.tasks);
|
|
108
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(state.onError).toHaveBeenCalledWith('Edge activity publication failed');
|
|
110
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/** @jest-environment node */
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { observeTrafficSocket, socketTrafficType } from '../socket';
|
|
4
|
+
import type { TrafficFlow } from '../collector';
|
|
5
|
+
|
|
6
|
+
class TestSocket extends EventEmitter {
|
|
7
|
+
handshake = { headers: { 'cf-ray': 'anonymous-MAD', authorization: 'private' } };
|
|
8
|
+
nsp = { name: '/messages' };
|
|
9
|
+
incoming = new Set<(event: string) => void>();
|
|
10
|
+
outgoing = new Set<(event: string) => void>();
|
|
11
|
+
onAny(listener: (event: string) => void) { this.incoming.add(listener); }
|
|
12
|
+
onAnyOutgoing(listener: (event: string) => void) { this.outgoing.add(listener); }
|
|
13
|
+
offAny(listener: (event: string) => void) { this.incoming.delete(listener); }
|
|
14
|
+
offAnyOutgoing(listener: (event: string) => void) { this.outgoing.delete(listener); }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('socket traffic', () => {
|
|
18
|
+
it('records real application directions and strips event identifiers and payloads', () => {
|
|
19
|
+
const socket = new TestSocket();
|
|
20
|
+
const events: TrafficFlow[] = [];
|
|
21
|
+
observeTrafficSocket(socket, { record: event => events.push(event) }, 'syra', 'us-west-2');
|
|
22
|
+
socket.incoming.forEach(listener => listener('player:private-track-id'));
|
|
23
|
+
socket.outgoing.forEach(listener => listener('message:private-user-id'));
|
|
24
|
+
expect(events).toEqual([
|
|
25
|
+
expect.objectContaining({ scope: 'external', direction: 'inbound', sourceRegion: 'edge-mad', targetRegion: 'us-west-2', activityType: 'media' }),
|
|
26
|
+
expect.objectContaining({ scope: 'external', direction: 'outbound', sourceRegion: 'us-west-2', targetRegion: 'edge-mad', activityType: 'communication' }),
|
|
27
|
+
]);
|
|
28
|
+
expect(JSON.stringify(events)).not.toMatch(/private|authorization|track-id|user-id/);
|
|
29
|
+
socket.emit('disconnect');
|
|
30
|
+
expect(socket.incoming.size + socket.outgoing.size).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('uses only verified peer metadata for internal traffic and reads it at event time', () => {
|
|
34
|
+
const socket = new TestSocket();
|
|
35
|
+
const events: TrafficFlow[] = [];
|
|
36
|
+
let peer: { service: string; region: string } | undefined;
|
|
37
|
+
const stop = observeTrafficSocket(socket, { record: event => events.push(event) }, 'alia', 'us-west-2', { getPeer: () => peer });
|
|
38
|
+
peer = { service: 'mention', region: 'eu-west-1' };
|
|
39
|
+
socket.incoming.forEach(listener => listener('agent:update'));
|
|
40
|
+
expect(events[0]).toMatchObject({ scope: 'internal', sourceService: 'mention', sourceRegion: 'eu-west-1', activityType: 'ai' });
|
|
41
|
+
stop(); stop();
|
|
42
|
+
expect(socket.listenerCount('disconnect')).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('does not observe dashboard control namespaces or create events merely for connecting', () => {
|
|
46
|
+
const socket = new TestSocket();
|
|
47
|
+
const record = jest.fn();
|
|
48
|
+
socket.nsp.name = '/platform-activity';
|
|
49
|
+
observeTrafficSocket(socket, { record }, 'oxy-api', 'us-west-2');
|
|
50
|
+
expect(socket.incoming.size + socket.outgoing.size).toBe(0);
|
|
51
|
+
expect(record).not.toHaveBeenCalled();
|
|
52
|
+
expect(socketTrafficType('arbitrary-private-value')).toBe('platform');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('real Socket.IO transport', () => {
|
|
57
|
+
it('observes events and namespace broadcasts without inventing acknowledgement traffic', async () => {
|
|
58
|
+
const { Server } = await import('socket.io');
|
|
59
|
+
const { io } = await import('socket.io-client');
|
|
60
|
+
const { createServer } = await import('node:http');
|
|
61
|
+
const server = createServer();
|
|
62
|
+
const sockets = new Server(server);
|
|
63
|
+
const events: TrafficFlow[] = [];
|
|
64
|
+
sockets.on('connection', socket => {
|
|
65
|
+
observeTrafficSocket(socket, { record: flow => events.push(flow) }, 'syra', 'us-west-2');
|
|
66
|
+
socket.on('player:seek', (_position, acknowledge) => {
|
|
67
|
+
acknowledge('ok');
|
|
68
|
+
sockets.emit('player:state', { privateTrackId: 'never-published' });
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
72
|
+
const address = server.address();
|
|
73
|
+
if (!address || typeof address === 'string') throw new Error('Missing listener');
|
|
74
|
+
const client = io(`http://127.0.0.1:${address.port}`, { transports: ['websocket'], extraHeaders: { 'X-Oxy-Edge-Region': 'NRT' } });
|
|
75
|
+
try {
|
|
76
|
+
await new Promise<void>((resolve, reject) => { client.once('connect', resolve); client.once('connect_error', reject); });
|
|
77
|
+
const received = new Promise<void>(resolve => client.once('player:state', () => resolve()));
|
|
78
|
+
await client.emitWithAck('player:seek', 42);
|
|
79
|
+
await received;
|
|
80
|
+
expect(events).toHaveLength(2);
|
|
81
|
+
expect(events.map(flow => [flow.direction, flow.scope, flow.activityType])).toEqual([
|
|
82
|
+
['inbound', 'external', 'media'], ['outbound', 'external', 'media'],
|
|
83
|
+
]);
|
|
84
|
+
expect(events[0].sourceRegion).toBe('edge-nrt');
|
|
85
|
+
expect(events[1].targetRegion).toBe('edge-nrt');
|
|
86
|
+
expect(JSON.stringify(events)).not.toContain('never-published');
|
|
87
|
+
} finally {
|
|
88
|
+
client.disconnect();
|
|
89
|
+
await new Promise<void>(resolve => sockets.close(() => resolve()));
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('raw WebSocket transport', () => {
|
|
95
|
+
it('preserves send callbacks, synchronous errors and method cleanup without reading messages', async () => {
|
|
96
|
+
const { observeTrafficWebSocket } = await import('../socket');
|
|
97
|
+
const events: TrafficFlow[] = [];
|
|
98
|
+
const original = jest.fn((_data: unknown, callback: (error?: Error) => void) => callback());
|
|
99
|
+
const socket = Object.assign(new EventEmitter(), { readyState: 1, send: original });
|
|
100
|
+
const stop = observeTrafficWebSocket(socket, { record: flow => events.push(flow) }, 'alia', 'us-west-2', {
|
|
101
|
+
activityType: 'ai', getPeer: () => ({ service: 'alia-integrations', region: 'eu-west-1' }),
|
|
102
|
+
});
|
|
103
|
+
const callback = jest.fn();
|
|
104
|
+
socket.send({ privateBody: 'not-recorded' }, callback);
|
|
105
|
+
socket.emit('message', Buffer.from('private-message'));
|
|
106
|
+
expect(original).toHaveBeenCalledWith({ privateBody: 'not-recorded' }, callback);
|
|
107
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
108
|
+
expect(events.map(flow => flow.direction)).toEqual(['outbound', 'inbound']);
|
|
109
|
+
expect(events.every(flow => flow.scope === 'internal' && flow.activityType === 'ai')).toBe(true);
|
|
110
|
+
expect(JSON.stringify(events)).not.toMatch(/private|not-recorded/);
|
|
111
|
+
original.mockImplementationOnce(() => { throw new Error('send failed'); });
|
|
112
|
+
expect(() => socket.send('', callback)).toThrow('send failed');
|
|
113
|
+
expect(events).toHaveLength(2);
|
|
114
|
+
stop(); stop();
|
|
115
|
+
expect(socket.send).toBe(original);
|
|
116
|
+
expect(socket.listenerCount('message')).toBe(0);
|
|
117
|
+
});
|
|
118
|
+
});
|
package/src/browser.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { OXY_ACTIVITY_ID_HEADER, OXY_EDGE_REGION_HEADER } from './index.js';
|
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_ACTIVITY_ID_ROTATION_MS = 5 * 60 * 1_000;
|
|
4
4
|
export const DEFAULT_EDGE_TRACE_TIMEOUT_MS = 1_000;
|
|
5
|
+
export const DEFAULT_EDGE_CACHE_MS = 15_000;
|
|
5
6
|
|
|
6
7
|
interface TraceResponse {
|
|
7
8
|
ok: boolean;
|
|
@@ -11,6 +12,7 @@ interface TraceResponse {
|
|
|
11
12
|
export interface BrowserTelemetryOptions {
|
|
12
13
|
activityIdRotationMs?: number;
|
|
13
14
|
edgeTraceTimeoutMs?: number;
|
|
15
|
+
edgeCacheMs?: number;
|
|
14
16
|
now?: () => number;
|
|
15
17
|
crypto?: Pick<Crypto, 'getRandomValues' | 'randomUUID'>;
|
|
16
18
|
location?: Pick<Location, 'hostname' | 'protocol'>;
|
|
@@ -45,6 +47,7 @@ export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): B
|
|
|
45
47
|
let activityId: string | null = null;
|
|
46
48
|
let activityIdCreatedAt = 0;
|
|
47
49
|
let edgePopPromise: Promise<string | null> | null = null;
|
|
50
|
+
let edgePopReadAt = -Infinity;
|
|
48
51
|
|
|
49
52
|
const getActivityIdHeader = (): Record<string, string> => {
|
|
50
53
|
const hasInjectedBrowser = Boolean(options.location);
|
|
@@ -79,7 +82,11 @@ export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): B
|
|
|
79
82
|
};
|
|
80
83
|
|
|
81
84
|
const getEdgeRegionHeader = async (): Promise<Record<string, string>> => {
|
|
82
|
-
|
|
85
|
+
const now = (options.now ?? Date.now)();
|
|
86
|
+
if (!edgePopPromise || now < edgePopReadAt || now - edgePopReadAt >= (options.edgeCacheMs ?? DEFAULT_EDGE_CACHE_MS)) {
|
|
87
|
+
edgePopReadAt = now;
|
|
88
|
+
edgePopPromise = discoverEdgePop();
|
|
89
|
+
}
|
|
83
90
|
const edgePop = await edgePopPromise;
|
|
84
91
|
return edgePop ? { [OXY_EDGE_REGION_HEADER]: edgePop } : {};
|
|
85
92
|
};
|
package/src/collector.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { metadataFromHeaders, type TelemetryHeaders } from './server.js';
|
|
2
|
+
|
|
3
|
+
export type TrafficScope = 'internal' | 'external';
|
|
4
|
+
export type TrafficDirection = 'inbound' | 'outbound';
|
|
5
|
+
export type TrafficType = 'identity' | 'ai' | 'communication' | 'media' | 'platform';
|
|
6
|
+
export interface TrafficFlow {
|
|
7
|
+
region: string;
|
|
8
|
+
sourceRegion?: string;
|
|
9
|
+
targetRegion?: string;
|
|
10
|
+
sourceService?: string;
|
|
11
|
+
targetService?: string;
|
|
12
|
+
service: string;
|
|
13
|
+
scope: TrafficScope;
|
|
14
|
+
direction: TrafficDirection;
|
|
15
|
+
activityType: TrafficType;
|
|
16
|
+
}
|
|
17
|
+
export interface TrafficAggregate extends TrafficFlow {
|
|
18
|
+
requests: number;
|
|
19
|
+
windowStartedAt: string;
|
|
20
|
+
emittedAt: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TYPES: Record<string, TrafficType> = {
|
|
24
|
+
auth: 'identity', session: 'identity', users: 'identity', profiles: 'identity', accounts: 'identity',
|
|
25
|
+
ai: 'ai', inference: 'ai', models: 'ai', chat: 'ai', completions: 'ai',
|
|
26
|
+
stream: 'media', tracks: 'media', episodes: 'media', recordings: 'media', rooms: 'media',
|
|
27
|
+
messages: 'communication', notifications: 'communication', mail: 'communication',
|
|
28
|
+
media: 'media', files: 'media', assets: 'media', upload: 'media', uploads: 'media', cdn: 'media', storage: 'media', images: 'media', audio: 'media', videos: 'media',
|
|
29
|
+
};
|
|
30
|
+
export function trafficType(path: string): TrafficType {
|
|
31
|
+
const parts = path.split('?')[0].split('/').filter(Boolean);
|
|
32
|
+
if (parts[0] === 'alia') parts.shift();
|
|
33
|
+
if (parts[0] === 'api') parts.shift();
|
|
34
|
+
if (/^v\d+$/.test(parts[0] ?? '')) parts.shift();
|
|
35
|
+
return TYPES[parts[0] ?? ''] ?? 'platform';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createTrafficCollector(publish: (events: TrafficAggregate[]) => Promise<unknown>, now = Date.now) {
|
|
39
|
+
const pending = new Map<string, { flow: TrafficFlow; requests: number; startedAt: number }>();
|
|
40
|
+
let flushing: Promise<void> | null = null;
|
|
41
|
+
return {
|
|
42
|
+
record(flow: TrafficFlow) {
|
|
43
|
+
const key = JSON.stringify(flow);
|
|
44
|
+
const existing = pending.get(key);
|
|
45
|
+
if (existing) existing.requests++;
|
|
46
|
+
else if (pending.size < 512) pending.set(key, { flow, requests: 1, startedAt: now() });
|
|
47
|
+
},
|
|
48
|
+
async flush() {
|
|
49
|
+
while (flushing) await flushing;
|
|
50
|
+
if (pending.size === 0) return;
|
|
51
|
+
const emittedAt = new Date(now()).toISOString();
|
|
52
|
+
const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({ ...flow, requests, windowStartedAt: new Date(startedAt).toISOString(), emittedAt }));
|
|
53
|
+
pending.clear();
|
|
54
|
+
flushing = (async () => {
|
|
55
|
+
for (let offset = 0; offset < events.length; offset += 256) await publish(events.slice(offset, offset + 256));
|
|
56
|
+
})();
|
|
57
|
+
try { await flushing; } finally { flushing = null; }
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface HttpRequest {
|
|
63
|
+
headers: TelemetryHeaders;
|
|
64
|
+
path?: string;
|
|
65
|
+
url?: string;
|
|
66
|
+
serviceApp?: { appName?: string };
|
|
67
|
+
}
|
|
68
|
+
interface HttpResponse { once(event: string, listener: () => void): unknown; setHeader?(name: string, value: string): unknown }
|
|
69
|
+
|
|
70
|
+
export function trafficMiddleware(
|
|
71
|
+
collector: Pick<ReturnType<typeof createTrafficCollector>, 'record'>,
|
|
72
|
+
service: string,
|
|
73
|
+
region: string,
|
|
74
|
+
) {
|
|
75
|
+
return (request: HttpRequest, response: HttpResponse, next: () => void): void => {
|
|
76
|
+
response.setHeader?.('X-Oxy-Region', region);
|
|
77
|
+
const path = request.path ?? request.url ?? '/';
|
|
78
|
+
// Never report collection itself, probes or the dashboard transport.
|
|
79
|
+
if (/^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(path) || path.startsWith('/internal/activity') || path.startsWith('/auth/service-token') || path.startsWith('/cdn-cgi/')) { next(); return; }
|
|
80
|
+
const { edgePop } = metadataFromHeaders(request.headers);
|
|
81
|
+
const activityType = trafficType(path);
|
|
82
|
+
let finished = false;
|
|
83
|
+
const inboundFlow = (): TrafficFlow => {
|
|
84
|
+
// Read service identity AFTER the route's shared auth middleware resolved it.
|
|
85
|
+
const name = request.serviceApp?.appName?.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 40);
|
|
86
|
+
const internal = Boolean(name);
|
|
87
|
+
const forwardedRegion = request.headers['x-oxy-source-region'];
|
|
88
|
+
const peerRegion = internal ? normalizeInfrastructureRegion(forwardedRegion) : edgePop ? `edge-${edgePop}` : undefined;
|
|
89
|
+
return { region, service, activityType, scope: internal ? 'internal' : 'external', direction: 'inbound', sourceRegion: peerRegion, targetRegion: region, sourceService: name, targetService: service };
|
|
90
|
+
};
|
|
91
|
+
const finish = () => {
|
|
92
|
+
if (finished) return;
|
|
93
|
+
finished = true;
|
|
94
|
+
const flow = inboundFlow();
|
|
95
|
+
collector.record(flow);
|
|
96
|
+
collector.record({ ...flow, direction: 'outbound', sourceRegion: region, targetRegion: flow.sourceRegion, sourceService: service, targetService: flow.sourceService });
|
|
97
|
+
};
|
|
98
|
+
response.once('finish', finish);
|
|
99
|
+
// Failed/aborted requests are still real inbound activity. Do not invent a
|
|
100
|
+
// completed outbound response when the connection closes prematurely.
|
|
101
|
+
response.once('close', () => {
|
|
102
|
+
if (finished) return;
|
|
103
|
+
finished = true;
|
|
104
|
+
collector.record(inboundFlow());
|
|
105
|
+
});
|
|
106
|
+
next();
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface ServiceEndpoint { service: string; region: string }
|
|
111
|
+
export function instrumentTrafficFetch(
|
|
112
|
+
fetcher: typeof fetch,
|
|
113
|
+
collector: Pick<ReturnType<typeof createTrafficCollector>, 'record'>,
|
|
114
|
+
service: string,
|
|
115
|
+
region: string,
|
|
116
|
+
resolveEndpoint: (url: URL) => ServiceEndpoint | undefined,
|
|
117
|
+
): typeof fetch {
|
|
118
|
+
return Object.assign(async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
119
|
+
const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
|
|
120
|
+
// Collection/auth control traffic cannot generate a feedback loop.
|
|
121
|
+
if (url.pathname.startsWith('/internal/activity') || url.pathname.startsWith('/auth/service-token') || url.pathname === '/platform-infrastructure') return fetcher(input, init);
|
|
122
|
+
const peer = resolveEndpoint(url);
|
|
123
|
+
const flow: TrafficFlow = {
|
|
124
|
+
service, region, scope: peer ? 'internal' : 'external', direction: 'outbound',
|
|
125
|
+
activityType: trafficType(url.pathname), sourceRegion: region, targetRegion: peer?.region,
|
|
126
|
+
sourceService: service, targetService: peer?.service,
|
|
127
|
+
};
|
|
128
|
+
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
129
|
+
if (peer) headers.set('X-Oxy-Source-Region', region);
|
|
130
|
+
let response: Response;
|
|
131
|
+
try { response = await fetcher(input, { ...init, headers }); }
|
|
132
|
+
catch (error) { collector.record({ ...flow, targetRegion: undefined }); throw error; }
|
|
133
|
+
const targetRegion = peer ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined) : undefined;
|
|
134
|
+
collector.record({ ...flow, targetRegion });
|
|
135
|
+
collector.record({ ...flow, direction: 'inbound', sourceRegion: targetRegion, targetRegion: region, sourceService: peer?.service, targetService: service });
|
|
136
|
+
return response;
|
|
137
|
+
}, fetcher) as typeof fetch;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Exact deployed API hosts; an arbitrary caller-supplied hostname cannot claim
|
|
141
|
+
// to be an internal service. Region is infrastructure metadata, never IP geo.
|
|
142
|
+
const OXY_API_SERVICES: Record<string, string> = {
|
|
143
|
+
'api.oxy.so': 'oxy-api', 'api.website.oxy.so': 'website',
|
|
144
|
+
'website-api.oxy.so': 'website', 'api.mention.earth': 'mention',
|
|
145
|
+
'api.mercaria.co': 'mercaria', 'api.alia.onl': 'alia', 'kaana.ai': 'kaana',
|
|
146
|
+
'api.homiio.com': 'homiio', 'api.syra.fm': 'syra',
|
|
147
|
+
'mention.earth': 'mention', 'mcp.mention.earth': 'mention-mcp',
|
|
148
|
+
'api.clarity.surf': 'clarity', 'api.peable.to': 'peable',
|
|
149
|
+
'api.crowdsource.oxy.so': 'crowdsource', 'api.moovo.now': 'moovo',
|
|
150
|
+
'api.allo.you': 'allo', 'api.noted.oxy.so': 'noted', 'api.schedio.app': 'schedio', 'api.tnp.network': 'tnp-api',
|
|
151
|
+
};
|
|
152
|
+
export function resolveOxyServiceEndpoint(url: URL): ServiceEndpoint | undefined {
|
|
153
|
+
const service = OXY_API_SERVICES[url.hostname];
|
|
154
|
+
return url.protocol === 'https:' && service ? { service, region: 'us-west-2' } : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const REGION_LOCATIONS: Record<string, { label: string; coordinates: [number, number] }> = {
|
|
158
|
+
'us-west-2': { label: 'Oregon', coordinates: [-122.6765, 45.5231] },
|
|
159
|
+
'us-west-1': { label: 'Northern California', coordinates: [-121.89, 37.34] },
|
|
160
|
+
'us-east-1': { label: 'Northern Virginia', coordinates: [-77.49, 39.04] },
|
|
161
|
+
'us-east-2': { label: 'Ohio', coordinates: [-82.99, 39.96] },
|
|
162
|
+
'eu-west-1': { label: 'Ireland', coordinates: [-6.26, 53.35] },
|
|
163
|
+
'eu-west-2': { label: 'London', coordinates: [-0.1276, 51.5072] },
|
|
164
|
+
'eu-west-3': { label: 'Paris', coordinates: [2.3522, 48.8566] },
|
|
165
|
+
'eu-central-1': { label: 'Frankfurt', coordinates: [8.6821, 50.1109] },
|
|
166
|
+
'eu-central-2': { label: 'Zurich', coordinates: [8.54, 47.38] },
|
|
167
|
+
'eu-north-1': { label: 'Stockholm', coordinates: [18.07, 59.33] },
|
|
168
|
+
'eu-south-1': { label: 'Milan', coordinates: [9.19, 45.46] },
|
|
169
|
+
'eu-south-2': { label: 'Spain', coordinates: [-0.89, 41.65] },
|
|
170
|
+
'ap-northeast-1': { label: 'Tokyo', coordinates: [139.69, 35.69] },
|
|
171
|
+
'ap-northeast-2': { label: 'Seoul', coordinates: [126.98, 37.57] },
|
|
172
|
+
'ap-northeast-3': { label: 'Osaka', coordinates: [135.5, 34.69] },
|
|
173
|
+
'ap-southeast-1': { label: 'Singapore', coordinates: [103.82, 1.35] },
|
|
174
|
+
'ap-southeast-2': { label: 'Sydney', coordinates: [151.21, -33.87] },
|
|
175
|
+
'ap-southeast-3': { label: 'Jakarta', coordinates: [106.85, -6.21] },
|
|
176
|
+
'ap-southeast-4': { label: 'Melbourne', coordinates: [144.96, -37.81] },
|
|
177
|
+
'ap-south-1': { label: 'Mumbai', coordinates: [72.88, 19.08] },
|
|
178
|
+
'ap-south-2': { label: 'Hyderabad', coordinates: [78.49, 17.38] },
|
|
179
|
+
'ap-east-1': { label: 'Hong Kong', coordinates: [114.17, 22.32] },
|
|
180
|
+
'ca-central-1': { label: 'Canada', coordinates: [-73.57, 45.5] },
|
|
181
|
+
'ca-west-1': { label: 'Calgary', coordinates: [-114.07, 51.05] },
|
|
182
|
+
'sa-east-1': { label: 'São Paulo', coordinates: [-46.63, -23.55] },
|
|
183
|
+
'af-south-1': { label: 'Cape Town', coordinates: [18.42, -33.92] },
|
|
184
|
+
'me-south-1': { label: 'Bahrain', coordinates: [50.59, 26.22] },
|
|
185
|
+
'me-central-1': { label: 'UAE', coordinates: [54.37, 24.45] },
|
|
186
|
+
'il-central-1': { label: 'Tel Aviv', coordinates: [34.78, 32.09] },
|
|
187
|
+
};
|
|
188
|
+
export function infrastructureLocation(region: string) { return REGION_LOCATIONS[region]; }
|
|
189
|
+
|
|
190
|
+
export function normalizeInfrastructureRegion(value: unknown): string | undefined {
|
|
191
|
+
return typeof value === 'string' && /^[a-z]{2}(?:-[a-z]+)+-\d$/.test(value) ? value : undefined;
|
|
192
|
+
}
|