@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.
@@ -1,10 +1,33 @@
1
1
  import { metadataFromHeaders } from './server.js';
2
2
  const TYPES = {
3
- auth: 'identity', session: 'identity', users: 'identity', profiles: 'identity', accounts: 'identity',
4
- ai: 'ai', inference: 'ai', models: 'ai', chat: 'ai', completions: 'ai',
5
- stream: 'media', tracks: 'media', episodes: 'media', recordings: 'media', rooms: 'media',
6
- messages: 'communication', notifications: 'communication', mail: 'communication',
7
- media: 'media', files: 'media', assets: 'media', upload: 'media', uploads: 'media', cdn: 'media', storage: 'media', images: 'media', audio: 'media', videos: 'media',
3
+ auth: 'identity',
4
+ session: 'identity',
5
+ users: 'identity',
6
+ profiles: 'identity',
7
+ accounts: 'identity',
8
+ ai: 'ai',
9
+ inference: 'ai',
10
+ models: 'ai',
11
+ chat: 'ai',
12
+ completions: 'ai',
13
+ stream: 'media',
14
+ tracks: 'media',
15
+ episodes: 'media',
16
+ recordings: 'media',
17
+ rooms: 'media',
18
+ messages: 'communication',
19
+ notifications: 'communication',
20
+ mail: 'communication',
21
+ media: 'media',
22
+ files: 'media',
23
+ assets: 'media',
24
+ upload: 'media',
25
+ uploads: 'media',
26
+ cdn: 'media',
27
+ storage: 'media',
28
+ images: 'media',
29
+ audio: 'media',
30
+ videos: 'media',
8
31
  };
9
32
  export function trafficType(path) {
10
33
  const parts = path.split('?')[0].split('/').filter(Boolean);
@@ -34,7 +57,12 @@ export function createTrafficCollector(publish, now = Date.now) {
34
57
  if (pending.size === 0)
35
58
  return;
36
59
  const emittedAt = new Date(now()).toISOString();
37
- const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({ ...flow, requests, windowStartedAt: new Date(startedAt).toISOString(), emittedAt }));
60
+ const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({
61
+ ...flow,
62
+ requests,
63
+ windowStartedAt: new Date(startedAt).toISOString(),
64
+ emittedAt,
65
+ }));
38
66
  pending.clear();
39
67
  flushing = (async () => {
40
68
  for (let offset = 0; offset < events.length; offset += 256)
@@ -54,7 +82,10 @@ export function trafficMiddleware(collector, service, region) {
54
82
  response.setHeader?.('X-Oxy-Region', region);
55
83
  const path = request.path ?? request.url ?? '/';
56
84
  // Never report collection itself, probes or the dashboard transport.
57
- if (/^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(path) || path.startsWith('/internal/activity') || path.startsWith('/auth/service-token') || path.startsWith('/cdn-cgi/')) {
85
+ if (/^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(path) ||
86
+ path.startsWith('/internal/activity') ||
87
+ path.startsWith('/auth/service-token') ||
88
+ path.startsWith('/cdn-cgi/')) {
58
89
  next();
59
90
  return;
60
91
  }
@@ -63,11 +94,28 @@ export function trafficMiddleware(collector, service, region) {
63
94
  let finished = false;
64
95
  const inboundFlow = () => {
65
96
  // Read service identity AFTER the route's shared auth middleware resolved it.
66
- const name = request.serviceApp?.appName?.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 40);
97
+ const name = request.serviceApp?.appName
98
+ ?.toLowerCase()
99
+ .replace(/[^a-z0-9-]/g, '-')
100
+ .slice(0, 40);
67
101
  const internal = Boolean(name);
68
102
  const forwardedRegion = request.headers['x-oxy-source-region'];
69
- const peerRegion = internal ? normalizeInfrastructureRegion(forwardedRegion) : edgePop ? `edge-${edgePop}` : undefined;
70
- return { region, service, activityType, scope: internal ? 'internal' : 'external', direction: 'inbound', sourceRegion: peerRegion, targetRegion: region, sourceService: name, targetService: service };
103
+ const peerRegion = internal
104
+ ? normalizeInfrastructureRegion(forwardedRegion)
105
+ : edgePop
106
+ ? `edge-${edgePop}`
107
+ : undefined;
108
+ return {
109
+ region,
110
+ service,
111
+ activityType,
112
+ scope: internal ? 'internal' : 'external',
113
+ direction: 'inbound',
114
+ sourceRegion: peerRegion,
115
+ targetRegion: region,
116
+ sourceService: name,
117
+ targetService: service,
118
+ };
71
119
  };
72
120
  const finish = () => {
73
121
  if (finished)
@@ -75,7 +123,14 @@ export function trafficMiddleware(collector, service, region) {
75
123
  finished = true;
76
124
  const flow = inboundFlow();
77
125
  collector.record(flow);
78
- collector.record({ ...flow, direction: 'outbound', sourceRegion: region, targetRegion: flow.sourceRegion, sourceService: service, targetService: flow.sourceService });
126
+ collector.record({
127
+ ...flow,
128
+ direction: 'outbound',
129
+ sourceRegion: region,
130
+ targetRegion: flow.sourceRegion,
131
+ sourceService: service,
132
+ targetService: flow.sourceService,
133
+ });
79
134
  };
80
135
  response.once('finish', finish);
81
136
  // Failed/aborted requests are still real inbound activity. Do not invent a
@@ -93,13 +148,21 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
93
148
  return Object.assign(async (input, init) => {
94
149
  const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
95
150
  // Collection/auth control traffic cannot generate a feedback loop.
96
- if (url.pathname.startsWith('/internal/activity') || url.pathname.startsWith('/auth/service-token') || url.pathname === '/platform-infrastructure')
151
+ if (url.pathname.startsWith('/internal/activity') ||
152
+ url.pathname.startsWith('/auth/service-token') ||
153
+ url.pathname === '/platform-infrastructure')
97
154
  return fetcher(input, init);
98
155
  const peer = resolveEndpoint(url);
99
156
  const flow = {
100
- service, region, scope: peer ? 'internal' : 'external', direction: 'outbound',
101
- activityType: trafficType(url.pathname), sourceRegion: region, targetRegion: peer?.region,
102
- sourceService: service, targetService: peer?.service,
157
+ service,
158
+ region,
159
+ scope: peer ? 'internal' : 'external',
160
+ direction: 'outbound',
161
+ activityType: trafficType(url.pathname),
162
+ sourceRegion: region,
163
+ targetRegion: peer?.region,
164
+ sourceService: service,
165
+ targetService: peer?.service,
103
166
  };
104
167
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
105
168
  if (peer)
@@ -112,23 +175,43 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
112
175
  collector.record({ ...flow, targetRegion: undefined });
113
176
  throw error;
114
177
  }
115
- const targetRegion = peer ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined) : undefined;
178
+ const targetRegion = peer
179
+ ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined)
180
+ : undefined;
116
181
  collector.record({ ...flow, targetRegion });
117
- collector.record({ ...flow, direction: 'inbound', sourceRegion: targetRegion, targetRegion: region, sourceService: peer?.service, targetService: service });
182
+ collector.record({
183
+ ...flow,
184
+ direction: 'inbound',
185
+ sourceRegion: targetRegion,
186
+ targetRegion: region,
187
+ sourceService: peer?.service,
188
+ targetService: service,
189
+ });
118
190
  return response;
119
191
  }, fetcher);
120
192
  }
121
193
  // Exact deployed API hosts; an arbitrary caller-supplied hostname cannot claim
122
194
  // to be an internal service. Region is infrastructure metadata, never IP geo.
123
195
  const OXY_API_SERVICES = {
124
- 'api.oxy.so': 'oxy-api', 'api.website.oxy.so': 'website',
125
- 'website-api.oxy.so': 'website', 'api.mention.earth': 'mention',
126
- 'api.mercaria.co': 'mercaria', 'api.alia.onl': 'alia', 'kaana.ai': 'kaana',
127
- 'api.homiio.com': 'homiio', 'api.syra.fm': 'syra',
128
- 'mention.earth': 'mention', 'mcp.mention.earth': 'mention-mcp',
129
- 'api.clarity.surf': 'clarity', 'api.peable.to': 'peable',
130
- 'api.crowdsource.oxy.so': 'crowdsource', 'api.moovo.now': 'moovo',
131
- 'api.allo.you': 'allo', 'api.noted.oxy.so': 'noted', 'api.schedio.app': 'schedio', 'api.tnp.network': 'tnp-api',
196
+ 'api.oxy.so': 'oxy-api',
197
+ 'api.website.oxy.so': 'website',
198
+ 'website-api.oxy.so': 'website',
199
+ 'api.mention.earth': 'mention',
200
+ 'api.mercaria.co': 'mercaria',
201
+ 'api.alia.onl': 'alia',
202
+ 'kaana.ai': 'kaana',
203
+ 'api.homiio.com': 'homiio',
204
+ 'api.syra.fm': 'syra',
205
+ 'mention.earth': 'mention',
206
+ 'mcp.mention.earth': 'mention-mcp',
207
+ 'api.clarity.surf': 'clarity',
208
+ 'api.peable.to': 'peable',
209
+ 'api.crowdsource.oxy.so': 'crowdsource',
210
+ 'api.moovo.now': 'moovo',
211
+ 'api.allo.you': 'allo',
212
+ 'api.noted.oxy.so': 'noted',
213
+ 'api.schedio.app': 'schedio',
214
+ 'api.tnp.network': 'tnp-api',
132
215
  };
133
216
  export function resolveOxyServiceEndpoint(url) {
134
217
  const service = OXY_API_SERVICES[url.hostname];
@@ -165,7 +248,9 @@ const REGION_LOCATIONS = {
165
248
  'me-central-1': { label: 'UAE', coordinates: [54.37, 24.45] },
166
249
  'il-central-1': { label: 'Tel Aviv', coordinates: [34.78, 32.09] },
167
250
  };
168
- export function infrastructureLocation(region) { return REGION_LOCATIONS[region]; }
251
+ export function infrastructureLocation(region) {
252
+ return REGION_LOCATIONS[region];
253
+ }
169
254
  export function normalizeInfrastructureRegion(value) {
170
255
  return typeof value === 'string' && /^[a-z]{2}(?:-[a-z]+)+-\d$/.test(value) ? value : undefined;
171
256
  }
@@ -7,7 +7,9 @@ function firstHeaderValue(value) {
7
7
  return typeof first === 'string' ? first : undefined;
8
8
  }
9
9
  export function normalizeEdgePop(value) {
10
- return firstHeaderValue(value)?.match(/^[a-z]{3}$/i)?.[0]?.toLowerCase();
10
+ return firstHeaderValue(value)
11
+ ?.match(/^[a-z]{3}$/i)?.[0]
12
+ ?.toLowerCase();
11
13
  }
12
14
  export function normalizeActivityId(value) {
13
15
  const candidate = firstHeaderValue(value);
@@ -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', session: 'identity', user: 'identity', profile: 'identity',
5
- chat: 'ai', agent: 'ai', inference: 'ai', completion: 'ai',
6
- message: 'communication', notification: 'communication', typing: 'communication',
7
- media: 'media', audio: 'media', video: 'media', stream: 'media', player: 'media',
8
- track: 'media', playback: 'media', playlist: 'media', room: 'media',
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 ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
45
+ const peerRegion = peerService
46
+ ? normalizeInfrastructureRegion(peer?.region)
47
+ : edgePop
48
+ ? `edge-${edgePop}`
49
+ : undefined;
31
50
  collector.record({
32
- region, service, scope: peerService ? 'internal' : 'external', direction,
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 ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
86
+ const peerRegion = peerService
87
+ ? normalizeInfrastructureRegion(peer?.region)
88
+ : edgePop
89
+ ? `edge-${edgePop}`
90
+ : undefined;
65
91
  collector.record({
66
- service, region, scope: peerService ? 'internal' : 'external', direction,
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,
@@ -1,6 +1,12 @@
1
1
  export declare const DEFAULT_ACTIVITY_ID_ROTATION_MS: number;
2
2
  export declare const DEFAULT_EDGE_TRACE_TIMEOUT_MS = 1000;
3
3
  export declare const DEFAULT_EDGE_CACHE_MS = 15000;
4
+ /**
5
+ * How long `peekEdgeRegionHeader` trusts a known PoP before refreshing it in the
6
+ * background. A client's serving PoP rarely changes within minutes, and the
7
+ * peek never blocks, so this only bounds the trace traffic.
8
+ */
9
+ export declare const DEFAULT_EDGE_PEEK_REFRESH_MS: number;
4
10
  interface TraceResponse {
5
11
  ok: boolean;
6
12
  text(): Promise<string>;
@@ -9,6 +15,7 @@ export interface BrowserTelemetryOptions {
9
15
  activityIdRotationMs?: number;
10
16
  edgeTraceTimeoutMs?: number;
11
17
  edgeCacheMs?: number;
18
+ edgePeekRefreshMs?: number;
12
19
  now?: () => number;
13
20
  crypto?: Pick<Crypto, 'getRandomValues' | 'randomUUID'>;
14
21
  location?: Pick<Location, 'hostname' | 'protocol'>;
@@ -17,10 +24,17 @@ export interface BrowserTelemetryOptions {
17
24
  export interface BrowserTelemetry {
18
25
  getActivityIdHeader(): Record<string, string>;
19
26
  getEdgeRegionHeader(): Promise<Record<string, string>>;
27
+ /**
28
+ * The last known edge-region header, synchronously, never waiting on the
29
+ * trace: `{}` until the first discovery lands. A stale or missing value
30
+ * starts a refresh in the background.
31
+ */
32
+ peekEdgeRegionHeader(): Record<string, string>;
20
33
  getHeaders(): Promise<Record<string, string>>;
21
34
  }
22
35
  export declare function createBrowserTelemetry(options?: BrowserTelemetryOptions): BrowserTelemetry;
23
36
  export declare const getBrowserActivityIdHeader: () => Record<string, string>;
24
37
  export declare const getBrowserEdgeRegionHeader: () => Promise<Record<string, string>>;
38
+ export declare const peekBrowserEdgeRegionHeader: () => Record<string, string>;
25
39
  export declare const getBrowserTelemetryHeaders: () => Promise<Record<string, string>>;
26
40
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/telemetry",
3
- "version": "1.1.1",
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,10 +96,10 @@
109
96
  ],
110
97
  "repository": {
111
98
  "type": "git",
112
- "url": "https://github.com/oxyhq/sdk",
99
+ "url": "git+https://github.com/OxyHQ/oxy.git",
113
100
  "directory": "packages/telemetry"
114
101
  },
115
- "license": "Apache-2.0",
102
+ "license": "SEE LICENSE IN LICENSE",
116
103
  "author": "Oxy",
117
104
  "homepage": "https://oxy.so",
118
105
  "engines": {
@@ -130,10 +117,11 @@
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",
123
+ "socket.io": "^4.8.1",
124
+ "socket.io-client": "^4.8.1",
137
125
  "ts-jest": "^29.4.12",
138
126
  "typescript": "^5.9.2"
139
127
  }
@@ -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,11 +65,42 @@ 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
  });
81
+ });
82
+
83
+ describe('peekEdgeRegionHeader', () => {
84
+ it('never waits: {} first, the PoP once discovered, refreshed in the background', async () => {
85
+ let clock = 0;
86
+ const pops = ['mad', 'nrt'];
87
+ const telemetry = createBrowserTelemetry({
88
+ now: () => clock,
89
+ location: { hostname: 'app.oxy.so', protocol: 'https:' },
90
+ edgePeekRefreshMs: 1000,
91
+ fetchTrace: async () => ({
92
+ ok: true,
93
+ text: async () => `colo=${pops.shift()?.toUpperCase()}\n`,
94
+ }),
95
+ });
67
96
 
97
+ expect(telemetry.peekEdgeRegionHeader()).toEqual({});
98
+ await new Promise((resolve) => setTimeout(resolve, 0));
99
+ expect(telemetry.peekEdgeRegionHeader()).toEqual({ 'X-Oxy-Edge-Region': 'mad' });
100
+
101
+ clock = 1000;
102
+ expect(telemetry.peekEdgeRegionHeader()).toEqual({ 'X-Oxy-Edge-Region': 'mad' });
103
+ await new Promise((resolve) => setTimeout(resolve, 0));
104
+ expect(telemetry.peekEdgeRegionHeader()).toEqual({ 'X-Oxy-Edge-Region': 'nrt' });
105
+ });
68
106
  });