@oxy.so/telemetry 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,6 @@
1
+ import { OXY_ACTIVITY_ID_HEADER, OXY_EDGE_REGION_HEADER, } from './index.js';
1
2
  export const DEFAULT_PRESENCE_BUCKET_MS = 30000;
3
+ /** @deprecated Unused by any Oxy repo; `presenceKeys` already encodes the window. Removal is planned for the next major. */
2
4
  export const DEFAULT_ACTIVE_CLIENT_WINDOW_MS = 60000;
3
5
  function firstHeaderValue(value) {
4
6
  if (typeof value === 'string')
@@ -15,23 +17,43 @@ export function normalizeActivityId(value) {
15
17
  const candidate = firstHeaderValue(value);
16
18
  return candidate && /^[a-z0-9_-]{16,64}$/i.test(candidate) ? candidate : undefined;
17
19
  }
20
+ /**
21
+ * The collector's source region for a normalised edge PoP (`edge-<pop>`), the
22
+ * one derivation every reporter shares. `undefined` when there is no PoP.
23
+ */
24
+ export function sourceRegionFromEdgePop(edgePop) {
25
+ return edgePop ? `edge-${edgePop}` : undefined;
26
+ }
18
27
  export function metadataFromHeaders(headers) {
19
- const forwardedPop = normalizeEdgePop(headers['x-oxy-edge-region']);
28
+ const forwardedPop = normalizeEdgePop(headers[OXY_EDGE_REGION_HEADER.toLowerCase()]);
20
29
  const ray = firstHeaderValue(headers['cf-ray']);
21
30
  const rayPop = ray?.match(/-([a-z]{3})$/i)?.[1]?.toLowerCase();
22
- const activityId = normalizeActivityId(headers['x-oxy-activity-id']);
31
+ const activityId = normalizeActivityId(headers[OXY_ACTIVITY_ID_HEADER.toLowerCase()]);
23
32
  return {
24
33
  ...(activityId ? { activityId } : {}),
25
34
  ...(forwardedPop || rayPop ? { edgePop: forwardedPop ?? rayPop } : {}),
26
35
  };
27
36
  }
37
+ /**
38
+ * @deprecated Unused by any Oxy repo. For the bounded activity category of a
39
+ * path use `trafficType` from `@oxy.so/telemetry/collector`. Removal is planned
40
+ * for the next major.
41
+ */
28
42
  export function serviceFromPath(path) {
29
43
  const segment = path.split('/').filter(Boolean)[0];
30
44
  return segment && /^[a-z][a-z0-9-]{0,31}$/i.test(segment) ? segment.toLowerCase() : 'platform';
31
45
  }
46
+ /**
47
+ * @deprecated Unused by any Oxy repo; `createTrafficCollector` keys its own
48
+ * flows. Removal is planned for the next major.
49
+ */
32
50
  export function activityFlow(sourceRegion, service) {
33
51
  return { sourceRegion, service, key: `${sourceRegion ?? ''}|${service}` };
34
52
  }
53
+ /**
54
+ * @deprecated Unused outside `presenceKeys`; call `presenceKeys` for the
55
+ * current and previous bucket keys. Removal is planned for the next major.
56
+ */
35
57
  export function presenceBucket(observedAt, bucketMs = DEFAULT_PRESENCE_BUCKET_MS) {
36
58
  if (!Number.isFinite(observedAt) || !Number.isFinite(bucketMs) || bucketMs <= 0) {
37
59
  throw new RangeError('observedAt and bucketMs must define a finite positive bucket');
@@ -1,5 +1,5 @@
1
1
  import { normalizeInfrastructureRegion } from './collector.js';
2
- import { metadataFromHeaders } from './server.js';
2
+ import { metadataFromHeaders, sourceRegionFromEdgePop } from './server.js';
3
3
  const CATEGORIES = {
4
4
  auth: 'identity',
5
5
  session: 'identity',
@@ -44,9 +44,7 @@ export function observeTrafficSocket(socket, collector, service, region, options
44
44
  const edgePop = metadataFromHeaders(socket.handshake.headers).edgePop;
45
45
  const peerRegion = peerService
46
46
  ? normalizeInfrastructureRegion(peer?.region)
47
- : edgePop
48
- ? `edge-${edgePop}`
49
- : undefined;
47
+ : sourceRegionFromEdgePop(edgePop);
50
48
  collector.record({
51
49
  region,
52
50
  service,
@@ -85,9 +83,7 @@ export function observeTrafficWebSocket(socket, collector, service, region, opti
85
83
  const edgePop = metadataFromHeaders(options.headers ?? {}).edgePop;
86
84
  const peerRegion = peerService
87
85
  ? normalizeInfrastructureRegion(peer?.region)
88
- : edgePop
89
- ? `edge-${edgePop}`
90
- : undefined;
86
+ : sourceRegionFromEdgePop(edgePop);
91
87
  collector.record({
92
88
  service,
93
89
  region,
@@ -32,9 +32,20 @@ export interface BrowserTelemetry {
32
32
  peekEdgeRegionHeader(): Record<string, string>;
33
33
  getHeaders(): Promise<Record<string, string>>;
34
34
  }
35
+ /**
36
+ * @deprecated No Oxy repo constructs its own runtime: use the module-level
37
+ * `getBrowserActivityIdHeader`, `getBrowserEdgeRegionHeader` and
38
+ * `peekBrowserEdgeRegionHeader`. The factory remains the test seam. Removal from
39
+ * the public surface is planned for the next major.
40
+ */
35
41
  export declare function createBrowserTelemetry(options?: BrowserTelemetryOptions): BrowserTelemetry;
36
42
  export declare const getBrowserActivityIdHeader: () => Record<string, string>;
37
43
  export declare const getBrowserEdgeRegionHeader: () => Promise<Record<string, string>>;
38
44
  export declare const peekBrowserEdgeRegionHeader: () => Record<string, string>;
45
+ /**
46
+ * @deprecated Unused by any Oxy repo. `@oxy.so/core` sends
47
+ * `peekBrowserEdgeRegionHeader()` and `getBrowserActivityIdHeader()` per request.
48
+ * Removal is planned for the next major.
49
+ */
39
50
  export declare const getBrowserTelemetryHeaders: () => Promise<Record<string, string>>;
40
51
  export {};
@@ -1,24 +1,26 @@
1
+ import { type TrafficAggregate, type TrafficFlow, type TrafficType } from './index.js';
1
2
  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
- }
3
+ export type { TrafficAggregate, TrafficDirection, TrafficFlow, TrafficScope, TrafficType, } from './index.js';
21
4
  export declare function trafficType(path: string): TrafficType;
5
+ /** Which side of a request a reporter is observing. */
6
+ export type ObservationDirection = 'inbound' | 'outbound';
7
+ /**
8
+ * Every path a reporter does not observe, in ONE table. Each entry says which
9
+ * directions it applies to: the inbound and outbound sets differ today (an
10
+ * outbound call to a probe or to `/platform-stats` IS recorded), and that is
11
+ * preserved exactly. Unifying them is a separate, deliberate decision.
12
+ */
13
+ export declare const UNOBSERVED_PATHS: readonly {
14
+ readonly directions: ObservationDirection | 'both';
15
+ readonly matches: (path: string) => boolean;
16
+ }[];
17
+ /**
18
+ * Is this path part of what a reporter observes in this direction? Collection
19
+ * itself, service-token minting and (inbound) probes and the dashboard
20
+ * transport are control traffic: observing them would feed back into the very
21
+ * activity they carry.
22
+ */
23
+ export declare function isObservedPath(path: string, direction: ObservationDirection): boolean;
22
24
  export declare function createTrafficCollector(publish: (events: TrafficAggregate[]) => Promise<unknown>, now?: () => number): {
23
25
  record(flow: TrafficFlow): void;
24
26
  flush(): Promise<void>;
@@ -47,4 +49,3 @@ export declare function infrastructureLocation(region: string): {
47
49
  coordinates: [number, number];
48
50
  };
49
51
  export declare function normalizeInfrastructureRegion(value: unknown): string | undefined;
50
- export {};
@@ -1,16 +1,69 @@
1
+ /** Request header a browser sends: the ephemeral anonymous activity ID. */
1
2
  export declare const OXY_ACTIVITY_ID_HEADER = "X-Oxy-Activity-Id";
3
+ /** Request header a browser sends: the three-letter Cloudflare PoP code. */
2
4
  export declare const OXY_EDGE_REGION_HEADER = "X-Oxy-Edge-Region";
5
+ /** Response header a reporting service sets: the region that processed the request. */
6
+ export declare const OXY_REGION_HEADER = "X-Oxy-Region";
7
+ /** Request header one Oxy service sends another: the caller's infrastructure region. */
8
+ export declare const OXY_SOURCE_REGION_HEADER = "X-Oxy-Source-Region";
9
+ /**
10
+ * Request headers a browser may send and a CORS allow-list must therefore
11
+ * permit (`Access-Control-Allow-Headers`). `X-Oxy-Source-Region` is service to
12
+ * service only and is deliberately absent. The list is derived from the
13
+ * constants above, so a CORS list built from it cannot drift from them.
14
+ */
15
+ export declare const OXY_TELEMETRY_REQUEST_HEADERS: readonly string[];
16
+ /**
17
+ * Response headers a browser client may need to read
18
+ * (`Access-Control-Expose-Headers`).
19
+ */
20
+ export declare const OXY_TELEMETRY_EXPOSED_HEADERS: readonly string[];
3
21
  export interface AnonymousActivityMetadata {
4
22
  activityId?: string;
5
23
  edgePop?: string;
6
24
  }
7
- export interface ActivityAggregate {
25
+ export type TrafficScope = 'internal' | 'external';
26
+ export type TrafficDirection = 'inbound' | 'outbound';
27
+ export type TrafficType = 'identity' | 'ai' | 'communication' | 'media' | 'platform';
28
+ /** One observed flow, as a reporter records it. */
29
+ export interface TrafficFlow {
30
+ region: string;
8
31
  sourceRegion?: string;
9
- targetRegion: string;
32
+ targetRegion?: string;
33
+ sourceService?: string;
34
+ targetService?: string;
10
35
  service: string;
36
+ scope: TrafficScope;
37
+ direction: TrafficDirection;
38
+ activityType: TrafficType;
39
+ }
40
+ /**
41
+ * A flow with its request count and window: the exact shape the API's strict
42
+ * `platformActivityBatchSchema` accepts on `POST /internal/activity`.
43
+ */
44
+ export interface TrafficAggregate extends TrafficFlow {
11
45
  requests: number;
12
- activeClients: number;
13
46
  windowStartedAt: string;
14
47
  emittedAt: string;
15
- direction: 'inbound';
48
+ }
49
+ /**
50
+ * The `platform_activity` socket payload: an aggregate plus the approximate
51
+ * active-client count for its origin.
52
+ */
53
+ export type ActivityAggregate = TrafficAggregate & {
54
+ activeClients: number;
55
+ };
56
+ /** One region in the `platform_infrastructure` snapshot. */
57
+ export interface InfrastructureNode {
58
+ region: string;
59
+ label: string;
60
+ coordinates: [number, number];
61
+ services: string[];
62
+ instances: number;
63
+ status: 'online' | 'degraded' | 'offline' | 'unknown';
64
+ }
65
+ /** The `platform_infrastructure` socket payload. */
66
+ export interface InfrastructureSnapshot {
67
+ emittedAt: string;
68
+ nodes: InfrastructureNode[];
16
69
  }
@@ -76,4 +76,9 @@ export declare const HTTP_REQUEST_LATENCY: MetricDefinition;
76
76
  export declare const HTTP_RATE_LIMITED: MetricDefinition;
77
77
  /** `2xx`, `4xx`, … — a closed set, safe as a dimension. */
78
78
  export declare function statusClass(status: number): string;
79
+ /**
80
+ * `createEmfPublisher` on ECS, `null` everywhere else (a laptop, CI, a test):
81
+ * the gate oxy-api and Mention each repeated. `env` is injectable for tests.
82
+ */
83
+ export declare function createEmfPublisherIfDeployed(options: EmfPublisherOptions, env?: Readonly<Record<string, string | undefined>>): EmfPublisher | null;
79
84
  export declare function createEmfPublisher(options: EmfPublisherOptions): EmfPublisher;
@@ -1,12 +1,18 @@
1
- import type { ActivityAggregate, AnonymousActivityMetadata } from './index.js';
1
+ import { type ActivityAggregate, type AnonymousActivityMetadata } from './index.js';
2
2
  export declare const DEFAULT_PRESENCE_BUCKET_MS = 30000;
3
+ /** @deprecated Unused by any Oxy repo; `presenceKeys` already encodes the window. Removal is planned for the next major. */
3
4
  export declare const DEFAULT_ACTIVE_CLIENT_WINDOW_MS = 60000;
4
5
  export type TelemetryHeaderValue = string | readonly string[] | undefined;
5
6
  export type TelemetryHeaders = Readonly<Record<string, TelemetryHeaderValue>>;
7
+ /**
8
+ * @deprecated Unused by any Oxy repo (presence counting lives in the API). No
9
+ * replacement; removal is planned for the next major.
10
+ */
6
11
  export interface ActivityCardinalityAdapter {
7
12
  add(key: string, activityId: string, ttlSeconds: number): Promise<void>;
8
13
  count(keys: readonly string[]): Promise<number>;
9
14
  }
15
+ /** @deprecated Unused by any Oxy repo; see `activityFlow`. */
10
16
  export interface ActivityFlow {
11
17
  sourceRegion?: string;
12
18
  service: string;
@@ -14,9 +20,28 @@ export interface ActivityFlow {
14
20
  }
15
21
  export declare function normalizeEdgePop(value: TelemetryHeaderValue): string | undefined;
16
22
  export declare function normalizeActivityId(value: TelemetryHeaderValue): string | undefined;
23
+ /**
24
+ * The collector's source region for a normalised edge PoP (`edge-<pop>`), the
25
+ * one derivation every reporter shares. `undefined` when there is no PoP.
26
+ */
27
+ export declare function sourceRegionFromEdgePop(edgePop: string | undefined): string | undefined;
17
28
  export declare function metadataFromHeaders(headers: TelemetryHeaders): AnonymousActivityMetadata;
29
+ /**
30
+ * @deprecated Unused by any Oxy repo. For the bounded activity category of a
31
+ * path use `trafficType` from `@oxy.so/telemetry/collector`. Removal is planned
32
+ * for the next major.
33
+ */
18
34
  export declare function serviceFromPath(path: string): string;
35
+ /**
36
+ * @deprecated Unused by any Oxy repo; `createTrafficCollector` keys its own
37
+ * flows. Removal is planned for the next major.
38
+ */
19
39
  export declare function activityFlow(sourceRegion: string | undefined, service: string): ActivityFlow;
40
+ /**
41
+ * @deprecated Unused outside `presenceKeys`; call `presenceKeys` for the
42
+ * current and previous bucket keys. Removal is planned for the next major.
43
+ */
20
44
  export declare function presenceBucket(observedAt: number, bucketMs?: number): number;
21
45
  export declare function presenceKeys(sourceRegion: string, observedAt: number, bucketMs?: number): readonly [string, string];
46
+ /** @deprecated Unused by any Oxy repo; use `ActivityAggregate` from `@oxy.so/telemetry`. */
22
47
  export type ActivityBucket = ActivityAggregate;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxy.so/telemetry",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Framework-agnostic anonymous activity telemetry and CloudWatch EMF metrics primitives for Oxy services.",
5
5
  "type": "module",
6
6
  "main": "dist/cjs/index.js",
@@ -105,8 +105,10 @@
105
105
  },
106
106
  "files": [
107
107
  "dist",
108
- "src"
108
+ "src",
109
+ "!dist/.oxy-source-digest"
109
110
  ],
111
+ "oxySourceStamp": "dist",
110
112
  "repository": {
111
113
  "type": "git",
112
114
  "url": "git+https://github.com/OxyHQ/oxy.git",
@@ -119,23 +121,22 @@
119
121
  "node": ">=18.0.0"
120
122
  },
121
123
  "scripts": {
122
- "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types",
124
+ "build": "bun run clean && bun run build:cjs && bun run build:esm && bun run build:types && node -e \"const p='../app-preset/metro/source-stamp.js';require('node:fs').existsSync(p)&&require(p).writeStamp(process.cwd())\"",
123
125
  "build:cjs": "tsc -p tsconfig.cjs.json && node scripts/mark-cjs.mjs",
124
126
  "build:esm": "tsc -p tsconfig.esm.json",
125
127
  "build:types": "tsc -p tsconfig.types.json && node scripts/mark-types-cjs.mjs",
126
128
  "clean": "rm -rf dist",
127
129
  "typescript": "tsc --noEmit",
128
130
  "test": "jest",
129
- "lint": "biome lint --error-on-warnings ./src",
130
- "prepublishOnly": "node ../../scripts/assert-bun-publish.mjs && bun run typescript && bun run test && bun run build"
131
+ "lint": "biome lint --error-on-warnings ./src"
131
132
  },
132
133
  "devDependencies": {
133
134
  "@types/jest": "^30.0.0",
134
- "jest": "^30.5.1",
135
- "jest-environment-jsdom": "^30.5.1",
136
- "socket.io": "^4.8.1",
137
- "socket.io-client": "^4.8.1",
138
- "ts-jest": "^29.4.12",
139
- "typescript": "^5.9.2"
135
+ "jest": "^30.5.2",
136
+ "jest-environment-jsdom": "^30.5.2",
137
+ "socket.io": "^4.8.4",
138
+ "socket.io-client": "^4.8.4",
139
+ "ts-jest": "^29.4.15",
140
+ "typescript": "~6.0.3"
140
141
  }
141
142
  }
@@ -3,6 +3,7 @@ import { EventEmitter } from 'node:events';
3
3
  import {
4
4
  createTrafficCollector,
5
5
  instrumentTrafficFetch,
6
+ resolveOxyServiceEndpoint,
6
7
  trafficMiddleware,
7
8
  trafficType,
8
9
  type TrafficAggregate,
@@ -197,4 +198,20 @@ describe('ecosystem activity collector', () => {
197
198
  expect(trafficType('/api/v1/uploads/private-id?token=secret')).toBe('media');
198
199
  expect(trafficType('/v1/chat/completions')).toBe('ai');
199
200
  });
201
+
202
+ it('attributes the Gemies API to gemies and keeps Alia’s API as alia', () => {
203
+ expect(resolveOxyServiceEndpoint(new URL('https://api.gemies.ai/v1/chat'))).toEqual({
204
+ service: 'gemies',
205
+ region: 'us-west-2',
206
+ });
207
+ expect(resolveOxyServiceEndpoint(new URL('https://api.alia.onl/v1/chat'))?.service).toBe(
208
+ 'alia',
209
+ );
210
+ // Exact host only: neither the web app nor a lookalike claims the service.
211
+ expect(resolveOxyServiceEndpoint(new URL('https://gemies.ai/'))).toBeUndefined();
212
+ expect(
213
+ resolveOxyServiceEndpoint(new URL('https://api.gemies.ai.evil.example/')),
214
+ ).toBeUndefined();
215
+ expect(resolveOxyServiceEndpoint(new URL('http://api.gemies.ai/'))).toBeUndefined();
216
+ });
200
217
  });
@@ -0,0 +1,53 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { createEmfPublisher, type MetricDefinition } from '../metrics';
4
+
5
+ // The cross-language contract: Kaana's Go `internal/emf` runs the same file.
6
+ // Numbers JSON cannot carry travel as the strings below.
7
+ const SENTINELS: Record<string, number> = {
8
+ NaN: Number.NaN,
9
+ Infinity: Number.POSITIVE_INFINITY,
10
+ '-Infinity': Number.NEGATIVE_INFINITY,
11
+ };
12
+
13
+ interface GoldenCase {
14
+ name: string;
15
+ definition: MetricDefinition;
16
+ service: string;
17
+ namespace: string;
18
+ now: number;
19
+ maxSeries?: number;
20
+ observations: { labels: Record<string, string>; value: number | string }[];
21
+ expectedLines: unknown[];
22
+ }
23
+
24
+ const cases: GoldenCase[] = JSON.parse(
25
+ readFileSync(join(__dirname, '..', '..', 'fixtures', 'emf-golden.json'), 'utf8'),
26
+ );
27
+
28
+ describe('EMF golden fixture', () => {
29
+ it('is not empty and has unique case names', () => {
30
+ expect(cases.length).toBeGreaterThan(0);
31
+ expect(new Set(cases.map((entry) => entry.name)).size).toBe(cases.length);
32
+ });
33
+
34
+ it.each(cases.map((entry) => [entry.name, entry] as const))('%s', (_name, entry) => {
35
+ const lines: string[] = [];
36
+ const publisher = createEmfPublisher({
37
+ namespace: entry.namespace,
38
+ service: entry.service,
39
+ write: (line) => lines.push(line),
40
+ now: () => entry.now,
41
+ ...(entry.maxSeries === undefined ? {} : { maxSeries: entry.maxSeries }),
42
+ });
43
+ for (const { labels, value } of entry.observations) {
44
+ publisher.record(
45
+ entry.definition,
46
+ typeof value === 'string' ? SENTINELS[value] : value,
47
+ labels,
48
+ );
49
+ }
50
+ publisher.stop();
51
+ expect(lines.map((line) => JSON.parse(line))).toEqual(entry.expectedLines);
52
+ });
53
+ });
@@ -0,0 +1,184 @@
1
+ /** @jest-environment node */
2
+ import { isObservedPath } from '../collector';
3
+ import {
4
+ OXY_ACTIVITY_ID_HEADER,
5
+ OXY_EDGE_REGION_HEADER,
6
+ OXY_REGION_HEADER,
7
+ OXY_SOURCE_REGION_HEADER,
8
+ OXY_TELEMETRY_EXPOSED_HEADERS,
9
+ OXY_TELEMETRY_REQUEST_HEADERS,
10
+ } from '../index';
11
+ import { createEmfPublisherIfDeployed } from '../metrics';
12
+ import { metadataFromHeaders, sourceRegionFromEdgePop } from '../server';
13
+
14
+ describe('telemetry header lists', () => {
15
+ it('request headers are exactly the headers a browser sends', () => {
16
+ expect([...OXY_TELEMETRY_REQUEST_HEADERS]).toEqual([
17
+ OXY_ACTIVITY_ID_HEADER,
18
+ OXY_EDGE_REGION_HEADER,
19
+ ]);
20
+ expect(OXY_TELEMETRY_REQUEST_HEADERS).not.toContain(OXY_SOURCE_REGION_HEADER);
21
+ });
22
+
23
+ it('exposed headers are exactly the response headers a browser reads', () => {
24
+ expect([...OXY_TELEMETRY_EXPOSED_HEADERS]).toEqual([OXY_REGION_HEADER]);
25
+ });
26
+
27
+ it('the server reads the same headers the constants name', () => {
28
+ expect(
29
+ metadataFromHeaders({
30
+ [OXY_EDGE_REGION_HEADER.toLowerCase()]: 'MAD',
31
+ [OXY_ACTIVITY_ID_HEADER.toLowerCase()]: 'anonymous-runtime-a1',
32
+ }),
33
+ ).toEqual({ edgePop: 'mad', activityId: 'anonymous-runtime-a1' });
34
+ });
35
+ });
36
+
37
+ // The pre-change behaviour, copied verbatim from origin/main collector.ts
38
+ // (middleware lines 119-124, instrumentTrafficFetch lines 200-202).
39
+ const legacyInboundExcluded = (path: string): boolean =>
40
+ /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|platform-infrastructure|infra-status)(?:\/|$)/.test(
41
+ path,
42
+ ) ||
43
+ path.startsWith('/internal/activity') ||
44
+ path.startsWith('/auth/service-token') ||
45
+ path.startsWith('/cdn-cgi/');
46
+ const legacyOutboundExcluded = (pathname: string): boolean =>
47
+ pathname.startsWith('/internal/activity') ||
48
+ pathname.startsWith('/auth/service-token') ||
49
+ pathname === '/platform-infrastructure';
50
+
51
+ const PATHS = [
52
+ '/',
53
+ '/users/me',
54
+ '/api/v1/users',
55
+ '/auth/session',
56
+ '/healthy',
57
+ '/readyz',
58
+ '/cdn-cgi',
59
+ '/health',
60
+ '/health/deep',
61
+ '/health?x=1',
62
+ '/api/health',
63
+ '/api/health/x',
64
+ '/ready',
65
+ '/api/ready',
66
+ '/live',
67
+ '/api/live',
68
+ '/platform-stats',
69
+ '/platform-stats/stream',
70
+ '/api/platform-stats',
71
+ '/platform-activity',
72
+ '/platform-activity/x',
73
+ '/api/platform-activity',
74
+ '/infra-status',
75
+ '/infra-status/x',
76
+ '/api/infra-status',
77
+ '/platform-infrastructure',
78
+ '/platform-infrastructure/',
79
+ '/platform-infrastructure/x',
80
+ '/api/platform-infrastructure',
81
+ '/api/platform-infrastructure/x',
82
+ '/platform-infrastructures',
83
+ '/api/api/health',
84
+ '/internal/activity',
85
+ '/internal/activity/batch',
86
+ '/internal/activityx',
87
+ '/auth/service-token',
88
+ '/auth/service-token/x',
89
+ '/auth/service-tokens',
90
+ '/cdn-cgi/',
91
+ '/cdn-cgi/trace',
92
+ '/v1/health',
93
+ ];
94
+
95
+ describe('isObservedPath', () => {
96
+ it.each(PATHS)('%s matches the pre-change inbound behaviour', (path) => {
97
+ expect(isObservedPath(path, 'inbound')).toBe(!legacyInboundExcluded(path));
98
+ });
99
+
100
+ it.each(PATHS)('%s matches the pre-change outbound behaviour', (path) => {
101
+ expect(isObservedPath(path, 'outbound')).toBe(!legacyOutboundExcluded(path));
102
+ });
103
+
104
+ it('pins the excluded sets explicitly in both directions', () => {
105
+ const excluded = (direction: 'inbound' | 'outbound') =>
106
+ PATHS.filter((path) => !isObservedPath(path, direction));
107
+ expect(excluded('outbound')).toEqual([
108
+ '/platform-infrastructure',
109
+ '/internal/activity',
110
+ '/internal/activity/batch',
111
+ '/internal/activityx',
112
+ '/auth/service-token',
113
+ '/auth/service-token/x',
114
+ '/auth/service-tokens',
115
+ ]);
116
+ expect(excluded('inbound')).toEqual(PATHS.filter(legacyInboundExcluded));
117
+ // Excluded inbound only today; recorded outbound. Unifying is an owner decision.
118
+ expect(excluded('inbound').filter((path) => isObservedPath(path, 'outbound'))).toContain(
119
+ '/platform-stats',
120
+ );
121
+ });
122
+ });
123
+
124
+ describe('sourceRegionFromEdgePop', () => {
125
+ it('prefixes a PoP and passes absence through', () => {
126
+ expect(sourceRegionFromEdgePop('mad')).toBe('edge-mad');
127
+ expect(sourceRegionFromEdgePop(undefined)).toBeUndefined();
128
+ expect(sourceRegionFromEdgePop('')).toBeUndefined();
129
+ });
130
+ });
131
+
132
+ describe('createEmfPublisherIfDeployed', () => {
133
+ const options = { namespace: 'Oxy/Test', service: 'svc', write: () => {} };
134
+
135
+ it('returns null off ECS', () => {
136
+ expect(createEmfPublisherIfDeployed(options, {})).toBeNull();
137
+ expect(createEmfPublisherIfDeployed(options, { ECS_CONTAINER_METADATA_URI_V4: '' })).toBeNull();
138
+ });
139
+
140
+ it('returns a publisher on ECS', () => {
141
+ const publisher = createEmfPublisherIfDeployed(options, {
142
+ ECS_CONTAINER_METADATA_URI_V4: 'http://169.254.170.2/v4/task',
143
+ });
144
+ expect(publisher).not.toBeNull();
145
+ publisher?.stop();
146
+ });
147
+
148
+ it('reads process.env by default', () => {
149
+ const previous = process.env.ECS_CONTAINER_METADATA_URI_V4;
150
+ delete process.env.ECS_CONTAINER_METADATA_URI_V4;
151
+ try {
152
+ expect(createEmfPublisherIfDeployed(options)).toBeNull();
153
+ } finally {
154
+ if (previous !== undefined) process.env.ECS_CONTAINER_METADATA_URI_V4 = previous;
155
+ }
156
+ });
157
+ });
158
+
159
+ describe('instrumentTrafficFetch exclusion', () => {
160
+ it('skips control requests and still records outbound calls to inbound-excluded paths', async () => {
161
+ const { createTrafficCollector, instrumentTrafficFetch } = await import('../collector');
162
+ const published: unknown[][] = [];
163
+ const collector = createTrafficCollector(async (events) => {
164
+ published.push(events);
165
+ });
166
+ const fetcher = jest.fn(async () => new Response('ok')) as unknown as typeof fetch;
167
+ const observed = instrumentTrafficFetch(
168
+ fetcher,
169
+ collector,
170
+ 'svc',
171
+ 'us-west-2',
172
+ () => undefined,
173
+ );
174
+ await observed('https://example.com/internal/activity');
175
+ await observed('https://example.com/auth/service-token');
176
+ await observed('https://example.com/platform-infrastructure');
177
+ await collector.flush();
178
+ expect(published).toHaveLength(0);
179
+ // Recorded outbound today, though excluded inbound.
180
+ await observed('https://example.com/platform-stats');
181
+ await collector.flush();
182
+ expect(published).toHaveLength(1);
183
+ });
184
+ });
package/src/browser.ts CHANGED
@@ -60,6 +60,12 @@ function eligibleLocation(location: Pick<Location, 'hostname' | 'protocol'> | un
60
60
  );
61
61
  }
62
62
 
63
+ /**
64
+ * @deprecated No Oxy repo constructs its own runtime: use the module-level
65
+ * `getBrowserActivityIdHeader`, `getBrowserEdgeRegionHeader` and
66
+ * `peekBrowserEdgeRegionHeader`. The factory remains the test seam. Removal from
67
+ * the public surface is planned for the next major.
68
+ */
63
69
  export function createBrowserTelemetry(options: BrowserTelemetryOptions = {}): BrowserTelemetry {
64
70
  let activityId: string | null = null;
65
71
  let activityIdCreatedAt = 0;
@@ -169,5 +175,10 @@ export const getBrowserEdgeRegionHeader = (): Promise<Record<string, string>> =>
169
175
  export const peekBrowserEdgeRegionHeader = (): Record<string, string> =>
170
176
  defaultBrowserTelemetry.peekEdgeRegionHeader();
171
177
 
178
+ /**
179
+ * @deprecated Unused by any Oxy repo. `@oxy.so/core` sends
180
+ * `peekBrowserEdgeRegionHeader()` and `getBrowserActivityIdHeader()` per request.
181
+ * Removal is planned for the next major.
182
+ */
172
183
  export const getBrowserTelemetryHeaders = (): Promise<Record<string, string>> =>
173
184
  defaultBrowserTelemetry.getHeaders();