@oxy.so/telemetry 1.2.0 → 2.1.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/README.md +30 -0
- package/dist/cjs/browser.js +18 -8
- package/dist/cjs/collector.js +111 -26
- package/dist/cjs/metrics.js +208 -0
- 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/metrics.js +203 -0
- package/dist/esm/server.js +3 -1
- package/dist/esm/socket.js +38 -9
- package/dist/types/metrics.d.ts +79 -0
- package/dist/types/package.json +1 -0
- package/package.json +11 -12
- package/src/__tests__/browser.test.ts +22 -6
- package/src/__tests__/collector.test.ts +132 -27
- package/src/__tests__/metrics.test.ts +45 -0
- 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/metrics.ts +286 -0
- 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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxy.so/telemetry",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Framework-agnostic anonymous activity telemetry primitives for Oxy services.",
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "Framework-agnostic anonymous activity telemetry and CloudWatch EMF metrics primitives for Oxy services.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/cjs/index.js",
|
|
7
7
|
"module": "dist/esm/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"socket": [
|
|
21
21
|
"dist/types/socket.d.ts"
|
|
22
22
|
],
|
|
23
|
-
"
|
|
24
|
-
"dist/types/
|
|
23
|
+
"metrics": [
|
|
24
|
+
"dist/types/metrics.d.ts"
|
|
25
25
|
]
|
|
26
26
|
}
|
|
27
27
|
},
|
|
@@ -92,14 +92,14 @@
|
|
|
92
92
|
"default": "./dist/cjs/socket.js"
|
|
93
93
|
}
|
|
94
94
|
},
|
|
95
|
-
"./
|
|
95
|
+
"./metrics": {
|
|
96
96
|
"import": {
|
|
97
|
-
"types": "./dist/types/
|
|
98
|
-
"default": "./dist/esm/
|
|
97
|
+
"types": "./dist/types/metrics.d.ts",
|
|
98
|
+
"default": "./dist/esm/metrics.js"
|
|
99
99
|
},
|
|
100
100
|
"require": {
|
|
101
|
-
"types": "./dist/types/
|
|
102
|
-
"default": "./dist/cjs/
|
|
101
|
+
"types": "./dist/types/metrics.d.ts",
|
|
102
|
+
"default": "./dist/cjs/metrics.js"
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
},
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
],
|
|
110
110
|
"repository": {
|
|
111
111
|
"type": "git",
|
|
112
|
-
"url": "https://github.com/
|
|
112
|
+
"url": "git+https://github.com/OxyHQ/oxy.git",
|
|
113
113
|
"directory": "packages/telemetry"
|
|
114
114
|
},
|
|
115
115
|
"license": "SEE LICENSE IN LICENSE",
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types",
|
|
123
123
|
"build:cjs": "tsc -p tsconfig.cjs.json && node scripts/mark-cjs.mjs",
|
|
124
124
|
"build:esm": "tsc -p tsconfig.esm.json",
|
|
125
|
-
"build:types": "tsc -p tsconfig.types.json",
|
|
125
|
+
"build:types": "tsc -p tsconfig.types.json && node scripts/mark-types-cjs.mjs",
|
|
126
126
|
"clean": "rm -rf dist",
|
|
127
127
|
"typescript": "tsc --noEmit",
|
|
128
128
|
"test": "jest",
|
|
@@ -130,7 +130,6 @@
|
|
|
130
130
|
"prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run typescript && bun run test && bun run build"
|
|
131
131
|
},
|
|
132
132
|
"devDependencies": {
|
|
133
|
-
"@biomejs/biome": "^1.9.4",
|
|
134
133
|
"@types/jest": "^30.0.0",
|
|
135
134
|
"jest": "^30.5.1",
|
|
136
135
|
"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', () => {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createEmfPublisher, HTTP_REQUEST_LATENCY, HTTP_RATE_LIMITED } from '../metrics';
|
|
2
|
+
|
|
3
|
+
describe('EMF publisher', () => {
|
|
4
|
+
it('folds an interval into one EMF line per series with bounded dimensions', () => {
|
|
5
|
+
const lines: string[] = [];
|
|
6
|
+
const publisher = createEmfPublisher({
|
|
7
|
+
namespace: 'Oxy/Test',
|
|
8
|
+
service: 'svc',
|
|
9
|
+
write: (line) => lines.push(line),
|
|
10
|
+
now: () => 1_000,
|
|
11
|
+
maxSeries: 2,
|
|
12
|
+
});
|
|
13
|
+
const labels = { Route: '/users/:id', StatusClass: '2xx', Caller: 'anonymous' };
|
|
14
|
+
for (const ms of [10, 10, 200]) publisher.record(HTTP_REQUEST_LATENCY, ms, labels);
|
|
15
|
+
publisher.record(HTTP_RATE_LIMITED, 1, { Caller: 'service:app' });
|
|
16
|
+
// A third series exceeds maxSeries and folds into `other`.
|
|
17
|
+
publisher.record(HTTP_RATE_LIMITED, 1, { Caller: 'service:another' });
|
|
18
|
+
publisher.stop();
|
|
19
|
+
|
|
20
|
+
const [latency, limited, overflow] = lines.map((line) => JSON.parse(line));
|
|
21
|
+
expect(latency._aws).toEqual({
|
|
22
|
+
Timestamp: 1_000,
|
|
23
|
+
CloudWatchMetrics: [
|
|
24
|
+
{
|
|
25
|
+
Namespace: 'Oxy/Test',
|
|
26
|
+
Dimensions: [['Service'], ['Service', 'StatusClass'], ['Service', 'Caller']],
|
|
27
|
+
Metrics: [{ Name: 'RequestLatency', Unit: 'Milliseconds' }],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
expect(latency).toMatchObject({ Service: 'svc', Route: '/users/:id', StatusClass: '2xx' });
|
|
32
|
+
// Values are ~5 % buckets; Count/Sum/Min/Max stay exact.
|
|
33
|
+
expect(latency.RequestLatency.Values).toEqual([9.91, 204]);
|
|
34
|
+
expect(latency.RequestLatency).toMatchObject({
|
|
35
|
+
Counts: [2, 1],
|
|
36
|
+
Max: 200,
|
|
37
|
+
Min: 10,
|
|
38
|
+
Count: 3,
|
|
39
|
+
Sum: 220,
|
|
40
|
+
});
|
|
41
|
+
expect(limited).toMatchObject({ Caller: 'service:app', RateLimited: 1 });
|
|
42
|
+
expect(overflow).toMatchObject({ Caller: 'other', RateLimited: 1 });
|
|
43
|
+
expect(lines).toHaveLength(3);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -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
|
});
|