@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.
@@ -0,0 +1,171 @@
1
+ import { metadataFromHeaders } from './server.js';
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',
8
+ };
9
+ export function trafficType(path) {
10
+ const parts = path.split('?')[0].split('/').filter(Boolean);
11
+ if (parts[0] === 'alia')
12
+ parts.shift();
13
+ if (parts[0] === 'api')
14
+ parts.shift();
15
+ if (/^v\d+$/.test(parts[0] ?? ''))
16
+ parts.shift();
17
+ return TYPES[parts[0] ?? ''] ?? 'platform';
18
+ }
19
+ export function createTrafficCollector(publish, now = Date.now) {
20
+ const pending = new Map();
21
+ let flushing = null;
22
+ return {
23
+ record(flow) {
24
+ const key = JSON.stringify(flow);
25
+ const existing = pending.get(key);
26
+ if (existing)
27
+ existing.requests++;
28
+ else if (pending.size < 512)
29
+ pending.set(key, { flow, requests: 1, startedAt: now() });
30
+ },
31
+ async flush() {
32
+ while (flushing)
33
+ await flushing;
34
+ if (pending.size === 0)
35
+ return;
36
+ const emittedAt = new Date(now()).toISOString();
37
+ const events = [...pending.values()].map(({ flow, requests, startedAt }) => ({ ...flow, requests, windowStartedAt: new Date(startedAt).toISOString(), emittedAt }));
38
+ pending.clear();
39
+ flushing = (async () => {
40
+ for (let offset = 0; offset < events.length; offset += 256)
41
+ await publish(events.slice(offset, offset + 256));
42
+ })();
43
+ try {
44
+ await flushing;
45
+ }
46
+ finally {
47
+ flushing = null;
48
+ }
49
+ },
50
+ };
51
+ }
52
+ export function trafficMiddleware(collector, service, region) {
53
+ return (request, response, next) => {
54
+ response.setHeader?.('X-Oxy-Region', region);
55
+ const path = request.path ?? request.url ?? '/';
56
+ // 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/')) {
58
+ next();
59
+ return;
60
+ }
61
+ const { edgePop } = metadataFromHeaders(request.headers);
62
+ const activityType = trafficType(path);
63
+ let finished = false;
64
+ const inboundFlow = () => {
65
+ // 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);
67
+ const internal = Boolean(name);
68
+ 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 };
71
+ };
72
+ const finish = () => {
73
+ if (finished)
74
+ return;
75
+ finished = true;
76
+ const flow = inboundFlow();
77
+ collector.record(flow);
78
+ collector.record({ ...flow, direction: 'outbound', sourceRegion: region, targetRegion: flow.sourceRegion, sourceService: service, targetService: flow.sourceService });
79
+ };
80
+ response.once('finish', finish);
81
+ // Failed/aborted requests are still real inbound activity. Do not invent a
82
+ // completed outbound response when the connection closes prematurely.
83
+ response.once('close', () => {
84
+ if (finished)
85
+ return;
86
+ finished = true;
87
+ collector.record(inboundFlow());
88
+ });
89
+ next();
90
+ };
91
+ }
92
+ export function instrumentTrafficFetch(fetcher, collector, service, region, resolveEndpoint) {
93
+ return Object.assign(async (input, init) => {
94
+ const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
95
+ // 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')
97
+ return fetcher(input, init);
98
+ const peer = resolveEndpoint(url);
99
+ 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,
103
+ };
104
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
105
+ if (peer)
106
+ headers.set('X-Oxy-Source-Region', region);
107
+ let response;
108
+ try {
109
+ response = await fetcher(input, { ...init, headers });
110
+ }
111
+ catch (error) {
112
+ collector.record({ ...flow, targetRegion: undefined });
113
+ throw error;
114
+ }
115
+ const targetRegion = peer ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined) : undefined;
116
+ collector.record({ ...flow, targetRegion });
117
+ collector.record({ ...flow, direction: 'inbound', sourceRegion: targetRegion, targetRegion: region, sourceService: peer?.service, targetService: service });
118
+ return response;
119
+ }, fetcher);
120
+ }
121
+ // Exact deployed API hosts; an arbitrary caller-supplied hostname cannot claim
122
+ // to be an internal service. Region is infrastructure metadata, never IP geo.
123
+ 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',
132
+ };
133
+ export function resolveOxyServiceEndpoint(url) {
134
+ const service = OXY_API_SERVICES[url.hostname];
135
+ return url.protocol === 'https:' && service ? { service, region: 'us-west-2' } : undefined;
136
+ }
137
+ const REGION_LOCATIONS = {
138
+ 'us-west-2': { label: 'Oregon', coordinates: [-122.6765, 45.5231] },
139
+ 'us-west-1': { label: 'Northern California', coordinates: [-121.89, 37.34] },
140
+ 'us-east-1': { label: 'Northern Virginia', coordinates: [-77.49, 39.04] },
141
+ 'us-east-2': { label: 'Ohio', coordinates: [-82.99, 39.96] },
142
+ 'eu-west-1': { label: 'Ireland', coordinates: [-6.26, 53.35] },
143
+ 'eu-west-2': { label: 'London', coordinates: [-0.1276, 51.5072] },
144
+ 'eu-west-3': { label: 'Paris', coordinates: [2.3522, 48.8566] },
145
+ 'eu-central-1': { label: 'Frankfurt', coordinates: [8.6821, 50.1109] },
146
+ 'eu-central-2': { label: 'Zurich', coordinates: [8.54, 47.38] },
147
+ 'eu-north-1': { label: 'Stockholm', coordinates: [18.07, 59.33] },
148
+ 'eu-south-1': { label: 'Milan', coordinates: [9.19, 45.46] },
149
+ 'eu-south-2': { label: 'Spain', coordinates: [-0.89, 41.65] },
150
+ 'ap-northeast-1': { label: 'Tokyo', coordinates: [139.69, 35.69] },
151
+ 'ap-northeast-2': { label: 'Seoul', coordinates: [126.98, 37.57] },
152
+ 'ap-northeast-3': { label: 'Osaka', coordinates: [135.5, 34.69] },
153
+ 'ap-southeast-1': { label: 'Singapore', coordinates: [103.82, 1.35] },
154
+ 'ap-southeast-2': { label: 'Sydney', coordinates: [151.21, -33.87] },
155
+ 'ap-southeast-3': { label: 'Jakarta', coordinates: [106.85, -6.21] },
156
+ 'ap-southeast-4': { label: 'Melbourne', coordinates: [144.96, -37.81] },
157
+ 'ap-south-1': { label: 'Mumbai', coordinates: [72.88, 19.08] },
158
+ 'ap-south-2': { label: 'Hyderabad', coordinates: [78.49, 17.38] },
159
+ 'ap-east-1': { label: 'Hong Kong', coordinates: [114.17, 22.32] },
160
+ 'ca-central-1': { label: 'Canada', coordinates: [-73.57, 45.5] },
161
+ 'ca-west-1': { label: 'Calgary', coordinates: [-114.07, 51.05] },
162
+ 'sa-east-1': { label: 'São Paulo', coordinates: [-46.63, -23.55] },
163
+ 'af-south-1': { label: 'Cape Town', coordinates: [18.42, -33.92] },
164
+ 'me-south-1': { label: 'Bahrain', coordinates: [50.59, 26.22] },
165
+ 'me-central-1': { label: 'UAE', coordinates: [54.37, 24.45] },
166
+ 'il-central-1': { label: 'Tel Aviv', coordinates: [34.78, 32.09] },
167
+ };
168
+ export function infrastructureLocation(region) { return REGION_LOCATIONS[region]; }
169
+ export function normalizeInfrastructureRegion(value) {
170
+ return typeof value === 'string' && /^[a-z]{2}(?:-[a-z]+)+-\d$/.test(value) ? value : undefined;
171
+ }
@@ -0,0 +1,134 @@
1
+ import { trafficType } from './collector.js';
2
+ import { normalizeEdgePop } from './server.js';
3
+ // Share values only: Workers prohibit I/O promises/bodies crossing request contexts.
4
+ const tokens = new WeakMap();
5
+ const CONTROL = /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status|internal\/activity|auth\/service-token|cdn-cgi)(?:\/|$)/;
6
+ async function smallJson(response) {
7
+ if (!response.body)
8
+ throw new Error('Missing token response');
9
+ const reader = response.body.getReader();
10
+ const chunks = [];
11
+ let size = 0;
12
+ try {
13
+ for (;;) {
14
+ const { done, value } = await reader.read();
15
+ if (done)
16
+ break;
17
+ size += value.byteLength;
18
+ if (size > 16384) {
19
+ await reader.cancel();
20
+ throw new Error('Oversized token response');
21
+ }
22
+ chunks.push(value);
23
+ }
24
+ }
25
+ finally {
26
+ reader.releaseLock();
27
+ }
28
+ const bytes = new Uint8Array(size);
29
+ let offset = 0;
30
+ for (const chunk of chunks) {
31
+ bytes.set(chunk, offset);
32
+ offset += chunk.byteLength;
33
+ }
34
+ return JSON.parse(new TextDecoder().decode(bytes));
35
+ }
36
+ async function publish(options, events) {
37
+ const { env } = options;
38
+ const base = new URL(env.OXY_EDGE_ACTIVITY_API_URL ?? 'https://api.oxy.so');
39
+ if (base.protocol !== 'https:' || base.username || base.password || base.pathname !== '/' || base.search || base.hash)
40
+ throw new Error('Invalid activity origin');
41
+ const fetcher = options.fetcher ?? fetch;
42
+ const abort = new AbortController();
43
+ const timeout = setTimeout(() => abort.abort(), 4000);
44
+ try {
45
+ let token;
46
+ if (options.credential)
47
+ token = await Promise.race([
48
+ options.credential(),
49
+ new Promise((_, reject) => abort.signal.addEventListener('abort', () => reject(new Error('Activity credential timeout')), { once: true })),
50
+ ]);
51
+ else {
52
+ const key = env.OXY_EDGE_ACTIVITY_API_KEY;
53
+ const secret = env.OXY_EDGE_ACTIVITY_API_SECRET;
54
+ if (!key || !secret)
55
+ throw new Error('Missing edge activity credentials');
56
+ const cached = tokens.get(env);
57
+ if (cached && cached.key === key && cached.secret === secret && cached.origin === base.origin && cached.expiresAt > Date.now() + 60000)
58
+ token = cached.token;
59
+ else {
60
+ // Same exchange and expiry margin as OxyServices.getServiceToken().
61
+ const response = await fetcher(new URL('/auth/service-token', base), {
62
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ apiKey: key, apiSecret: secret }), signal: abort.signal,
64
+ });
65
+ if (!response.ok) {
66
+ await response.body?.cancel();
67
+ throw new Error('Activity authentication failed');
68
+ }
69
+ const data = await smallJson(response);
70
+ if (typeof data.token !== 'string' || !data.token || typeof data.expiresIn !== 'number' || !Number.isFinite(data.expiresIn) || data.expiresIn <= 0)
71
+ throw new Error('Invalid activity token');
72
+ token = data.token;
73
+ tokens.set(env, { key, secret, origin: base.origin, token, expiresAt: Date.now() + data.expiresIn * 1000 });
74
+ }
75
+ }
76
+ const response = await fetcher(new URL('/internal/activity', base), {
77
+ method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
78
+ body: JSON.stringify(events), signal: abort.signal,
79
+ });
80
+ if (response.status === 401)
81
+ tokens.delete(env);
82
+ await response.body?.cancel();
83
+ if (!response.ok)
84
+ throw new Error('Activity ingestion failed');
85
+ }
86
+ finally {
87
+ clearTimeout(timeout);
88
+ }
89
+ }
90
+ /** Returns the original response/stream and preserves original handler errors. */
91
+ export async function observeEdgeRequest(options) {
92
+ const report = () => { try {
93
+ (options.onError ?? console.error)('Edge activity publication failed');
94
+ }
95
+ catch { /* Never break serving. */ } };
96
+ const enabled = options.env.OXY_EDGE_ACTIVITY_ENABLED;
97
+ if (enabled === undefined || enabled === 'false')
98
+ return options.next();
99
+ if (enabled !== 'true' || !/^[a-z][a-z0-9-]{0,39}$/.test(options.service)) {
100
+ report();
101
+ return options.next();
102
+ }
103
+ const path = new URL(options.request.url).pathname;
104
+ if (CONTROL.test(path))
105
+ return options.next();
106
+ const pop = normalizeEdgePop(typeof options.request.cf?.colo === 'string' ? options.request.cf.colo : undefined);
107
+ const region = pop ? `edge-${pop}` : 'unknown';
108
+ const startedAt = new Date().toISOString();
109
+ const complete = (response) => {
110
+ const contentType = response?.headers.get('Content-Type') ?? '';
111
+ const activityType = /^(?:image|audio|video)\//i.test(contentType) ? 'media' : trafficType(path);
112
+ const common = { service: options.service, region, scope: options.peer ? 'internal' : 'external', activityType, requests: 1, windowStartedAt: startedAt, emittedAt: new Date().toISOString() };
113
+ // The serving PoP is infrastructure metadata. Do not invent visitor location.
114
+ const events = [{ ...common, direction: 'inbound', sourceRegion: options.peer?.region, sourceService: options.peer?.service, targetRegion: region, targetService: options.service }];
115
+ if (response)
116
+ events.push({ ...common, direction: 'outbound', sourceRegion: region, sourceService: options.service, targetRegion: options.peer?.region, targetService: options.peer?.service });
117
+ try {
118
+ options.ctx.waitUntil(publish(options, events).catch(report));
119
+ }
120
+ catch {
121
+ report();
122
+ }
123
+ };
124
+ let response;
125
+ try {
126
+ response = await options.next();
127
+ }
128
+ catch (error) {
129
+ complete();
130
+ throw error;
131
+ }
132
+ complete(response);
133
+ return response;
134
+ }
@@ -0,0 +1,97 @@
1
+ import { normalizeInfrastructureRegion } from './collector.js';
2
+ import { metadataFromHeaders } from './server.js';
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',
9
+ };
10
+ /** Event names are reduced locally to five categories; payloads are never read. */
11
+ export function socketTrafficType(event) {
12
+ return CATEGORIES[event.toLowerCase().split(/[:/._-]/, 1)[0]] ?? 'platform';
13
+ }
14
+ /**
15
+ * Observes application events only, once per local socket. Socket.IO catch-all
16
+ * hooks exclude acknowledgements, ping/pong and transport frames. Outgoing
17
+ * packets describe sends, not delivery acknowledgements. No connection ID,
18
+ * event name, IP, credential, room name or payload enters a traffic record.
19
+ */
20
+ export function observeTrafficSocket(socket, collector, service, region, options = {}) {
21
+ if (/^\/platform-(?:activity|stats)(?:\/|$)/.test(socket.nsp?.name ?? ''))
22
+ return () => { };
23
+ let stopped = false;
24
+ const record = (event, direction) => {
25
+ if (stopped)
26
+ return;
27
+ const peer = options.getPeer?.();
28
+ const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
29
+ const edgePop = metadataFromHeaders(socket.handshake.headers).edgePop;
30
+ const peerRegion = peerService ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
31
+ collector.record({
32
+ region, service, scope: peerService ? 'internal' : 'external', direction,
33
+ activityType: socketTrafficType(event),
34
+ sourceRegion: direction === 'inbound' ? peerRegion : region,
35
+ targetRegion: direction === 'inbound' ? region : peerRegion,
36
+ sourceService: direction === 'inbound' ? peerService : service,
37
+ targetService: direction === 'inbound' ? service : peerService,
38
+ });
39
+ };
40
+ const inbound = (event) => record(event, 'inbound');
41
+ const outbound = (event) => record(event, 'outbound');
42
+ const stop = () => {
43
+ if (stopped)
44
+ return;
45
+ stopped = true;
46
+ socket.offAny(inbound);
47
+ socket.offAnyOutgoing(outbound);
48
+ socket.off('disconnect', stop);
49
+ };
50
+ socket.onAny(inbound);
51
+ socket.onAnyOutgoing(outbound);
52
+ socket.once('disconnect', stop);
53
+ return stop;
54
+ }
55
+ /** Raw ws application messages. Sends are attempts, never delivery receipts. */
56
+ export function observeTrafficWebSocket(socket, collector, service, region, options = {}) {
57
+ let stopped = false;
58
+ const record = (direction) => {
59
+ if (stopped)
60
+ return;
61
+ const peer = options.getPeer?.();
62
+ const peerService = peer && /^[a-z][a-z0-9-]{0,39}$/.test(peer.service) ? peer.service : undefined;
63
+ const edgePop = metadataFromHeaders(options.headers ?? {}).edgePop;
64
+ const peerRegion = peerService ? normalizeInfrastructureRegion(peer?.region) : edgePop ? `edge-${edgePop}` : undefined;
65
+ collector.record({
66
+ service, region, scope: peerService ? 'internal' : 'external', direction,
67
+ activityType: options.activityType ?? 'communication',
68
+ sourceRegion: direction === 'inbound' ? peerRegion : region,
69
+ targetRegion: direction === 'inbound' ? region : peerRegion,
70
+ sourceService: direction === 'inbound' ? peerService : service,
71
+ targetService: direction === 'inbound' ? service : peerService,
72
+ });
73
+ };
74
+ const inbound = () => record('inbound');
75
+ const originalSend = socket.send;
76
+ const send = function (...args) {
77
+ const open = this.readyState === 1;
78
+ // Preserve overload arguments, callback identity, return value and throws.
79
+ const result = Reflect.apply(originalSend, this, args);
80
+ if (open)
81
+ record('outbound');
82
+ return result;
83
+ };
84
+ const stop = () => {
85
+ if (stopped)
86
+ return;
87
+ stopped = true;
88
+ socket.off('message', inbound);
89
+ socket.off('close', stop);
90
+ if (socket.send === send)
91
+ socket.send = originalSend;
92
+ };
93
+ socket.send = send;
94
+ socket.on('message', inbound);
95
+ socket.once('close', stop);
96
+ return stop;
97
+ }
@@ -1,5 +1,6 @@
1
1
  export declare const DEFAULT_ACTIVITY_ID_ROTATION_MS: number;
2
2
  export declare const DEFAULT_EDGE_TRACE_TIMEOUT_MS = 1000;
3
+ export declare const DEFAULT_EDGE_CACHE_MS = 15000;
3
4
  interface TraceResponse {
4
5
  ok: boolean;
5
6
  text(): Promise<string>;
@@ -7,6 +8,7 @@ interface TraceResponse {
7
8
  export interface BrowserTelemetryOptions {
8
9
  activityIdRotationMs?: number;
9
10
  edgeTraceTimeoutMs?: number;
11
+ edgeCacheMs?: number;
10
12
  now?: () => number;
11
13
  crypto?: Pick<Crypto, 'getRandomValues' | 'randomUUID'>;
12
14
  location?: Pick<Location, 'hostname' | 'protocol'>;
@@ -0,0 +1,50 @@
1
+ import { type TelemetryHeaders } from './server.js';
2
+ export type TrafficScope = 'internal' | 'external';
3
+ export type TrafficDirection = 'inbound' | 'outbound';
4
+ export type TrafficType = 'identity' | 'ai' | 'communication' | 'media' | 'platform';
5
+ export interface TrafficFlow {
6
+ region: string;
7
+ sourceRegion?: string;
8
+ targetRegion?: string;
9
+ sourceService?: string;
10
+ targetService?: string;
11
+ service: string;
12
+ scope: TrafficScope;
13
+ direction: TrafficDirection;
14
+ activityType: TrafficType;
15
+ }
16
+ export interface TrafficAggregate extends TrafficFlow {
17
+ requests: number;
18
+ windowStartedAt: string;
19
+ emittedAt: string;
20
+ }
21
+ export declare function trafficType(path: string): TrafficType;
22
+ export declare function createTrafficCollector(publish: (events: TrafficAggregate[]) => Promise<unknown>, now?: () => number): {
23
+ record(flow: TrafficFlow): void;
24
+ flush(): Promise<void>;
25
+ };
26
+ interface HttpRequest {
27
+ headers: TelemetryHeaders;
28
+ path?: string;
29
+ url?: string;
30
+ serviceApp?: {
31
+ appName?: string;
32
+ };
33
+ }
34
+ interface HttpResponse {
35
+ once(event: string, listener: () => void): unknown;
36
+ setHeader?(name: string, value: string): unknown;
37
+ }
38
+ export declare function trafficMiddleware(collector: Pick<ReturnType<typeof createTrafficCollector>, 'record'>, service: string, region: string): (request: HttpRequest, response: HttpResponse, next: () => void) => void;
39
+ export interface ServiceEndpoint {
40
+ service: string;
41
+ region: string;
42
+ }
43
+ export declare function instrumentTrafficFetch(fetcher: typeof fetch, collector: Pick<ReturnType<typeof createTrafficCollector>, 'record'>, service: string, region: string, resolveEndpoint: (url: URL) => ServiceEndpoint | undefined): typeof fetch;
44
+ export declare function resolveOxyServiceEndpoint(url: URL): ServiceEndpoint | undefined;
45
+ export declare function infrastructureLocation(region: string): {
46
+ label: string;
47
+ coordinates: [number, number];
48
+ };
49
+ export declare function normalizeInfrastructureRegion(value: unknown): string | undefined;
50
+ export {};
@@ -0,0 +1,32 @@
1
+ /** Server-only bindings; never expose them through frontend build variables. */
2
+ export interface EdgeActivityEnv {
3
+ OXY_EDGE_ACTIVITY_ENABLED?: string;
4
+ OXY_EDGE_ACTIVITY_API_KEY?: string;
5
+ OXY_EDGE_ACTIVITY_API_SECRET?: string;
6
+ OXY_EDGE_ACTIVITY_API_URL?: string;
7
+ }
8
+ export interface EdgeActivityContext {
9
+ waitUntil(promise: Promise<unknown>): void;
10
+ }
11
+ export interface EdgeActivityOptions {
12
+ service: string;
13
+ request: Request & {
14
+ cf?: {
15
+ colo?: unknown;
16
+ };
17
+ };
18
+ env: EdgeActivityEnv;
19
+ ctx: EdgeActivityContext;
20
+ next: () => Response | Promise<Response>;
21
+ credential?: () => Promise<string>;
22
+ fetcher?: typeof fetch;
23
+ /** Only supply after existing authentication verified this service identity. */
24
+ peer?: {
25
+ service: string;
26
+ region?: string;
27
+ };
28
+ /** Fixed message only: never credentials, URLs or upstream error bodies. */
29
+ onError?: (message: string) => void;
30
+ }
31
+ /** Returns the original response/stream and preserves original handler errors. */
32
+ export declare function observeEdgeRequest(options: EdgeActivityOptions): Promise<Response>;
@@ -0,0 +1,50 @@
1
+ import { type TrafficFlow, type TrafficType } from './collector.js';
2
+ import { type TelemetryHeaders } from './server.js';
3
+ export interface TrafficSocket {
4
+ handshake: {
5
+ headers: TelemetryHeaders;
6
+ };
7
+ nsp?: {
8
+ name: string;
9
+ };
10
+ onAny(listener: (event: string) => void): unknown;
11
+ onAnyOutgoing(listener: (event: string) => void): unknown;
12
+ offAny(listener: (event: string) => void): unknown;
13
+ offAnyOutgoing(listener: (event: string) => void): unknown;
14
+ once(event: 'disconnect', listener: () => void): unknown;
15
+ off(event: 'disconnect', listener: () => void): unknown;
16
+ }
17
+ /** Event names are reduced locally to five categories; payloads are never read. */
18
+ export declare function socketTrafficType(event: string): TrafficType;
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
+ export declare function observeTrafficSocket(socket: TrafficSocket, collector: {
26
+ record(flow: TrafficFlow): void;
27
+ }, service: string, region: string, options?: {
28
+ getPeer?: () => {
29
+ service: string;
30
+ region?: string;
31
+ } | undefined;
32
+ }): () => void;
33
+ export interface TrafficWebSocket {
34
+ readonly readyState: number;
35
+ send(...args: any[]): unknown;
36
+ on(event: 'message', listener: () => void): unknown;
37
+ once(event: 'close', listener: () => void): unknown;
38
+ off(event: 'message' | 'close', listener: () => void): unknown;
39
+ }
40
+ /** Raw ws application messages. Sends are attempts, never delivery receipts. */
41
+ export declare function observeTrafficWebSocket(socket: TrafficWebSocket, collector: {
42
+ record(flow: TrafficFlow): void;
43
+ }, service: string, region: string, options?: {
44
+ headers?: TelemetryHeaders;
45
+ activityType?: TrafficType;
46
+ getPeer?: () => {
47
+ service: string;
48
+ region?: string;
49
+ } | undefined;
50
+ }): () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/telemetry",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Framework-agnostic anonymous activity telemetry primitives for Oxy services.",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -13,6 +13,15 @@
13
13
  ],
14
14
  "server": [
15
15
  "dist/types/server.d.ts"
16
+ ],
17
+ "collector": [
18
+ "dist/types/collector.d.ts"
19
+ ],
20
+ "socket": [
21
+ "dist/types/socket.d.ts"
22
+ ],
23
+ "edge": [
24
+ "dist/types/edge.d.ts"
16
25
  ]
17
26
  }
18
27
  },
@@ -62,7 +71,37 @@
62
71
  "default": "./dist/cjs/server.js"
63
72
  }
64
73
  },
65
- "./package.json": "./package.json"
74
+ "./package.json": "./package.json",
75
+ "./collector": {
76
+ "import": {
77
+ "types": "./dist/types/collector.d.ts",
78
+ "default": "./dist/esm/collector.js"
79
+ },
80
+ "require": {
81
+ "types": "./dist/types/collector.d.ts",
82
+ "default": "./dist/cjs/collector.js"
83
+ }
84
+ },
85
+ "./socket": {
86
+ "import": {
87
+ "types": "./dist/types/socket.d.ts",
88
+ "default": "./dist/esm/socket.js"
89
+ },
90
+ "require": {
91
+ "types": "./dist/types/socket.d.ts",
92
+ "default": "./dist/cjs/socket.js"
93
+ }
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
+ }
66
105
  },
67
106
  "files": [
68
107
  "dist",
@@ -44,4 +44,25 @@ describe('browser telemetry', () => {
44
44
  await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({});
45
45
  expect(fetchTrace).not.toHaveBeenCalled();
46
46
  });
47
+ it('rediscovers the edge after a VPN change without reloading the page', async () => {
48
+ let now = 1_000;
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 });
52
+ await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({ 'X-Oxy-Edge-Region': 'mad' });
53
+ edge = 'NRT';
54
+ now += 15_000;
55
+ await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({ 'X-Oxy-Edge-Region': 'nrt' });
56
+ expect(fetchTrace).toHaveBeenCalledTimes(2);
57
+ });
58
+
59
+ it('retries failed discovery on the next window instead of caching failure for the session', async () => {
60
+ 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 });
63
+ await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({});
64
+ now += 15_000;
65
+ await expect(telemetry.getEdgeRegionHeader()).resolves.toEqual({ 'X-Oxy-Edge-Region': 'syd' });
66
+ });
67
+
47
68
  });