@oxy.so/telemetry 1.2.0 → 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/dist/cjs/browser.js +18 -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 +18 -8
- package/dist/esm/collector.js +111 -26
- package/dist/esm/server.js +3 -1
- package/dist/esm/socket.js +38 -9
- package/package.json +2 -16
- package/src/__tests__/browser.test.ts +22 -6
- 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 +35 -11
- 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
package/dist/esm/socket.js
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import { normalizeInfrastructureRegion } from './collector.js';
|
|
2
2
|
import { metadataFromHeaders } from './server.js';
|
|
3
3
|
const CATEGORIES = {
|
|
4
|
-
auth: 'identity',
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
auth: 'identity',
|
|
5
|
+
session: 'identity',
|
|
6
|
+
user: 'identity',
|
|
7
|
+
profile: 'identity',
|
|
8
|
+
chat: 'ai',
|
|
9
|
+
agent: 'ai',
|
|
10
|
+
inference: 'ai',
|
|
11
|
+
completion: 'ai',
|
|
12
|
+
message: 'communication',
|
|
13
|
+
notification: 'communication',
|
|
14
|
+
typing: 'communication',
|
|
15
|
+
media: 'media',
|
|
16
|
+
audio: 'media',
|
|
17
|
+
video: 'media',
|
|
18
|
+
stream: 'media',
|
|
19
|
+
player: 'media',
|
|
20
|
+
track: 'media',
|
|
21
|
+
playback: 'media',
|
|
22
|
+
playlist: 'media',
|
|
23
|
+
room: 'media',
|
|
9
24
|
};
|
|
10
25
|
/** Event names are reduced locally to five categories; payloads are never read. */
|
|
11
26
|
export function socketTrafficType(event) {
|
|
@@ -27,9 +42,16 @@ export function observeTrafficSocket(socket, collector, service, region, options
|
|
|
27
42
|
const peer = options.getPeer?.();
|
|
28
43
|
const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
|
|
29
44
|
const edgePop = metadataFromHeaders(socket.handshake.headers).edgePop;
|
|
30
|
-
const peerRegion = peerService
|
|
45
|
+
const peerRegion = peerService
|
|
46
|
+
? normalizeInfrastructureRegion(peer?.region)
|
|
47
|
+
: edgePop
|
|
48
|
+
? `edge-${edgePop}`
|
|
49
|
+
: undefined;
|
|
31
50
|
collector.record({
|
|
32
|
-
region,
|
|
51
|
+
region,
|
|
52
|
+
service,
|
|
53
|
+
scope: peerService ? 'internal' : 'external',
|
|
54
|
+
direction,
|
|
33
55
|
activityType: socketTrafficType(event),
|
|
34
56
|
sourceRegion: direction === 'inbound' ? peerRegion : region,
|
|
35
57
|
targetRegion: direction === 'inbound' ? region : peerRegion,
|
|
@@ -61,9 +83,16 @@ export function observeTrafficWebSocket(socket, collector, service, region, opti
|
|
|
61
83
|
const peer = options.getPeer?.();
|
|
62
84
|
const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
|
|
63
85
|
const edgePop = metadataFromHeaders(options.headers ?? {}).edgePop;
|
|
64
|
-
const peerRegion = peerService
|
|
86
|
+
const peerRegion = peerService
|
|
87
|
+
? normalizeInfrastructureRegion(peer?.region)
|
|
88
|
+
: edgePop
|
|
89
|
+
? `edge-${edgePop}`
|
|
90
|
+
: undefined;
|
|
65
91
|
collector.record({
|
|
66
|
-
service,
|
|
92
|
+
service,
|
|
93
|
+
region,
|
|
94
|
+
scope: peerService ? 'internal' : 'external',
|
|
95
|
+
direction,
|
|
67
96
|
activityType: options.activityType ?? 'communication',
|
|
68
97
|
sourceRegion: direction === 'inbound' ? peerRegion : region,
|
|
69
98
|
targetRegion: direction === 'inbound' ? region : peerRegion,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/telemetry",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Framework-agnostic anonymous activity telemetry primitives for Oxy services.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
@@ -19,9 +19,6 @@
|
|
|
19
19
|
],
|
|
20
20
|
"socket": [
|
|
21
21
|
"dist/types/socket.d.ts"
|
|
22
|
-
],
|
|
23
|
-
"edge": [
|
|
24
|
-
"dist/types/edge.d.ts"
|
|
25
22
|
]
|
|
26
23
|
}
|
|
27
24
|
},
|
|
@@ -91,16 +88,6 @@
|
|
|
91
88
|
"types": "./dist/types/socket.d.ts",
|
|
92
89
|
"default": "./dist/cjs/socket.js"
|
|
93
90
|
}
|
|
94
|
-
},
|
|
95
|
-
"./edge": {
|
|
96
|
-
"import": {
|
|
97
|
-
"types": "./dist/types/edge.d.ts",
|
|
98
|
-
"default": "./dist/esm/edge.js"
|
|
99
|
-
},
|
|
100
|
-
"require": {
|
|
101
|
-
"types": "./dist/types/edge.d.ts",
|
|
102
|
-
"default": "./dist/cjs/edge.js"
|
|
103
|
-
}
|
|
104
91
|
}
|
|
105
92
|
},
|
|
106
93
|
"files": [
|
|
@@ -109,7 +96,7 @@
|
|
|
109
96
|
],
|
|
110
97
|
"repository": {
|
|
111
98
|
"type": "git",
|
|
112
|
-
"url": "https://github.com/
|
|
99
|
+
"url": "git+https://github.com/OxyHQ/oxy.git",
|
|
113
100
|
"directory": "packages/telemetry"
|
|
114
101
|
},
|
|
115
102
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -130,7 +117,6 @@
|
|
|
130
117
|
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run typescript && bun run test && bun run build"
|
|
131
118
|
},
|
|
132
119
|
"devDependencies": {
|
|
133
|
-
"@biomejs/biome": "^1.9.4",
|
|
134
120
|
"@types/jest": "^30.0.0",
|
|
135
121
|
"jest": "^30.5.1",
|
|
136
122
|
"jest-environment-jsdom": "^30.5.1",
|
|
@@ -47,8 +47,15 @@ describe('browser telemetry', () => {
|
|
|
47
47
|
it('rediscovers the edge after a VPN change without reloading the page', async () => {
|
|
48
48
|
let now = 1_000;
|
|
49
49
|
let edge = 'MAD';
|
|
50
|
-
const fetchTrace = jest.fn(async () => ({
|
|
51
|
-
|
|
50
|
+
const fetchTrace = jest.fn(async () => ({
|
|
51
|
+
ok: true,
|
|
52
|
+
text: async () => `colo=${edge}\nip=203.0.113.8\nloc=ES`,
|
|
53
|
+
}));
|
|
54
|
+
const telemetry = createBrowserTelemetry({
|
|
55
|
+
location: { hostname: 'oxy.so', protocol: 'https:' },
|
|
56
|
+
now: () => now,
|
|
57
|
+
fetchTrace,
|
|
58
|
+
});
|
|
52
59
|
await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({ 'X-Oxy-Edge-Region': 'mad' });
|
|
53
60
|
edge = 'NRT';
|
|
54
61
|
now += 15_000;
|
|
@@ -58,13 +65,19 @@ describe('browser telemetry', () => {
|
|
|
58
65
|
|
|
59
66
|
it('retries failed discovery on the next window instead of caching failure for the session', async () => {
|
|
60
67
|
let now = 1_000;
|
|
61
|
-
const fetchTrace = jest
|
|
62
|
-
|
|
68
|
+
const fetchTrace = jest
|
|
69
|
+
.fn()
|
|
70
|
+
.mockRejectedValueOnce(new Error('offline'))
|
|
71
|
+
.mockResolvedValue({ ok: true, text: async () => 'colo=SYD' });
|
|
72
|
+
const telemetry = createBrowserTelemetry({
|
|
73
|
+
location: { hostname: 'oxy.so', protocol: 'https:' },
|
|
74
|
+
now: () => now,
|
|
75
|
+
fetchTrace,
|
|
76
|
+
});
|
|
63
77
|
await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({});
|
|
64
78
|
now += 15_000;
|
|
65
79
|
await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({ 'X-Oxy-Edge-Region': 'syd' });
|
|
66
80
|
});
|
|
67
|
-
|
|
68
81
|
});
|
|
69
82
|
|
|
70
83
|
describe('peekEdgeRegionHeader', () => {
|
|
@@ -75,7 +88,10 @@ describe('peekEdgeRegionHeader', () => {
|
|
|
75
88
|
now: () => clock,
|
|
76
89
|
location: { hostname: 'app.oxy.so', protocol: 'https:' },
|
|
77
90
|
edgePeekRefreshMs: 1000,
|
|
78
|
-
fetchTrace: async () => ({
|
|
91
|
+
fetchTrace: async () => ({
|
|
92
|
+
ok: true,
|
|
93
|
+
text: async () => `colo=${pops.shift()?.toUpperCase()}\n`,
|
|
94
|
+
}),
|
|
79
95
|
});
|
|
80
96
|
|
|
81
97
|
expect(telemetry.peekEdgeRegionHeader()).toEqual({});
|
|
@@ -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
|
});
|