@oxy.so/telemetry 1.1.1 → 2.0.0
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/LICENSE +676 -0
- package/README.md +6 -3
- package/dist/cjs/browser.js +46 -8
- package/dist/cjs/collector.js +111 -26
- package/dist/cjs/server.js +3 -1
- package/dist/cjs/socket.js +38 -9
- package/dist/esm/browser.js +44 -7
- package/dist/esm/collector.js +111 -26
- package/dist/esm/server.js +3 -1
- package/dist/esm/socket.js +38 -9
- package/dist/types/browser.d.ts +14 -0
- package/package.json +5 -17
- package/src/__tests__/browser.test.ts +42 -4
- package/src/__tests__/collector.test.ts +132 -27
- package/src/__tests__/server.test.ts +7 -5
- package/src/__tests__/socket.test.ts +80 -29
- package/src/browser.ts +71 -10
- package/src/collector.ts +152 -35
- package/src/server.ts +8 -2
- package/src/socket.ts +43 -11
- package/dist/cjs/edge.js +0 -137
- package/dist/esm/edge.js +0 -134
- package/dist/types/edge.d.ts +0 -32
- package/src/__tests__/edge.test.ts +0 -110
- package/src/edge.ts +0 -117
|
@@ -1,48 +1,95 @@
|
|
|
1
1
|
/** @jest-environment node */
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
createTrafficCollector,
|
|
5
|
+
instrumentTrafficFetch,
|
|
6
|
+
trafficMiddleware,
|
|
7
|
+
trafficType,
|
|
8
|
+
type TrafficAggregate,
|
|
9
|
+
} from '../collector';
|
|
4
10
|
|
|
5
11
|
describe('ecosystem activity collector', () => {
|
|
6
12
|
it('reports all products independently of dashboard viewers and counts errors too', async () => {
|
|
7
13
|
const batches: TrafficAggregate[][] = [];
|
|
8
|
-
const collector = createTrafficCollector(async events => {
|
|
14
|
+
const collector = createTrafficCollector(async (events) => {
|
|
15
|
+
batches.push(events);
|
|
16
|
+
});
|
|
9
17
|
for (const service of ['website', 'mention', 'mercaria', 'alia', 'kaana']) {
|
|
10
18
|
const response = new EventEmitter();
|
|
11
|
-
trafficMiddleware(collector, service, 'us-west-2')(
|
|
19
|
+
trafficMiddleware(collector, service, 'us-west-2')(
|
|
20
|
+
{
|
|
21
|
+
path: '/api/files/private-id',
|
|
22
|
+
headers: { 'cf-ray': 'example-NRT', authorization: 'secret' },
|
|
23
|
+
},
|
|
24
|
+
response,
|
|
25
|
+
() => {},
|
|
26
|
+
);
|
|
12
27
|
response.emit('finish');
|
|
13
28
|
response.emit('close');
|
|
14
29
|
}
|
|
15
30
|
await collector.flush();
|
|
16
31
|
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);
|
|
32
|
+
expect(batches[0].filter((event) => event.direction === 'inbound')).toHaveLength(5);
|
|
33
|
+
expect(batches[0].every((event) => event.activityType === 'media')).toBe(true);
|
|
19
34
|
expect(JSON.stringify(batches)).not.toMatch(/private-id|secret|authorization/);
|
|
20
35
|
});
|
|
21
36
|
|
|
22
37
|
it('keeps scope, direction and category independent for co-located internal services', async () => {
|
|
23
38
|
const batches: TrafficAggregate[][] = [];
|
|
24
|
-
const collector = createTrafficCollector(async events => {
|
|
39
|
+
const collector = createTrafficCollector(async (events) => {
|
|
40
|
+
batches.push(events);
|
|
41
|
+
});
|
|
25
42
|
const response = new EventEmitter();
|
|
26
|
-
const request = {
|
|
27
|
-
|
|
43
|
+
const request = {
|
|
44
|
+
path: '/v1/chat/completions',
|
|
45
|
+
headers: { 'x-oxy-source-region': 'us-west-2' },
|
|
46
|
+
serviceApp: undefined as undefined | { appName: string },
|
|
47
|
+
};
|
|
48
|
+
trafficMiddleware(collector, 'kaana', 'us-west-2')(request, response, () => {
|
|
49
|
+
request.serviceApp = { appName: 'oxy-api' };
|
|
50
|
+
});
|
|
28
51
|
response.emit('finish');
|
|
29
52
|
await collector.flush();
|
|
30
53
|
expect(batches[0]).toEqual([
|
|
31
|
-
expect.objectContaining({
|
|
32
|
-
|
|
54
|
+
expect.objectContaining({
|
|
55
|
+
sourceRegion: 'us-west-2',
|
|
56
|
+
targetRegion: 'us-west-2',
|
|
57
|
+
sourceService: 'oxy-api',
|
|
58
|
+
targetService: 'kaana',
|
|
59
|
+
direction: 'inbound',
|
|
60
|
+
scope: 'internal',
|
|
61
|
+
activityType: 'ai',
|
|
62
|
+
}),
|
|
63
|
+
expect.objectContaining({
|
|
64
|
+
sourceService: 'kaana',
|
|
65
|
+
targetService: 'oxy-api',
|
|
66
|
+
direction: 'outbound',
|
|
67
|
+
scope: 'internal',
|
|
68
|
+
activityType: 'ai',
|
|
69
|
+
}),
|
|
33
70
|
]);
|
|
34
71
|
});
|
|
35
72
|
|
|
36
73
|
it('does not fabricate a response on abort and excludes telemetry control traffic', async () => {
|
|
37
74
|
const batches: TrafficAggregate[][] = [];
|
|
38
|
-
const collector = createTrafficCollector(async events => {
|
|
75
|
+
const collector = createTrafficCollector(async (events) => {
|
|
76
|
+
batches.push(events);
|
|
77
|
+
});
|
|
39
78
|
for (const path of ['/health', '/api/ready', '/platform-stats/stream', '/internal/activity']) {
|
|
40
79
|
const response = new EventEmitter();
|
|
41
|
-
trafficMiddleware(collector, 'website', 'us-west-2')(
|
|
80
|
+
trafficMiddleware(collector, 'website', 'us-west-2')(
|
|
81
|
+
{ path, headers: {} },
|
|
82
|
+
response,
|
|
83
|
+
() => {},
|
|
84
|
+
);
|
|
42
85
|
response.emit('finish');
|
|
43
86
|
}
|
|
44
87
|
const aborted = new EventEmitter();
|
|
45
|
-
trafficMiddleware(collector, 'mention', 'us-west-2')(
|
|
88
|
+
trafficMiddleware(collector, 'mention', 'us-west-2')(
|
|
89
|
+
{ path: '/messages', headers: {} },
|
|
90
|
+
aborted,
|
|
91
|
+
() => {},
|
|
92
|
+
);
|
|
46
93
|
aborted.emit('close');
|
|
47
94
|
await collector.flush();
|
|
48
95
|
expect(batches[0]).toHaveLength(1);
|
|
@@ -51,41 +98,99 @@ describe('ecosystem activity collector', () => {
|
|
|
51
98
|
|
|
52
99
|
it('tracks real outgoing calls and only reports the response when one arrived', async () => {
|
|
53
100
|
const batches: TrafficAggregate[][] = [];
|
|
54
|
-
const collector = createTrafficCollector(async events => {
|
|
55
|
-
|
|
101
|
+
const collector = createTrafficCollector(async (events) => {
|
|
102
|
+
batches.push(events);
|
|
103
|
+
});
|
|
104
|
+
const fetcher = instrumentTrafficFetch(
|
|
105
|
+
jest.fn(
|
|
106
|
+
async () => new Response('ok', { headers: { 'X-Oxy-Region': 'eu-central-1' } }),
|
|
107
|
+
) as unknown as typeof fetch,
|
|
108
|
+
collector,
|
|
109
|
+
'alia',
|
|
110
|
+
'us-west-2',
|
|
111
|
+
(url) =>
|
|
112
|
+
url.hostname === 'api.oxy.so' ? { service: 'oxy-api', region: 'us-west-2' } : undefined,
|
|
113
|
+
);
|
|
56
114
|
await fetcher('https://api.oxy.so/v1/chat/completions');
|
|
57
115
|
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);
|
|
116
|
+
expect(batches[0].map((event) => event.direction)).toEqual(['outbound', 'inbound']);
|
|
117
|
+
expect(batches[0].every((event) => event.scope === 'internal')).toBe(true);
|
|
60
118
|
expect(batches[0][0].targetRegion).toBe('eu-central-1');
|
|
61
119
|
expect(batches[0][1].sourceRegion).toBe('eu-central-1');
|
|
62
|
-
const failed = instrumentTrafficFetch(
|
|
120
|
+
const failed = instrumentTrafficFetch(
|
|
121
|
+
jest.fn(async () => {
|
|
122
|
+
throw new Error('offline');
|
|
123
|
+
}) as unknown as typeof fetch,
|
|
124
|
+
collector,
|
|
125
|
+
'alia',
|
|
126
|
+
'us-west-2',
|
|
127
|
+
() => undefined,
|
|
128
|
+
);
|
|
63
129
|
await expect(failed('https://example.com/files')).rejects.toThrow('offline');
|
|
64
130
|
await collector.flush();
|
|
65
131
|
expect(batches[1]).toHaveLength(1);
|
|
66
|
-
expect(batches[1][0]).toMatchObject({
|
|
132
|
+
expect(batches[1][0]).toMatchObject({
|
|
133
|
+
scope: 'external',
|
|
134
|
+
direction: 'outbound',
|
|
135
|
+
activityType: 'media',
|
|
136
|
+
});
|
|
67
137
|
});
|
|
68
138
|
|
|
69
139
|
it('chunks a busy fleet into ingestible batches and preserves fetch extensions', async () => {
|
|
70
140
|
const batches: TrafficAggregate[][] = [];
|
|
71
|
-
const collector = createTrafficCollector(async events => {
|
|
72
|
-
|
|
141
|
+
const collector = createTrafficCollector(async (events) => {
|
|
142
|
+
batches.push(events);
|
|
143
|
+
});
|
|
144
|
+
for (let index = 0; index < 300; index++)
|
|
145
|
+
collector.record({
|
|
146
|
+
region: 'us-west-2',
|
|
147
|
+
service: `service-${index}`,
|
|
148
|
+
scope: 'internal',
|
|
149
|
+
direction: 'inbound',
|
|
150
|
+
activityType: 'platform',
|
|
151
|
+
});
|
|
73
152
|
await collector.flush();
|
|
74
|
-
expect(batches.map(batch => batch.length)).toEqual([256, 44]);
|
|
153
|
+
expect(batches.map((batch) => batch.length)).toEqual([256, 44]);
|
|
75
154
|
const preconnect = jest.fn();
|
|
76
|
-
const baseFetch = Object.assign(
|
|
77
|
-
|
|
155
|
+
const baseFetch = Object.assign(
|
|
156
|
+
jest.fn(async () => new Response()),
|
|
157
|
+
{ preconnect },
|
|
158
|
+
) as unknown as typeof fetch;
|
|
159
|
+
const wrapped = instrumentTrafficFetch(
|
|
160
|
+
baseFetch,
|
|
161
|
+
collector,
|
|
162
|
+
'website',
|
|
163
|
+
'us-west-2',
|
|
164
|
+
() => undefined,
|
|
165
|
+
);
|
|
78
166
|
expect((wrapped as typeof baseFetch & { preconnect: unknown }).preconnect).toBe(preconnect);
|
|
79
167
|
});
|
|
80
168
|
|
|
81
169
|
it('keeps verified internal attribution when a request aborts', async () => {
|
|
82
170
|
const batches: TrafficAggregate[][] = [];
|
|
83
|
-
const collector = createTrafficCollector(async events => {
|
|
171
|
+
const collector = createTrafficCollector(async (events) => {
|
|
172
|
+
batches.push(events);
|
|
173
|
+
});
|
|
84
174
|
const response = new EventEmitter();
|
|
85
|
-
trafficMiddleware(collector, 'oxy-api', 'us-west-2')(
|
|
175
|
+
trafficMiddleware(collector, 'oxy-api', 'us-west-2')(
|
|
176
|
+
{
|
|
177
|
+
path: '/messages',
|
|
178
|
+
headers: { 'x-oxy-source-region': 'eu-west-1' },
|
|
179
|
+
serviceApp: { appName: 'mention' },
|
|
180
|
+
},
|
|
181
|
+
response,
|
|
182
|
+
() => {},
|
|
183
|
+
);
|
|
86
184
|
response.emit('close');
|
|
87
185
|
await collector.flush();
|
|
88
|
-
expect(batches[0]).toEqual([
|
|
186
|
+
expect(batches[0]).toEqual([
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
scope: 'internal',
|
|
189
|
+
direction: 'inbound',
|
|
190
|
+
sourceRegion: 'eu-west-1',
|
|
191
|
+
sourceService: 'mention',
|
|
192
|
+
}),
|
|
193
|
+
]);
|
|
89
194
|
});
|
|
90
195
|
|
|
91
196
|
it('classifies versioned media and inference endpoints without retaining the path', () => {
|
|
@@ -10,11 +10,13 @@ import {
|
|
|
10
10
|
|
|
11
11
|
describe('server telemetry primitives', () => {
|
|
12
12
|
it('prefers a validated forwarded PoP and accepts Cloudflare ray fallback', () => {
|
|
13
|
-
expect(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
expect(
|
|
14
|
+
metadataFromHeaders({
|
|
15
|
+
'x-oxy-edge-region': 'MAD',
|
|
16
|
+
'cf-ray': 'deadbeef-CDG',
|
|
17
|
+
'x-oxy-activity-id': 'anonymous-runtime-a1',
|
|
18
|
+
}),
|
|
19
|
+
).toEqual({ edgePop: 'mad', activityId: 'anonymous-runtime-a1' });
|
|
18
20
|
expect(metadataFromHeaders({ 'cf-ray': 'deadbeef-CDG' })).toEqual({ edgePop: 'cdg' });
|
|
19
21
|
});
|
|
20
22
|
|
|
@@ -8,22 +8,42 @@ class TestSocket extends EventEmitter {
|
|
|
8
8
|
nsp = { name: '/messages' };
|
|
9
9
|
incoming = new Set<(event: string) => void>();
|
|
10
10
|
outgoing = new Set<(event: string) => void>();
|
|
11
|
-
onAny(listener: (event: string) => void) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
onAny(listener: (event: string) => void) {
|
|
12
|
+
this.incoming.add(listener);
|
|
13
|
+
}
|
|
14
|
+
onAnyOutgoing(listener: (event: string) => void) {
|
|
15
|
+
this.outgoing.add(listener);
|
|
16
|
+
}
|
|
17
|
+
offAny(listener: (event: string) => void) {
|
|
18
|
+
this.incoming.delete(listener);
|
|
19
|
+
}
|
|
20
|
+
offAnyOutgoing(listener: (event: string) => void) {
|
|
21
|
+
this.outgoing.delete(listener);
|
|
22
|
+
}
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
describe('socket traffic', () => {
|
|
18
26
|
it('records real application directions and strips event identifiers and payloads', () => {
|
|
19
27
|
const socket = new TestSocket();
|
|
20
28
|
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'));
|
|
29
|
+
observeTrafficSocket(socket, { record: (event) => events.push(event) }, 'syra', 'us-west-2');
|
|
30
|
+
socket.incoming.forEach((listener) => listener('player:private-track-id'));
|
|
31
|
+
socket.outgoing.forEach((listener) => listener('message:private-user-id'));
|
|
24
32
|
expect(events).toEqual([
|
|
25
|
-
expect.objectContaining({
|
|
26
|
-
|
|
33
|
+
expect.objectContaining({
|
|
34
|
+
scope: 'external',
|
|
35
|
+
direction: 'inbound',
|
|
36
|
+
sourceRegion: 'edge-mad',
|
|
37
|
+
targetRegion: 'us-west-2',
|
|
38
|
+
activityType: 'media',
|
|
39
|
+
}),
|
|
40
|
+
expect.objectContaining({
|
|
41
|
+
scope: 'external',
|
|
42
|
+
direction: 'outbound',
|
|
43
|
+
sourceRegion: 'us-west-2',
|
|
44
|
+
targetRegion: 'edge-mad',
|
|
45
|
+
activityType: 'communication',
|
|
46
|
+
}),
|
|
27
47
|
]);
|
|
28
48
|
expect(JSON.stringify(events)).not.toMatch(/private|authorization|track-id|user-id/);
|
|
29
49
|
socket.emit('disconnect');
|
|
@@ -34,11 +54,23 @@ describe('socket traffic', () => {
|
|
|
34
54
|
const socket = new TestSocket();
|
|
35
55
|
const events: TrafficFlow[] = [];
|
|
36
56
|
let peer: { service: string; region: string } | undefined;
|
|
37
|
-
const stop = observeTrafficSocket(
|
|
57
|
+
const stop = observeTrafficSocket(
|
|
58
|
+
socket,
|
|
59
|
+
{ record: (event) => events.push(event) },
|
|
60
|
+
'alia',
|
|
61
|
+
'us-west-2',
|
|
62
|
+
{ getPeer: () => peer },
|
|
63
|
+
);
|
|
38
64
|
peer = { service: 'mention', region: 'eu-west-1' };
|
|
39
|
-
socket.incoming.forEach(listener => listener('agent:update'));
|
|
40
|
-
expect(events[0]).toMatchObject({
|
|
41
|
-
|
|
65
|
+
socket.incoming.forEach((listener) => listener('agent:update'));
|
|
66
|
+
expect(events[0]).toMatchObject({
|
|
67
|
+
scope: 'internal',
|
|
68
|
+
sourceService: 'mention',
|
|
69
|
+
sourceRegion: 'eu-west-1',
|
|
70
|
+
activityType: 'ai',
|
|
71
|
+
});
|
|
72
|
+
stop();
|
|
73
|
+
stop();
|
|
42
74
|
expect(socket.listenerCount('disconnect')).toBe(0);
|
|
43
75
|
});
|
|
44
76
|
|
|
@@ -61,32 +93,39 @@ describe('real Socket.IO transport', () => {
|
|
|
61
93
|
const server = createServer();
|
|
62
94
|
const sockets = new Server(server);
|
|
63
95
|
const events: TrafficFlow[] = [];
|
|
64
|
-
sockets.on('connection', socket => {
|
|
65
|
-
observeTrafficSocket(socket, { record: flow => events.push(flow) }, 'syra', 'us-west-2');
|
|
96
|
+
sockets.on('connection', (socket) => {
|
|
97
|
+
observeTrafficSocket(socket, { record: (flow) => events.push(flow) }, 'syra', 'us-west-2');
|
|
66
98
|
socket.on('player:seek', (_position, acknowledge) => {
|
|
67
99
|
acknowledge('ok');
|
|
68
100
|
sockets.emit('player:state', { privateTrackId: 'never-published' });
|
|
69
101
|
});
|
|
70
102
|
});
|
|
71
|
-
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
|
103
|
+
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
72
104
|
const address = server.address();
|
|
73
105
|
if (!address || typeof address === 'string') throw new Error('Missing listener');
|
|
74
|
-
const client = io(`http://127.0.0.1:${address.port}`, {
|
|
106
|
+
const client = io(`http://127.0.0.1:${address.port}`, {
|
|
107
|
+
transports: ['websocket'],
|
|
108
|
+
extraHeaders: { 'X-Oxy-Edge-Region': 'NRT' },
|
|
109
|
+
});
|
|
75
110
|
try {
|
|
76
|
-
await new Promise<void>((resolve, reject) => {
|
|
77
|
-
|
|
111
|
+
await new Promise<void>((resolve, reject) => {
|
|
112
|
+
client.once('connect', resolve);
|
|
113
|
+
client.once('connect_error', reject);
|
|
114
|
+
});
|
|
115
|
+
const received = new Promise<void>((resolve) => client.once('player:state', () => resolve()));
|
|
78
116
|
await client.emitWithAck('player:seek', 42);
|
|
79
117
|
await received;
|
|
80
118
|
expect(events).toHaveLength(2);
|
|
81
|
-
expect(events.map(flow => [flow.direction, flow.scope, flow.activityType])).toEqual([
|
|
82
|
-
['inbound', 'external', 'media'],
|
|
119
|
+
expect(events.map((flow) => [flow.direction, flow.scope, flow.activityType])).toEqual([
|
|
120
|
+
['inbound', 'external', 'media'],
|
|
121
|
+
['outbound', 'external', 'media'],
|
|
83
122
|
]);
|
|
84
123
|
expect(events[0].sourceRegion).toBe('edge-nrt');
|
|
85
124
|
expect(events[1].targetRegion).toBe('edge-nrt');
|
|
86
125
|
expect(JSON.stringify(events)).not.toContain('never-published');
|
|
87
126
|
} finally {
|
|
88
127
|
client.disconnect();
|
|
89
|
-
await new Promise<void>(resolve => sockets.close(() => resolve()));
|
|
128
|
+
await new Promise<void>((resolve) => sockets.close(() => resolve()));
|
|
90
129
|
}
|
|
91
130
|
});
|
|
92
131
|
});
|
|
@@ -97,21 +136,33 @@ describe('raw WebSocket transport', () => {
|
|
|
97
136
|
const events: TrafficFlow[] = [];
|
|
98
137
|
const original = jest.fn((_data: unknown, callback: (error?: Error) => void) => callback());
|
|
99
138
|
const socket = Object.assign(new EventEmitter(), { readyState: 1, send: original });
|
|
100
|
-
const stop = observeTrafficWebSocket(
|
|
101
|
-
|
|
102
|
-
|
|
139
|
+
const stop = observeTrafficWebSocket(
|
|
140
|
+
socket,
|
|
141
|
+
{ record: (flow) => events.push(flow) },
|
|
142
|
+
'alia',
|
|
143
|
+
'us-west-2',
|
|
144
|
+
{
|
|
145
|
+
activityType: 'ai',
|
|
146
|
+
getPeer: () => ({ service: 'alia-integrations', region: 'eu-west-1' }),
|
|
147
|
+
},
|
|
148
|
+
);
|
|
103
149
|
const callback = jest.fn();
|
|
104
150
|
socket.send({ privateBody: 'not-recorded' }, callback);
|
|
105
151
|
socket.emit('message', Buffer.from('private-message'));
|
|
106
152
|
expect(original).toHaveBeenCalledWith({ privateBody: 'not-recorded' }, callback);
|
|
107
153
|
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(
|
|
154
|
+
expect(events.map((flow) => flow.direction)).toEqual(['outbound', 'inbound']);
|
|
155
|
+
expect(events.every((flow) => flow.scope === 'internal' && flow.activityType === 'ai')).toBe(
|
|
156
|
+
true,
|
|
157
|
+
);
|
|
110
158
|
expect(JSON.stringify(events)).not.toMatch(/private|not-recorded/);
|
|
111
|
-
original.mockImplementationOnce(() => {
|
|
159
|
+
original.mockImplementationOnce(() => {
|
|
160
|
+
throw new Error('send failed');
|
|
161
|
+
});
|
|
112
162
|
expect(() => socket.send('', callback)).toThrow('send failed');
|
|
113
163
|
expect(events).toHaveLength(2);
|
|
114
|
-
stop();
|
|
164
|
+
stop();
|
|
165
|
+
stop();
|
|
115
166
|
expect(socket.send).toBe(original);
|
|
116
167
|
expect(socket.listenerCount('message')).toBe(0);
|
|
117
168
|
});
|
package/src/browser.ts
CHANGED
|
@@ -3,6 +3,12 @@ import { OXY_ACTIVITY_ID_HEADER, OXY_EDGE_REGION_HEADER } from './index.js';
|
|
|
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
5
|
export const DEFAULT_EDGE_CACHE_MS = 15_000;
|
|
6
|
+
/**
|
|
7
|
+
* How long `peekEdgeRegionHeader` trusts a known PoP before refreshing it in the
|
|
8
|
+
* background. A client's serving PoP rarely changes within minutes, and the
|
|
9
|
+
* peek never blocks, so this only bounds the trace traffic.
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_EDGE_PEEK_REFRESH_MS = 10 * 60_000;
|
|
6
12
|
|
|
7
13
|
interface TraceResponse {
|
|
8
14
|
ok: boolean;
|
|
@@ -13,6 +19,7 @@ export interface BrowserTelemetryOptions {
|
|
|
13
19
|
activityIdRotationMs?: number;
|
|
14
20
|
edgeTraceTimeoutMs?: number;
|
|
15
21
|
edgeCacheMs?: number;
|
|
22
|
+
edgePeekRefreshMs?: number;
|
|
16
23
|
now?: () => number;
|
|
17
24
|
crypto?: Pick<Crypto, 'getRandomValues' | 'randomUUID'>;
|
|
18
25
|
location?: Pick<Location, 'hostname' | 'protocol'>;
|
|
@@ -22,10 +29,18 @@ export interface BrowserTelemetryOptions {
|
|
|
22
29
|
export interface BrowserTelemetry {
|
|
23
30
|
getActivityIdHeader(): Record<string, string>;
|
|
24
31
|
getEdgeRegionHeader(): Promise<Record<string, string>>;
|
|
32
|
+
/**
|
|
33
|
+
* The last known edge-region header, synchronously, never waiting on the
|
|
34
|
+
* trace: `{}` until the first discovery lands. A stale or missing value
|
|
35
|
+
* starts a refresh in the background.
|
|
36
|
+
*/
|
|
37
|
+
peekEdgeRegionHeader(): Record<string, string>;
|
|
25
38
|
getHeaders(): Promise<Record<string, string>>;
|
|
26
39
|
}
|
|
27
40
|
|
|
28
|
-
function createUuid(
|
|
41
|
+
function createUuid(
|
|
42
|
+
cryptoApi: Pick<Crypto, 'getRandomValues' | 'randomUUID'> | undefined,
|
|
43
|
+
): string | null {
|
|
29
44
|
if (!cryptoApi) return null;
|
|
30
45
|
if (typeof cryptoApi.randomUUID === 'function') return cryptoApi.randomUUID();
|
|
31
46
|
|
|
@@ -38,9 +53,11 @@ function createUuid(cryptoApi: Pick<Crypto, 'getRandomValues' | 'randomUUID'> |
|
|
|
38
53
|
|
|
39
54
|
function eligibleLocation(location: Pick<Location, 'hostname' | 'protocol'> | undefined): boolean {
|
|
40
55
|
if (!location || (location.protocol !== 'https:' && location.protocol !== 'http:')) return false;
|
|
41
|
-
return
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
return (
|
|
57
|
+
location.hostname !== 'localhost' &&
|
|
58
|
+
location.hostname !== '127.0.0.1' &&
|
|
59
|
+
location.hostname !== '[::1]'
|
|
60
|
+
);
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): BrowserTelemetry {
|
|
@@ -54,7 +71,11 @@ export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): B
|
|
|
54
71
|
const hasBrowserRuntime = typeof window !== 'undefined' && typeof document !== 'undefined';
|
|
55
72
|
if (!hasInjectedBrowser && !hasBrowserRuntime) return {};
|
|
56
73
|
const now = (options.now ?? Date.now)();
|
|
57
|
-
if (
|
|
74
|
+
if (
|
|
75
|
+
!activityId ||
|
|
76
|
+
now < activityIdCreatedAt ||
|
|
77
|
+
now - activityIdCreatedAt >= (options.activityIdRotationMs ?? DEFAULT_ACTIVITY_ID_ROTATION_MS)
|
|
78
|
+
) {
|
|
58
79
|
activityId = createUuid(options.crypto ?? globalThis.crypto);
|
|
59
80
|
if (!activityId) return {};
|
|
60
81
|
activityIdCreatedAt = now;
|
|
@@ -63,14 +84,22 @@ export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): B
|
|
|
63
84
|
};
|
|
64
85
|
|
|
65
86
|
const discoverEdgePop = async (): Promise<string | null> => {
|
|
66
|
-
const browserLocation =
|
|
87
|
+
const browserLocation =
|
|
88
|
+
options.location ?? (typeof window === 'undefined' ? undefined : window.location);
|
|
67
89
|
if (!eligibleLocation(browserLocation)) return null;
|
|
68
90
|
const controller = new AbortController();
|
|
69
|
-
const timeout = setTimeout(
|
|
91
|
+
const timeout = setTimeout(
|
|
92
|
+
() => controller.abort(),
|
|
93
|
+
options.edgeTraceTimeoutMs ?? DEFAULT_EDGE_TRACE_TIMEOUT_MS,
|
|
94
|
+
);
|
|
70
95
|
try {
|
|
71
96
|
const response = await (options.fetchTrace
|
|
72
97
|
? options.fetchTrace(controller.signal)
|
|
73
|
-
: fetch('/cdn-cgi/trace', {
|
|
98
|
+
: fetch('/cdn-cgi/trace', {
|
|
99
|
+
cache: 'no-store',
|
|
100
|
+
credentials: 'omit',
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
}));
|
|
74
103
|
if (!response.ok) return null;
|
|
75
104
|
const match = (await response.text()).match(/^colo=([a-z]{3})$/im);
|
|
76
105
|
return match ? match[1].toLowerCase() : null;
|
|
@@ -83,7 +112,11 @@ export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): B
|
|
|
83
112
|
|
|
84
113
|
const getEdgeRegionHeader = async (): Promise<Record<string, string>> => {
|
|
85
114
|
const now = (options.now ?? Date.now)();
|
|
86
|
-
if (
|
|
115
|
+
if (
|
|
116
|
+
!edgePopPromise ||
|
|
117
|
+
now < edgePopReadAt ||
|
|
118
|
+
now - edgePopReadAt >= (options.edgeCacheMs ?? DEFAULT_EDGE_CACHE_MS)
|
|
119
|
+
) {
|
|
87
120
|
edgePopReadAt = now;
|
|
88
121
|
edgePopPromise = discoverEdgePop();
|
|
89
122
|
}
|
|
@@ -91,11 +124,36 @@ export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): B
|
|
|
91
124
|
return edgePop ? { [OXY_EDGE_REGION_HEADER]: edgePop } : {};
|
|
92
125
|
};
|
|
93
126
|
|
|
127
|
+
let peekedPop: string | null = null;
|
|
128
|
+
let peekRefreshAt = -Infinity;
|
|
129
|
+
let peekInFlight = false;
|
|
130
|
+
|
|
131
|
+
const peekEdgeRegionHeader = (): Record<string, string> => {
|
|
132
|
+
const now = (options.now ?? Date.now)();
|
|
133
|
+
if (
|
|
134
|
+
!peekInFlight &&
|
|
135
|
+
(now < peekRefreshAt ||
|
|
136
|
+
now - peekRefreshAt >= (options.edgePeekRefreshMs ?? DEFAULT_EDGE_PEEK_REFRESH_MS))
|
|
137
|
+
) {
|
|
138
|
+
peekRefreshAt = now;
|
|
139
|
+
peekInFlight = true;
|
|
140
|
+
void discoverEdgePop()
|
|
141
|
+
.then((pop) => {
|
|
142
|
+
if (pop) peekedPop = pop;
|
|
143
|
+
})
|
|
144
|
+
.finally(() => {
|
|
145
|
+
peekInFlight = false;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return peekedPop ? { [OXY_EDGE_REGION_HEADER]: peekedPop } : {};
|
|
149
|
+
};
|
|
150
|
+
|
|
94
151
|
return {
|
|
95
152
|
getActivityIdHeader,
|
|
96
153
|
getEdgeRegionHeader,
|
|
154
|
+
peekEdgeRegionHeader,
|
|
97
155
|
async getHeaders(): Promise<Record<string, string>> {
|
|
98
|
-
return { ...await getEdgeRegionHeader(), ...getActivityIdHeader() };
|
|
156
|
+
return { ...(await getEdgeRegionHeader()), ...getActivityIdHeader() };
|
|
99
157
|
},
|
|
100
158
|
};
|
|
101
159
|
}
|
|
@@ -108,5 +166,8 @@ export const getBrowserActivityIdHeader = (): Record<string, string> =>
|
|
|
108
166
|
export const getBrowserEdgeRegionHeader = (): Promise<Record<string, string>> =>
|
|
109
167
|
defaultBrowserTelemetry.getEdgeRegionHeader();
|
|
110
168
|
|
|
169
|
+
export const peekBrowserEdgeRegionHeader = (): Record<string, string> =>
|
|
170
|
+
defaultBrowserTelemetry.peekEdgeRegionHeader();
|
|
171
|
+
|
|
111
172
|
export const getBrowserTelemetryHeaders = (): Promise<Record<string, string>> =>
|
|
112
173
|
defaultBrowserTelemetry.getHeaders();
|