@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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oxy.so/telemetry",
3
- "version": "1.2.0",
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
- "edge": [
24
- "dist/types/edge.d.ts"
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
- "./edge": {
95
+ "./metrics": {
96
96
  "import": {
97
- "types": "./dist/types/edge.d.ts",
98
- "default": "./dist/esm/edge.js"
97
+ "types": "./dist/types/metrics.d.ts",
98
+ "default": "./dist/esm/metrics.js"
99
99
  },
100
100
  "require": {
101
- "types": "./dist/types/edge.d.ts",
102
- "default": "./dist/cjs/edge.js"
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/oxyhq/sdk",
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 () => ({ ok: true, text: async () => `colo=${edge}\nip=203.0.113.8\nloc=ES` }));
51
- const telemetry = createBrowserTelemetry({ location: { hostname: 'oxy.so', protocol: 'https:' }, now: () => now, fetchTrace });
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.fn().mockRejectedValueOnce(new Error('offline')).mockResolvedValue({ ok: true, text: async () => 'colo=SYD' });
62
- const telemetry = createBrowserTelemetry({ location: { hostname: 'oxy.so', protocol: 'https:' }, now: () => now, fetchTrace });
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 () => ({ ok: true, text: async () => `colo=${pops.shift()?.toUpperCase()}\n` }),
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 { createTrafficCollector, instrumentTrafficFetch, trafficMiddleware, trafficType, type TrafficAggregate } from '../collector';
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 => { batches.push(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')({ path: '/api/files/private-id', headers: { 'cf-ray': 'example-NRT', authorization: 'secret' } }, response, () => {});
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 => { batches.push(events); });
39
+ const collector = createTrafficCollector(async (events) => {
40
+ batches.push(events);
41
+ });
25
42
  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' }; });
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({ 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' }),
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 => { batches.push(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')({ path, headers: {} }, response, () => {});
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')({ path: '/messages', headers: {} }, aborted, () => {});
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 => { 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);
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(jest.fn(async () => { throw new Error('offline'); }) as unknown as typeof fetch, collector, 'alia', 'us-west-2', () => undefined);
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({ scope: 'external', direction: 'outbound', activityType: 'media' });
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 => { 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' });
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(jest.fn(async () => new Response()), { preconnect }) as unknown as typeof fetch;
77
- const wrapped = instrumentTrafficFetch(baseFetch, collector, 'website', 'us-west-2', () => undefined);
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 => { batches.push(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')({ path: '/messages', headers: { 'x-oxy-source-region': 'eu-west-1' }, serviceApp: { appName: 'mention' } }, response, () => {});
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([expect.objectContaining({ scope: 'internal', direction: 'inbound', sourceRegion: 'eu-west-1', sourceService: 'mention' })]);
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(metadataFromHeaders({
14
- 'x-oxy-edge-region': 'MAD',
15
- 'cf-ray': 'deadbeef-CDG',
16
- 'x-oxy-activity-id': 'anonymous-runtime-a1',
17
- })).toEqual({ edgePop: 'mad', activityId: 'anonymous-runtime-a1' });
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) { 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); }
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({ 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' }),
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(socket, { record: event => events.push(event) }, 'alia', 'us-west-2', { getPeer: () => peer });
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({ scope: 'internal', sourceService: 'mention', sourceRegion: 'eu-west-1', activityType: 'ai' });
41
- stop(); stop();
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}`, { transports: ['websocket'], extraHeaders: { 'X-Oxy-Edge-Region': 'NRT' } });
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) => { client.once('connect', resolve); client.once('connect_error', reject); });
77
- const received = new Promise<void>(resolve => client.once('player:state', () => resolve()));
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'], ['outbound', '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(socket, { record: flow => events.push(flow) }, 'alia', 'us-west-2', {
101
- activityType: 'ai', getPeer: () => ({ service: 'alia-integrations', region: 'eu-west-1' }),
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(true);
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(() => { throw new Error('send failed'); });
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(); stop();
164
+ stop();
165
+ stop();
115
166
  expect(socket.send).toBe(original);
116
167
  expect(socket.listenerCount('message')).toBe(0);
117
168
  });