@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.
- package/LICENSE +676 -0
- package/README.md +6 -3
- package/dist/cjs/browser.js +46 -8
- package/dist/cjs/collector.js +111 -26
- package/dist/cjs/server.js +3 -1
- package/dist/cjs/socket.js +38 -9
- package/dist/esm/browser.js +44 -7
- package/dist/esm/collector.js +111 -26
- package/dist/esm/server.js +3 -1
- package/dist/esm/socket.js +38 -9
- package/dist/types/browser.d.ts +14 -0
- package/package.json +5 -17
- package/src/__tests__/browser.test.ts +42 -4
- package/src/__tests__/collector.test.ts +132 -27
- package/src/__tests__/server.test.ts +7 -5
- package/src/__tests__/socket.test.ts +80 -29
- package/src/browser.ts +71 -10
- package/src/collector.ts +152 -35
- package/src/server.ts +8 -2
- package/src/socket.ts +43 -11
- package/dist/cjs/edge.js +0 -137
- package/dist/esm/edge.js +0 -134
- package/dist/types/edge.d.ts +0 -32
- package/src/__tests__/edge.test.ts +0 -110
- package/src/edge.ts +0 -117
package/dist/types/edge.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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>;
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/** @jest-environment node */
|
|
2
|
-
import { observeEdgeRequest, type EdgeActivityEnv } from '../edge';
|
|
3
|
-
|
|
4
|
-
function setup(path = '/private-name.png?email=secret@example.com') {
|
|
5
|
-
const request = new Request(`https://example.com${path}`, { headers: { 'CF-Connecting-IP': '203.0.113.8', 'X-Oxy-Source-Region': 'eu-west-1', Authorization: 'private' } });
|
|
6
|
-
Object.assign(request, { cf: { colo: 'MAD', country: 'ES' } });
|
|
7
|
-
const tasks: Promise<unknown>[] = [];
|
|
8
|
-
const env: EdgeActivityEnv = { OXY_EDGE_ACTIVITY_ENABLED: 'true', OXY_EDGE_ACTIVITY_API_KEY: 'key', OXY_EDGE_ACTIVITY_API_SECRET: 'secret' };
|
|
9
|
-
const fetcher = jest.fn(async (input: RequestInfo | URL) => String(input).endsWith('service-token') ? Response.json({ token: 'token', expiresIn: 3600 }) : new Response(null, { status: 204 }));
|
|
10
|
-
return { request, env, tasks, fetcher, ctx: { waitUntil: (task: Promise<unknown>) => tasks.push(task) }, service: 'noted', onError: jest.fn() };
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
test('preserves the response stream, cache headers and media type without personal metadata', async () => {
|
|
14
|
-
const state = setup();
|
|
15
|
-
const response = new Response(new ReadableStream({ start(c) { c.enqueue(new TextEncoder().encode('image')); c.close(); } }), { headers: { 'Content-Type': 'image/png', 'Cache-Control': 'public,max-age=3600' } });
|
|
16
|
-
expect(await observeEdgeRequest({ ...state, next: () => response })).toBe(response);
|
|
17
|
-
expect(response.bodyUsed).toBe(false);
|
|
18
|
-
await Promise.all(state.tasks);
|
|
19
|
-
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
20
|
-
expect(JSON.parse(String(calls[0][1].body))).toEqual({ apiKey: 'key', apiSecret: 'secret' });
|
|
21
|
-
const events = JSON.parse(String(calls[1][1].body));
|
|
22
|
-
expect(events).toHaveLength(2);
|
|
23
|
-
expect(events[0]).toMatchObject({ region: 'edge-mad', scope: 'external', direction: 'inbound', activityType: 'media', targetRegion: 'edge-mad' });
|
|
24
|
-
expect(events[1]).toMatchObject({ direction: 'outbound', sourceRegion: 'edge-mad' });
|
|
25
|
-
expect(events[0].sourceRegion).toBeUndefined();
|
|
26
|
-
expect(JSON.stringify(events)).not.toMatch(/private|secret|203\.0|example|email|eu-west/);
|
|
27
|
-
expect(await response.text()).toBe('image');
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test('preserves handler errors and only reports received activity', async () => {
|
|
31
|
-
const state = setup(); const error = new Error('upstream');
|
|
32
|
-
await expect(observeEdgeRequest({ ...state, next: () => { throw error; } })).rejects.toBe(error);
|
|
33
|
-
await Promise.all(state.tasks);
|
|
34
|
-
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
35
|
-
expect(JSON.parse(String(calls[1][1].body)).map((x: {direction:string}) => x.direction)).toEqual(['inbound']);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
test.each(['/health', '/api/ready', '/internal/activity', '/auth/service-token', '/cdn-cgi/trace'])('excludes control route %s', async (path) => {
|
|
39
|
-
const state = setup(path); const response = new Response(null);
|
|
40
|
-
expect(await observeEdgeRequest({ ...state, next: () => response })).toBe(response);
|
|
41
|
-
expect(state.tasks).toHaveLength(0);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('disabled bindings bypass publishing and missing enabled credentials report a fixed error', async () => {
|
|
45
|
-
const state = setup(); state.env = {};
|
|
46
|
-
await observeEdgeRequest({ ...state, next: () => new Response(null) });
|
|
47
|
-
expect(state.tasks).toHaveLength(0);
|
|
48
|
-
state.env.OXY_EDGE_ACTIVITY_ENABLED = 'true';
|
|
49
|
-
await observeEdgeRequest({ ...state, next: () => new Response(null) });
|
|
50
|
-
await Promise.all(state.tasks);
|
|
51
|
-
expect(state.onError).toHaveBeenCalledWith('Edge activity publication failed');
|
|
52
|
-
expect(state.fetcher).not.toHaveBeenCalled();
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('verified peer is internal independently of inbound/outbound', async () => {
|
|
56
|
-
const state = setup();
|
|
57
|
-
await observeEdgeRequest({ ...state, peer: {service:'mention', region:'us-west-2'}, next: () => new Response(null) });
|
|
58
|
-
await Promise.all(state.tasks);
|
|
59
|
-
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
60
|
-
expect(JSON.parse(String(calls[1][1].body))).toEqual([
|
|
61
|
-
expect.objectContaining({scope:'internal', direction:'inbound', sourceRegion:'us-west-2', sourceService:'mention'}),
|
|
62
|
-
expect.objectContaining({scope:'internal', direction:'outbound', targetRegion:'us-west-2', targetService:'mention'}),
|
|
63
|
-
]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test('caches token values and invalidates on ingestion 401 without retrying a batch', async () => {
|
|
67
|
-
const state = setup();
|
|
68
|
-
const fetcher = jest.fn(async (input: RequestInfo | URL) => String(input).endsWith('service-token') ? Response.json({token:'token',expiresIn:3600}) : new Response(null, {status:401}));
|
|
69
|
-
for (let i=0;i<2;i++) { await observeEdgeRequest({...state, fetcher, next:()=>new Response(null)}); await Promise.all(state.tasks); }
|
|
70
|
-
expect(fetcher.mock.calls.map(([url])=>String(url))).toEqual(['https://api.oxy.so/auth/service-token','https://api.oxy.so/internal/activity','https://api.oxy.so/auth/service-token','https://api.oxy.so/internal/activity']);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('unknown colo stays unknown even when caller spoofs geo headers', async () => {
|
|
74
|
-
const state = setup(); Object.assign(state.request, {cf:undefined});
|
|
75
|
-
await observeEdgeRequest({...state, credential:async()=>'injected', next:()=>new Response(null)}); await Promise.all(state.tasks);
|
|
76
|
-
const calls = state.fetcher.mock.calls as unknown as [URL, RequestInit][];
|
|
77
|
-
expect(calls).toHaveLength(1);
|
|
78
|
-
expect(JSON.parse(String(calls[0][1].body))[0].region).toBe('unknown');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test('reuses only a token value on later requests and refreshes after credential rotation', async () => {
|
|
82
|
-
const state = setup();
|
|
83
|
-
for (let i=0;i<2;i++) { await observeEdgeRequest({...state,next:()=>new Response(null)}); await Promise.all(state.tasks); }
|
|
84
|
-
expect(state.fetcher.mock.calls).toHaveLength(3);
|
|
85
|
-
state.env.OXY_EDGE_ACTIVITY_API_SECRET = 'rotated';
|
|
86
|
-
await observeEdgeRequest({...state,next:()=>new Response(null)}); await Promise.all(state.tasks);
|
|
87
|
-
expect(state.fetcher.mock.calls).toHaveLength(5);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('bounds an injected credential that never resolves without delaying the response', async () => {
|
|
91
|
-
jest.useFakeTimers();
|
|
92
|
-
try {
|
|
93
|
-
const state = setup(); const response = new Response(null);
|
|
94
|
-
expect(await observeEdgeRequest({...state,credential:()=>new Promise(()=>{}),next:()=>response})).toBe(response);
|
|
95
|
-
await jest.advanceTimersByTimeAsync(4_000);
|
|
96
|
-
await Promise.all(state.tasks);
|
|
97
|
-
expect(state.onError).toHaveBeenCalledTimes(1);
|
|
98
|
-
expect(state.fetcher).not.toHaveBeenCalled();
|
|
99
|
-
expect(jest.getTimerCount()).toBe(0);
|
|
100
|
-
} finally { jest.useRealTimers(); }
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
test('bounds token response size and never publishes credentials in diagnostics', async () => {
|
|
104
|
-
const state = setup();
|
|
105
|
-
const fetcher = jest.fn(async()=>new Response('secret'.repeat(4000)));
|
|
106
|
-
await observeEdgeRequest({...state,fetcher,next:()=>new Response(null)});
|
|
107
|
-
await Promise.all(state.tasks);
|
|
108
|
-
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
109
|
-
expect(state.onError).toHaveBeenCalledWith('Edge activity publication failed');
|
|
110
|
-
});
|
package/src/edge.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { trafficType, type TrafficAggregate } from './collector.js';
|
|
2
|
-
import { normalizeEdgePop } from './server.js';
|
|
3
|
-
|
|
4
|
-
/** Server-only bindings; never expose them through frontend build variables. */
|
|
5
|
-
export interface EdgeActivityEnv {
|
|
6
|
-
OXY_EDGE_ACTIVITY_ENABLED?: string;
|
|
7
|
-
OXY_EDGE_ACTIVITY_API_KEY?: string;
|
|
8
|
-
OXY_EDGE_ACTIVITY_API_SECRET?: string;
|
|
9
|
-
OXY_EDGE_ACTIVITY_API_URL?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface EdgeActivityContext { waitUntil(promise: Promise<unknown>): void }
|
|
12
|
-
export interface EdgeActivityOptions {
|
|
13
|
-
service: string;
|
|
14
|
-
request: Request & { cf?: { colo?: unknown } };
|
|
15
|
-
env: EdgeActivityEnv;
|
|
16
|
-
ctx: EdgeActivityContext;
|
|
17
|
-
next: () => Response | Promise<Response>;
|
|
18
|
-
credential?: () => Promise<string>;
|
|
19
|
-
fetcher?: typeof fetch;
|
|
20
|
-
/** Only supply after existing authentication verified this service identity. */
|
|
21
|
-
peer?: { service: string; region?: string };
|
|
22
|
-
/** Fixed message only: never credentials, URLs or upstream error bodies. */
|
|
23
|
-
onError?: (message: string) => void;
|
|
24
|
-
}
|
|
25
|
-
interface CachedToken { key: string; secret: string; origin: string; token: string; expiresAt: number }
|
|
26
|
-
// Share values only: Workers prohibit I/O promises/bodies crossing request contexts.
|
|
27
|
-
const tokens = new WeakMap<EdgeActivityEnv, CachedToken>();
|
|
28
|
-
const CONTROL = /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status|internal\/activity|auth\/service-token|cdn-cgi)(?:\/|$)/;
|
|
29
|
-
|
|
30
|
-
async function smallJson(response: Response): Promise<unknown> {
|
|
31
|
-
if (!response.body) throw new Error('Missing token response');
|
|
32
|
-
const reader = response.body.getReader();
|
|
33
|
-
const chunks: Uint8Array[] = [];
|
|
34
|
-
let size = 0;
|
|
35
|
-
try {
|
|
36
|
-
for (;;) {
|
|
37
|
-
const { done, value } = await reader.read();
|
|
38
|
-
if (done) break;
|
|
39
|
-
size += value.byteLength;
|
|
40
|
-
if (size > 16_384) { await reader.cancel(); throw new Error('Oversized token response'); }
|
|
41
|
-
chunks.push(value);
|
|
42
|
-
}
|
|
43
|
-
} finally { reader.releaseLock(); }
|
|
44
|
-
const bytes = new Uint8Array(size);
|
|
45
|
-
let offset = 0;
|
|
46
|
-
for (const chunk of chunks) { bytes.set(chunk, offset); offset += chunk.byteLength; }
|
|
47
|
-
return JSON.parse(new TextDecoder().decode(bytes));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function publish(options: EdgeActivityOptions, events: TrafficAggregate[]): Promise<void> {
|
|
51
|
-
const { env } = options;
|
|
52
|
-
const base = new URL(env.OXY_EDGE_ACTIVITY_API_URL ?? 'https://api.oxy.so');
|
|
53
|
-
if (base.protocol !== 'https:' || base.username || base.password || base.pathname !== '/' || base.search || base.hash) throw new Error('Invalid activity origin');
|
|
54
|
-
const fetcher = options.fetcher ?? fetch;
|
|
55
|
-
const abort = new AbortController();
|
|
56
|
-
const timeout = setTimeout(() => abort.abort(), 4_000);
|
|
57
|
-
try {
|
|
58
|
-
let token: string;
|
|
59
|
-
if (options.credential) token = await Promise.race([
|
|
60
|
-
options.credential(),
|
|
61
|
-
new Promise<never>((_, reject) => abort.signal.addEventListener('abort', () => reject(new Error('Activity credential timeout')), { once: true })),
|
|
62
|
-
]);
|
|
63
|
-
else {
|
|
64
|
-
const key = env.OXY_EDGE_ACTIVITY_API_KEY;
|
|
65
|
-
const secret = env.OXY_EDGE_ACTIVITY_API_SECRET;
|
|
66
|
-
if (!key || !secret) throw new Error('Missing edge activity credentials');
|
|
67
|
-
const cached = tokens.get(env);
|
|
68
|
-
if (cached && cached.key === key && cached.secret === secret && cached.origin === base.origin && cached.expiresAt > Date.now() + 60_000) token = cached.token;
|
|
69
|
-
else {
|
|
70
|
-
// Same exchange and expiry margin as OxyServices.getServiceToken().
|
|
71
|
-
const response = await fetcher(new URL('/auth/service-token', base), {
|
|
72
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
73
|
-
body: JSON.stringify({ apiKey: key, apiSecret: secret }), signal: abort.signal,
|
|
74
|
-
});
|
|
75
|
-
if (!response.ok) { await response.body?.cancel(); throw new Error('Activity authentication failed'); }
|
|
76
|
-
const data = await smallJson(response) as { token?: unknown; expiresIn?: unknown };
|
|
77
|
-
if (typeof data.token !== 'string' || !data.token || typeof data.expiresIn !== 'number' || !Number.isFinite(data.expiresIn) || data.expiresIn <= 0) throw new Error('Invalid activity token');
|
|
78
|
-
token = data.token;
|
|
79
|
-
tokens.set(env, { key, secret, origin: base.origin, token, expiresAt: Date.now() + data.expiresIn * 1_000 });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const response = await fetcher(new URL('/internal/activity', base), {
|
|
83
|
-
method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
84
|
-
body: JSON.stringify(events), signal: abort.signal,
|
|
85
|
-
});
|
|
86
|
-
if (response.status === 401) tokens.delete(env);
|
|
87
|
-
await response.body?.cancel();
|
|
88
|
-
if (!response.ok) throw new Error('Activity ingestion failed');
|
|
89
|
-
} finally { clearTimeout(timeout); }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** Returns the original response/stream and preserves original handler errors. */
|
|
93
|
-
export async function observeEdgeRequest(options: EdgeActivityOptions): Promise<Response> {
|
|
94
|
-
const report = () => { try { (options.onError ?? console.error)('Edge activity publication failed'); } catch { /* Never break serving. */ } };
|
|
95
|
-
const enabled = options.env.OXY_EDGE_ACTIVITY_ENABLED;
|
|
96
|
-
if (enabled === undefined || enabled === 'false') return options.next();
|
|
97
|
-
if (enabled !== 'true' || !/^[a-z][a-z0-9-]{0,39}$/.test(options.service)) { report(); return options.next(); }
|
|
98
|
-
const path = new URL(options.request.url).pathname;
|
|
99
|
-
if (CONTROL.test(path)) return options.next();
|
|
100
|
-
const pop = normalizeEdgePop(typeof options.request.cf?.colo === 'string' ? options.request.cf.colo : undefined);
|
|
101
|
-
const region = pop ? `edge-${pop}` : 'unknown';
|
|
102
|
-
const startedAt = new Date().toISOString();
|
|
103
|
-
const complete = (response?: Response) => {
|
|
104
|
-
const contentType = response?.headers.get('Content-Type') ?? '';
|
|
105
|
-
const activityType = /^(?:image|audio|video)\//i.test(contentType) ? 'media' : trafficType(path);
|
|
106
|
-
const common = { service: options.service, region, scope: options.peer ? 'internal' as const : 'external' as const, activityType, requests: 1, windowStartedAt: startedAt, emittedAt: new Date().toISOString() };
|
|
107
|
-
// The serving PoP is infrastructure metadata. Do not invent visitor location.
|
|
108
|
-
const events: TrafficAggregate[] = [{ ...common, direction: 'inbound', sourceRegion: options.peer?.region, sourceService: options.peer?.service, targetRegion: region, targetService: options.service }];
|
|
109
|
-
if (response) events.push({ ...common, direction: 'outbound', sourceRegion: region, sourceService: options.service, targetRegion: options.peer?.region, targetService: options.peer?.service });
|
|
110
|
-
try { options.ctx.waitUntil(publish(options, events).catch(report)); } catch { report(); }
|
|
111
|
-
};
|
|
112
|
-
let response: Response;
|
|
113
|
-
try { response = await options.next(); }
|
|
114
|
-
catch (error) { complete(); throw error; }
|
|
115
|
-
complete(response);
|
|
116
|
-
return response;
|
|
117
|
-
}
|