@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/esm/collector.js
CHANGED
|
@@ -1,10 +1,33 @@
|
|
|
1
1
|
import { metadataFromHeaders } from './server.js';
|
|
2
2
|
const TYPES = {
|
|
3
|
-
auth: 'identity',
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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 }) => ({
|
|
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) ||
|
|
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
|
|
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
|
|
70
|
-
|
|
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({
|
|
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') ||
|
|
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,
|
|
101
|
-
|
|
102
|
-
|
|
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
|
|
178
|
+
const targetRegion = peer
|
|
179
|
+
? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined)
|
|
180
|
+
: undefined;
|
|
116
181
|
collector.record({ ...flow, targetRegion });
|
|
117
|
-
collector.record({
|
|
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',
|
|
125
|
-
'
|
|
126
|
-
'api.
|
|
127
|
-
'api.
|
|
128
|
-
'
|
|
129
|
-
'api.
|
|
130
|
-
'
|
|
131
|
-
'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) {
|
|
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
|
}
|
package/dist/esm/server.js
CHANGED
|
@@ -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)
|
|
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);
|
package/dist/esm/socket.js
CHANGED
|
@@ -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',
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
45
|
+
const peerRegion = peerService
|
|
46
|
+
? normalizeInfrastructureRegion(peer?.region)
|
|
47
|
+
: edgePop
|
|
48
|
+
? `edge-${edgePop}`
|
|
49
|
+
: undefined;
|
|
31
50
|
collector.record({
|
|
32
|
-
region,
|
|
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
|
|
86
|
+
const peerRegion = peerService
|
|
87
|
+
? normalizeInfrastructureRegion(peer?.region)
|
|
88
|
+
: edgePop
|
|
89
|
+
? `edge-${edgePop}`
|
|
90
|
+
: undefined;
|
|
65
91
|
collector.record({
|
|
66
|
-
service,
|
|
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,
|
package/dist/types/browser.d.ts
CHANGED
|
@@ -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": "
|
|
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/
|
|
99
|
+
"url": "git+https://github.com/OxyHQ/oxy.git",
|
|
113
100
|
"directory": "packages/telemetry"
|
|
114
101
|
},
|
|
115
|
-
"license": "
|
|
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 () => ({
|
|
51
|
-
|
|
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
|
|
62
|
-
|
|
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
|
});
|