@oxy.so/telemetry 1.0.0 → 1.1.1

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 CHANGED
@@ -4,12 +4,11 @@ Framework-agnostic primitives for anonymous, real-time Oxy activity telemetry.
4
4
  The package has no runtime dependencies and does not import Express, Socket.IO,
5
5
  Redis, React, React Native or Expo.
6
6
 
7
- Version: `0.1.2`.
8
-
9
7
  ## Entry points
10
8
 
11
9
  - `@oxy.so/telemetry` — shared header names and aggregate interfaces.
12
10
  - `@oxy.so/telemetry/browser` — ephemeral browser metadata.
11
+ - `@oxy.so/telemetry/collector` — bounded HTTP/custom-transport flow aggregation.
13
12
  - `@oxy.so/telemetry/server` — pure validation, route bucketing and cardinality
14
13
  keys/interfaces.
15
14
 
@@ -33,7 +32,7 @@ session storage, IndexedDB or a URL. Closing or reloading the page loses it.
33
32
  `X-Oxy-Edge-Region` contains only the three-letter Cloudflare PoP code obtained
34
33
  from the same-origin `/cdn-cgi/trace` endpoint. The helper reads no IP field and
35
34
  returns, logs and persists none of the rest of the trace response. Discovery is
36
- cached per runtime, has a one-second timeout and is disabled on loopback.
35
+ cached for 15 seconds per runtime (including failed discovery), has a one-second timeout and is disabled on loopback.
37
36
 
38
37
  Use `createBrowserTelemetry()` to create an isolated runtime or inject clock,
39
38
  crypto, location and trace-fetch dependencies in tests.
@@ -95,21 +94,44 @@ exports while applications migrate their direct imports.
95
94
 
96
95
  ## Collector, realtime and reconnect
97
96
 
98
- The intended collector records only successful, non-control-plane requests. It
99
- increments request totals per `source region × bounded service × time window`
100
- and sends the ephemeral activity ID to a cardinality adapter, never to an event
101
- row or log.
97
+ The collector records successful, failed and aborted HTTP requests. A completed
98
+ response is a separate observed outbound flow; an aborted request never produces
99
+ a fabricated response. Scope (`internal`/`external`), direction relative to the
100
+ reporter (`inbound`/`outbound`) and type (`identity`, `ai`, `communication`,
101
+ `media`, `platform`) are independent dimensions. Source and destination always
102
+ follow the actual movement, even when two services share one region.
103
+
104
+ `trafficMiddleware` attaches before routes and reads the verified service
105
+ identity after route authentication. `instrumentTrafficFetch` observes actual
106
+ outbound calls; internal destination regions come from the peer's `X-Oxy-Region`
107
+ response, not a guessed IP location. Unlocated peers remain unlocated. Health,
108
+ telemetry collection, infrastructure snapshots and service-token minting are
109
+ excluded from observation where applicable to prevent control traffic feedback.
110
+ Custom transports can call the collector's `record` method explicitly.
111
+
112
+ Each collector retains at most 512 distinct pending flows and publishes chunks
113
+ of at most 256. Publishing is best effort: a failed batch is discarded, with no
114
+ durable retry queue. These are live operational observations, not billing or
115
+ lossless historical request totals. No dashboard viewer is needed to collect.
102
116
 
103
117
  Realtime transport publishes aggregate buckets, not individual metadata. A
104
118
  socket event may carry `ActivityAggregate`: request count, approximate active
105
119
  clients, source/target regions, service and window timestamps. Transport and
106
120
  fan-out are application concerns and intentionally absent from this package.
107
121
 
108
- Realtime events are not a durable replay log. After reconnect, a consumer first
109
- loads the collector's current aggregate snapshot and then resumes the live
110
- stream, deduplicating any overlapping window by its stable flow/window identity.
111
- Reconnect backoff, socket lifecycle and snapshot endpoints belong to the
112
- consumer and collector, not these pure primitives.
122
+ Realtime activity events are not a durable replay log. The dashboard retains a
123
+ 60-second window of distinct flows and resumes live events on reconnect.
124
+ Infrastructure is separate: the API broadcasts a complete snapshot on connection
125
+ and on changes. Registration/removal updates immediately; heartbeats run every
126
+ 10 seconds and crashed instances expire after 45 seconds (observed at the next
127
+ registry refresh). Only reporting instances are included. The publisher must be
128
+ installed in each service before this can represent the whole ecosystem.
129
+
130
+ The authenticated publisher is `createEcosystemTraffic` from `@oxy.so/core/server`.
131
+ It exposes HTTP middleware, `wrapFetch`, `record` for explicit transport events,
132
+ and `stop` for graceful removal. It requires an existing trusted service
133
+ credential and a known infrastructure region (or explicit infrastructure
134
+ coordinates). Credentials, user metadata and paths never enter public snapshots.
113
135
 
114
136
  ## Development
115
137
 
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getBrowserTelemetryHeaders = exports.getBrowserEdgeRegionHeader = exports.getBrowserActivityIdHeader = exports.DEFAULT_EDGE_TRACE_TIMEOUT_MS = exports.DEFAULT_ACTIVITY_ID_ROTATION_MS = void 0;
3
+ exports.getBrowserTelemetryHeaders = exports.getBrowserEdgeRegionHeader = exports.getBrowserActivityIdHeader = exports.DEFAULT_EDGE_CACHE_MS = exports.DEFAULT_EDGE_TRACE_TIMEOUT_MS = exports.DEFAULT_ACTIVITY_ID_ROTATION_MS = void 0;
4
4
  exports.createBrowserTelemetry = createBrowserTelemetry;
5
5
  const index_js_1 = require("./index.js");
6
6
  exports.DEFAULT_ACTIVITY_ID_ROTATION_MS = 5 * 60 * 1000;
7
7
  exports.DEFAULT_EDGE_TRACE_TIMEOUT_MS = 1000;
8
+ exports.DEFAULT_EDGE_CACHE_MS = 15000;
8
9
  function createUuid(cryptoApi) {
9
10
  if (!cryptoApi)
10
11
  return null;
@@ -27,6 +28,7 @@ function createBrowserTelemetry(options = {}) {
27
28
  let activityId = null;
28
29
  let activityIdCreatedAt = 0;
29
30
  let edgePopPromise = null;
31
+ let edgePopReadAt = -Infinity;
30
32
  const getActivityIdHeader = () => {
31
33
  const hasInjectedBrowser = Boolean(options.location);
32
34
  const hasBrowserRuntime = typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -64,7 +66,11 @@ function createBrowserTelemetry(options = {}) {
64
66
  }
65
67
  };
66
68
  const getEdgeRegionHeader = async () => {
67
- edgePopPromise ?? (edgePopPromise = discoverEdgePop());
69
+ const now = (options.now ?? Date.now)();
70
+ if (!edgePopPromise || now < edgePopReadAt || now - edgePopReadAt >= (options.edgeCacheMs ?? exports.DEFAULT_EDGE_CACHE_MS)) {
71
+ edgePopReadAt = now;
72
+ edgePopPromise = discoverEdgePop();
73
+ }
68
74
  const edgePop = await edgePopPromise;
69
75
  return edgePop ? { [index_js_1.OXY_EDGE_REGION_HEADER]: edgePop } : {};
70
76
  };
@@ -0,0 +1,180 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.trafficType = trafficType;
4
+ exports.createTrafficCollector = createTrafficCollector;
5
+ exports.trafficMiddleware = trafficMiddleware;
6
+ exports.instrumentTrafficFetch = instrumentTrafficFetch;
7
+ exports.resolveOxyServiceEndpoint = resolveOxyServiceEndpoint;
8
+ exports.infrastructureLocation = infrastructureLocation;
9
+ exports.normalizeInfrastructureRegion = normalizeInfrastructureRegion;
10
+ const server_js_1 = require("./server.js");
11
+ const TYPES = {
12
+ auth: 'identity', session: 'identity', users: 'identity', profiles: 'identity', accounts: 'identity',
13
+ ai: 'ai', inference: 'ai', models: 'ai', chat: 'ai', completions: 'ai',
14
+ stream: 'media', tracks: 'media', episodes: 'media', recordings: 'media', rooms: 'media',
15
+ messages: 'communication', notifications: 'communication', mail: 'communication',
16
+ media: 'media', files: 'media', assets: 'media', upload: 'media', uploads: 'media', cdn: 'media', storage: 'media', images: 'media', audio: 'media', videos: 'media',
17
+ };
18
+ function trafficType(path) {
19
+ const parts = path.split('?')[0].split('/').filter(Boolean);
20
+ if (parts[0] === 'alia')
21
+ parts.shift();
22
+ if (parts[0] === 'api')
23
+ parts.shift();
24
+ if (/^v\d+$/.test(parts[0] ?? ''))
25
+ parts.shift();
26
+ return TYPES[parts[0] ?? ''] ?? 'platform';
27
+ }
28
+ function createTrafficCollector(publish, now = Date.now) {
29
+ const pending = new Map();
30
+ let flushing = null;
31
+ return {
32
+ record(flow) {
33
+ const key = JSON.stringify(flow);
34
+ const existing = pending.get(key);
35
+ if (existing)
36
+ existing.requests++;
37
+ else if (pending.size < 512)
38
+ pending.set(key, { flow, requests: 1, startedAt: now() });
39
+ },
40
+ async flush() {
41
+ while (flushing)
42
+ await flushing;
43
+ if (pending.size === 0)
44
+ return;
45
+ const emittedAt = new Date(now()).toISOString();
46
+ const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({ ...flow, requests, windowStartedAt: new Date(startedAt).toISOString(), emittedAt }));
47
+ pending.clear();
48
+ flushing = (async () => {
49
+ for (let offset = 0; offset < events.length; offset += 256)
50
+ await publish(events.slice(offset, offset + 256));
51
+ })();
52
+ try {
53
+ await flushing;
54
+ }
55
+ finally {
56
+ flushing = null;
57
+ }
58
+ },
59
+ };
60
+ }
61
+ function trafficMiddleware(collector, service, region) {
62
+ return (request, response, next) => {
63
+ response.setHeader?.('X-Oxy-Region', region);
64
+ const path = request.path ?? request.url ?? '/';
65
+ // Never report collection itself, probes or the dashboard transport.
66
+ 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/')) {
67
+ next();
68
+ return;
69
+ }
70
+ const { edgePop } = (0, server_js_1.metadataFromHeaders)(request.headers);
71
+ const activityType = trafficType(path);
72
+ let finished = false;
73
+ const inboundFlow = () => {
74
+ // Read service identity AFTER the route's shared auth middleware resolved it.
75
+ const name = request.serviceApp?.appName?.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 40);
76
+ const internal = Boolean(name);
77
+ const forwardedRegion = request.headers['x-oxy-source-region'];
78
+ const peerRegion = internal ? normalizeInfrastructureRegion(forwardedRegion) : edgePop ? `edge-${edgePop}` : undefined;
79
+ return { region, service, activityType, scope: internal ? 'internal' : 'external', direction: 'inbound', sourceRegion: peerRegion, targetRegion: region, sourceService: name, targetService: service };
80
+ };
81
+ const finish = () => {
82
+ if (finished)
83
+ return;
84
+ finished = true;
85
+ const flow = inboundFlow();
86
+ collector.record(flow);
87
+ collector.record({ ...flow, direction: 'outbound', sourceRegion: region, targetRegion: flow.sourceRegion, sourceService: service, targetService: flow.sourceService });
88
+ };
89
+ response.once('finish', finish);
90
+ // Failed/aborted requests are still real inbound activity. Do not invent a
91
+ // completed outbound response when the connection closes prematurely.
92
+ response.once('close', () => {
93
+ if (finished)
94
+ return;
95
+ finished = true;
96
+ collector.record(inboundFlow());
97
+ });
98
+ next();
99
+ };
100
+ }
101
+ function instrumentTrafficFetch(fetcher, collector, service, region, resolveEndpoint) {
102
+ return Object.assign(async (input, init) => {
103
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
104
+ // Collection/auth control traffic cannot generate a feedback loop.
105
+ if (url.pathname.startsWith('/internal/activity') || url.pathname.startsWith('/auth/service-token') || url.pathname === '/platform-infrastructure')
106
+ return fetcher(input, init);
107
+ const peer = resolveEndpoint(url);
108
+ const flow = {
109
+ service, region, scope: peer ? 'internal' : 'external', direction: 'outbound',
110
+ activityType: trafficType(url.pathname), sourceRegion: region, targetRegion: peer?.region,
111
+ sourceService: service, targetService: peer?.service,
112
+ };
113
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
114
+ if (peer)
115
+ headers.set('X-Oxy-Source-Region', region);
116
+ let response;
117
+ try {
118
+ response = await fetcher(input, { ...init, headers });
119
+ }
120
+ catch (error) {
121
+ collector.record({ ...flow, targetRegion: undefined });
122
+ throw error;
123
+ }
124
+ const targetRegion = peer ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined) : undefined;
125
+ collector.record({ ...flow, targetRegion });
126
+ collector.record({ ...flow, direction: 'inbound', sourceRegion: targetRegion, targetRegion: region, sourceService: peer?.service, targetService: service });
127
+ return response;
128
+ }, fetcher);
129
+ }
130
+ // Exact deployed API hosts; an arbitrary caller-supplied hostname cannot claim
131
+ // to be an internal service. Region is infrastructure metadata, never IP geo.
132
+ const OXY_API_SERVICES = {
133
+ 'api.oxy.so': 'oxy-api', 'api.website.oxy.so': 'website',
134
+ 'website-api.oxy.so': 'website', 'api.mention.earth': 'mention',
135
+ 'api.mercaria.co': 'mercaria', 'api.alia.onl': 'alia', 'kaana.ai': 'kaana',
136
+ 'api.homiio.com': 'homiio', 'api.syra.fm': 'syra',
137
+ 'mention.earth': 'mention', 'mcp.mention.earth': 'mention-mcp',
138
+ 'api.clarity.surf': 'clarity', 'api.peable.to': 'peable',
139
+ 'api.crowdsource.oxy.so': 'crowdsource', 'api.moovo.now': 'moovo',
140
+ 'api.allo.you': 'allo', 'api.noted.oxy.so': 'noted', 'api.schedio.app': 'schedio', 'api.tnp.network': 'tnp-api',
141
+ };
142
+ function resolveOxyServiceEndpoint(url) {
143
+ const service = OXY_API_SERVICES[url.hostname];
144
+ return url.protocol === 'https:' && service ? { service, region: 'us-west-2' } : undefined;
145
+ }
146
+ const REGION_LOCATIONS = {
147
+ 'us-west-2': { label: 'Oregon', coordinates: [-122.6765, 45.5231] },
148
+ 'us-west-1': { label: 'Northern California', coordinates: [-121.89, 37.34] },
149
+ 'us-east-1': { label: 'Northern Virginia', coordinates: [-77.49, 39.04] },
150
+ 'us-east-2': { label: 'Ohio', coordinates: [-82.99, 39.96] },
151
+ 'eu-west-1': { label: 'Ireland', coordinates: [-6.26, 53.35] },
152
+ 'eu-west-2': { label: 'London', coordinates: [-0.1276, 51.5072] },
153
+ 'eu-west-3': { label: 'Paris', coordinates: [2.3522, 48.8566] },
154
+ 'eu-central-1': { label: 'Frankfurt', coordinates: [8.6821, 50.1109] },
155
+ 'eu-central-2': { label: 'Zurich', coordinates: [8.54, 47.38] },
156
+ 'eu-north-1': { label: 'Stockholm', coordinates: [18.07, 59.33] },
157
+ 'eu-south-1': { label: 'Milan', coordinates: [9.19, 45.46] },
158
+ 'eu-south-2': { label: 'Spain', coordinates: [-0.89, 41.65] },
159
+ 'ap-northeast-1': { label: 'Tokyo', coordinates: [139.69, 35.69] },
160
+ 'ap-northeast-2': { label: 'Seoul', coordinates: [126.98, 37.57] },
161
+ 'ap-northeast-3': { label: 'Osaka', coordinates: [135.5, 34.69] },
162
+ 'ap-southeast-1': { label: 'Singapore', coordinates: [103.82, 1.35] },
163
+ 'ap-southeast-2': { label: 'Sydney', coordinates: [151.21, -33.87] },
164
+ 'ap-southeast-3': { label: 'Jakarta', coordinates: [106.85, -6.21] },
165
+ 'ap-southeast-4': { label: 'Melbourne', coordinates: [144.96, -37.81] },
166
+ 'ap-south-1': { label: 'Mumbai', coordinates: [72.88, 19.08] },
167
+ 'ap-south-2': { label: 'Hyderabad', coordinates: [78.49, 17.38] },
168
+ 'ap-east-1': { label: 'Hong Kong', coordinates: [114.17, 22.32] },
169
+ 'ca-central-1': { label: 'Canada', coordinates: [-73.57, 45.5] },
170
+ 'ca-west-1': { label: 'Calgary', coordinates: [-114.07, 51.05] },
171
+ 'sa-east-1': { label: 'São Paulo', coordinates: [-46.63, -23.55] },
172
+ 'af-south-1': { label: 'Cape Town', coordinates: [18.42, -33.92] },
173
+ 'me-south-1': { label: 'Bahrain', coordinates: [50.59, 26.22] },
174
+ 'me-central-1': { label: 'UAE', coordinates: [54.37, 24.45] },
175
+ 'il-central-1': { label: 'Tel Aviv', coordinates: [34.78, 32.09] },
176
+ };
177
+ function infrastructureLocation(region) { return REGION_LOCATIONS[region]; }
178
+ function normalizeInfrastructureRegion(value) {
179
+ return typeof value === 'string' && /^[a-z]{2}(?:-[a-z]+)+-\d$/.test(value) ? value : undefined;
180
+ }
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.observeEdgeRequest = observeEdgeRequest;
4
+ const collector_js_1 = require("./collector.js");
5
+ const server_js_1 = require("./server.js");
6
+ // Share values only: Workers prohibit I/O promises/bodies crossing request contexts.
7
+ const tokens = new WeakMap();
8
+ const CONTROL = /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status|internal\/activity|auth\/service-token|cdn-cgi)(?:\/|$)/;
9
+ async function smallJson(response) {
10
+ if (!response.body)
11
+ throw new Error('Missing token response');
12
+ const reader = response.body.getReader();
13
+ const chunks = [];
14
+ let size = 0;
15
+ try {
16
+ for (;;) {
17
+ const { done, value } = await reader.read();
18
+ if (done)
19
+ break;
20
+ size += value.byteLength;
21
+ if (size > 16384) {
22
+ await reader.cancel();
23
+ throw new Error('Oversized token response');
24
+ }
25
+ chunks.push(value);
26
+ }
27
+ }
28
+ finally {
29
+ reader.releaseLock();
30
+ }
31
+ const bytes = new Uint8Array(size);
32
+ let offset = 0;
33
+ for (const chunk of chunks) {
34
+ bytes.set(chunk, offset);
35
+ offset += chunk.byteLength;
36
+ }
37
+ return JSON.parse(new TextDecoder().decode(bytes));
38
+ }
39
+ async function publish(options, events) {
40
+ const { env } = options;
41
+ const base = new URL(env.OXY_EDGE_ACTIVITY_API_URL ?? 'https://api.oxy.so');
42
+ if (base.protocol !== 'https:' || base.username || base.password || base.pathname !== '/' || base.search || base.hash)
43
+ throw new Error('Invalid activity origin');
44
+ const fetcher = options.fetcher ?? fetch;
45
+ const abort = new AbortController();
46
+ const timeout = setTimeout(() => abort.abort(), 4000);
47
+ try {
48
+ let token;
49
+ if (options.credential)
50
+ token = await Promise.race([
51
+ options.credential(),
52
+ new Promise((_, reject) => abort.signal.addEventListener('abort', () => reject(new Error('Activity credential timeout')), { once: true })),
53
+ ]);
54
+ else {
55
+ const key = env.OXY_EDGE_ACTIVITY_API_KEY;
56
+ const secret = env.OXY_EDGE_ACTIVITY_API_SECRET;
57
+ if (!key || !secret)
58
+ throw new Error('Missing edge activity credentials');
59
+ const cached = tokens.get(env);
60
+ if (cached && cached.key === key && cached.secret === secret && cached.origin === base.origin && cached.expiresAt > Date.now() + 60000)
61
+ token = cached.token;
62
+ else {
63
+ // Same exchange and expiry margin as OxyServices.getServiceToken().
64
+ const response = await fetcher(new URL('/auth/service-token', base), {
65
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
66
+ body: JSON.stringify({ apiKey: key, apiSecret: secret }), signal: abort.signal,
67
+ });
68
+ if (!response.ok) {
69
+ await response.body?.cancel();
70
+ throw new Error('Activity authentication failed');
71
+ }
72
+ const data = await smallJson(response);
73
+ if (typeof data.token !== 'string' || !data.token || typeof data.expiresIn !== 'number' || !Number.isFinite(data.expiresIn) || data.expiresIn <= 0)
74
+ throw new Error('Invalid activity token');
75
+ token = data.token;
76
+ tokens.set(env, { key, secret, origin: base.origin, token, expiresAt: Date.now() + data.expiresIn * 1000 });
77
+ }
78
+ }
79
+ const response = await fetcher(new URL('/internal/activity', base), {
80
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
81
+ body: JSON.stringify(events), signal: abort.signal,
82
+ });
83
+ if (response.status === 401)
84
+ tokens.delete(env);
85
+ await response.body?.cancel();
86
+ if (!response.ok)
87
+ throw new Error('Activity ingestion failed');
88
+ }
89
+ finally {
90
+ clearTimeout(timeout);
91
+ }
92
+ }
93
+ /** Returns the original response/stream and preserves original handler errors. */
94
+ async function observeEdgeRequest(options) {
95
+ const report = () => { try {
96
+ (options.onError ?? console.error)('Edge activity publication failed');
97
+ }
98
+ catch { /* Never break serving. */ } };
99
+ const enabled = options.env.OXY_EDGE_ACTIVITY_ENABLED;
100
+ if (enabled === undefined || enabled === 'false')
101
+ return options.next();
102
+ if (enabled !== 'true' || !/^[a-z][a-z0-9-]{0,39}$/.test(options.service)) {
103
+ report();
104
+ return options.next();
105
+ }
106
+ const path = new URL(options.request.url).pathname;
107
+ if (CONTROL.test(path))
108
+ return options.next();
109
+ const pop = (0, server_js_1.normalizeEdgePop)(typeof options.request.cf?.colo === 'string' ? options.request.cf.colo : undefined);
110
+ const region = pop ? `edge-${pop}` : 'unknown';
111
+ const startedAt = new Date().toISOString();
112
+ const complete = (response) => {
113
+ const contentType = response?.headers.get('Content-Type') ?? '';
114
+ const activityType = /^(?:image|audio|video)\//i.test(contentType) ? 'media' : (0, collector_js_1.trafficType)(path);
115
+ const common = { service: options.service, region, scope: options.peer ? 'internal' : 'external', activityType, requests: 1, windowStartedAt: startedAt, emittedAt: new Date().toISOString() };
116
+ // The serving PoP is infrastructure metadata. Do not invent visitor location.
117
+ const events = [{ ...common, direction: 'inbound', sourceRegion: options.peer?.region, sourceService: options.peer?.service, targetRegion: region, targetService: options.service }];
118
+ if (response)
119
+ events.push({ ...common, direction: 'outbound', sourceRegion: region, sourceService: options.service, targetRegion: options.peer?.region, targetService: options.peer?.service });
120
+ try {
121
+ options.ctx.waitUntil(publish(options, events).catch(report));
122
+ }
123
+ catch {
124
+ report();
125
+ }
126
+ };
127
+ let response;
128
+ try {
129
+ response = await options.next();
130
+ }
131
+ catch (error) {
132
+ complete();
133
+ throw error;
134
+ }
135
+ complete(response);
136
+ return response;
137
+ }
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.socketTrafficType = socketTrafficType;
4
+ exports.observeTrafficSocket = observeTrafficSocket;
5
+ exports.observeTrafficWebSocket = observeTrafficWebSocket;
6
+ const collector_js_1 = require("./collector.js");
7
+ const server_js_1 = require("./server.js");
8
+ const CATEGORIES = {
9
+ auth: 'identity', session: 'identity', user: 'identity', profile: 'identity',
10
+ chat: 'ai', agent: 'ai', inference: 'ai', completion: 'ai',
11
+ message: 'communication', notification: 'communication', typing: 'communication',
12
+ media: 'media', audio: 'media', video: 'media', stream: 'media', player: 'media',
13
+ track: 'media', playback: 'media', playlist: 'media', room: 'media',
14
+ };
15
+ /** Event names are reduced locally to five categories; payloads are never read. */
16
+ function socketTrafficType(event) {
17
+ return CATEGORIES[event.toLowerCase().split(/[:/._-]/, 1)[0]] ?? 'platform';
18
+ }
19
+ /**
20
+ * Observes application events only, once per local socket. Socket.IO catch-all
21
+ * hooks exclude acknowledgements, ping/pong and transport frames. Outgoing
22
+ * packets describe sends, not delivery acknowledgements. No connection ID,
23
+ * event name, IP, credential, room name or payload enters a traffic record.
24
+ */
25
+ function observeTrafficSocket(socket, collector, service, region, options = {}) {
26
+ if (/^\/platform-(?:activity|stats)(?:\/|$)/.test(socket.nsp?.name ?? ''))
27
+ return () => { };
28
+ let stopped = false;
29
+ const record = (event, direction) => {
30
+ if (stopped)
31
+ return;
32
+ const peer = options.getPeer?.();
33
+ const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
34
+ const edgePop = (0, server_js_1.metadataFromHeaders)(socket.handshake.headers).edgePop;
35
+ const peerRegion = peerService ? (0, collector_js_1.normalizeInfrastructureRegion)(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
36
+ collector.record({
37
+ region, service, scope: peerService ? 'internal' : 'external', direction,
38
+ activityType: socketTrafficType(event),
39
+ sourceRegion: direction === 'inbound' ? peerRegion : region,
40
+ targetRegion: direction === 'inbound' ? region : peerRegion,
41
+ sourceService: direction === 'inbound' ? peerService : service,
42
+ targetService: direction === 'inbound' ? service : peerService,
43
+ });
44
+ };
45
+ const inbound = (event) => record(event, 'inbound');
46
+ const outbound = (event) => record(event, 'outbound');
47
+ const stop = () => {
48
+ if (stopped)
49
+ return;
50
+ stopped = true;
51
+ socket.offAny(inbound);
52
+ socket.offAnyOutgoing(outbound);
53
+ socket.off('disconnect', stop);
54
+ };
55
+ socket.onAny(inbound);
56
+ socket.onAnyOutgoing(outbound);
57
+ socket.once('disconnect', stop);
58
+ return stop;
59
+ }
60
+ /** Raw ws application messages. Sends are attempts, never delivery receipts. */
61
+ function observeTrafficWebSocket(socket, collector, service, region, options = {}) {
62
+ let stopped = false;
63
+ const record = (direction) => {
64
+ if (stopped)
65
+ return;
66
+ const peer = options.getPeer?.();
67
+ const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
68
+ const edgePop = (0, server_js_1.metadataFromHeaders)(options.headers ?? {}).edgePop;
69
+ const peerRegion = peerService ? (0, collector_js_1.normalizeInfrastructureRegion)(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
70
+ collector.record({
71
+ service, region, scope: peerService ? 'internal' : 'external', direction,
72
+ activityType: options.activityType ?? 'communication',
73
+ sourceRegion: direction === 'inbound' ? peerRegion : region,
74
+ targetRegion: direction === 'inbound' ? region : peerRegion,
75
+ sourceService: direction === 'inbound' ? peerService : service,
76
+ targetService: direction === 'inbound' ? service : peerService,
77
+ });
78
+ };
79
+ const inbound = () => record('inbound');
80
+ const originalSend = socket.send;
81
+ const send = function (...args) {
82
+ const open = this.readyState === 1;
83
+ // Preserve overload arguments, callback identity, return value and throws.
84
+ const result = Reflect.apply(originalSend, this, args);
85
+ if (open)
86
+ record('outbound');
87
+ return result;
88
+ };
89
+ const stop = () => {
90
+ if (stopped)
91
+ return;
92
+ stopped = true;
93
+ socket.off('message', inbound);
94
+ socket.off('close', stop);
95
+ if (socket.send === send)
96
+ socket.send = originalSend;
97
+ };
98
+ socket.send = send;
99
+ socket.on('message', inbound);
100
+ socket.once('close', stop);
101
+ return stop;
102
+ }
@@ -1,6 +1,7 @@
1
1
  import { OXY_ACTIVITY_ID_HEADER, OXY_EDGE_REGION_HEADER } from './index.js';
2
2
  export const DEFAULT_ACTIVITY_ID_ROTATION_MS = 5 * 60 * 1000;
3
3
  export const DEFAULT_EDGE_TRACE_TIMEOUT_MS = 1000;
4
+ export const DEFAULT_EDGE_CACHE_MS = 15000;
4
5
  function createUuid(cryptoApi) {
5
6
  if (!cryptoApi)
6
7
  return null;
@@ -23,6 +24,7 @@ export function createBrowserTelemetry(options = {}) {
23
24
  let activityId = null;
24
25
  let activityIdCreatedAt = 0;
25
26
  let edgePopPromise = null;
27
+ let edgePopReadAt = -Infinity;
26
28
  const getActivityIdHeader = () => {
27
29
  const hasInjectedBrowser = Boolean(options.location);
28
30
  const hasBrowserRuntime = typeof window !== 'undefined' && typeof document !== 'undefined';
@@ -60,7 +62,11 @@ export function createBrowserTelemetry(options = {}) {
60
62
  }
61
63
  };
62
64
  const getEdgeRegionHeader = async () => {
63
- edgePopPromise ?? (edgePopPromise = discoverEdgePop());
65
+ const now = (options.now ?? Date.now)();
66
+ if (!edgePopPromise || now < edgePopReadAt || now - edgePopReadAt >= (options.edgeCacheMs ?? DEFAULT_EDGE_CACHE_MS)) {
67
+ edgePopReadAt = now;
68
+ edgePopPromise = discoverEdgePop();
69
+ }
64
70
  const edgePop = await edgePopPromise;
65
71
  return edgePop ? { [OXY_EDGE_REGION_HEADER]: edgePop } : {};
66
72
  };