@oxy.so/telemetry 2.0.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.
@@ -26,6 +26,12 @@ function eligibleLocation(location) {
26
26
  location.hostname !== '127.0.0.1' &&
27
27
  location.hostname !== '[::1]');
28
28
  }
29
+ /**
30
+ * @deprecated No Oxy repo constructs its own runtime: use the module-level
31
+ * `getBrowserActivityIdHeader`, `getBrowserEdgeRegionHeader` and
32
+ * `peekBrowserEdgeRegionHeader`. The factory remains the test seam. Removal from
33
+ * the public surface is planned for the next major.
34
+ */
29
35
  export function createBrowserTelemetry(options = {}) {
30
36
  let activityId = null;
31
37
  let activityIdCreatedAt = 0;
@@ -118,4 +124,9 @@ const defaultBrowserTelemetry = createBrowserTelemetry();
118
124
  export const getBrowserActivityIdHeader = () => defaultBrowserTelemetry.getActivityIdHeader();
119
125
  export const getBrowserEdgeRegionHeader = () => defaultBrowserTelemetry.getEdgeRegionHeader();
120
126
  export const peekBrowserEdgeRegionHeader = () => defaultBrowserTelemetry.peekEdgeRegionHeader();
127
+ /**
128
+ * @deprecated Unused by any Oxy repo. `@oxy.so/core` sends
129
+ * `peekBrowserEdgeRegionHeader()` and `getBrowserActivityIdHeader()` per request.
130
+ * Removal is planned for the next major.
131
+ */
121
132
  export const getBrowserTelemetryHeaders = () => defaultBrowserTelemetry.getHeaders();
@@ -1,4 +1,5 @@
1
- import { metadataFromHeaders } from './server.js';
1
+ import { OXY_REGION_HEADER, OXY_SOURCE_REGION_HEADER, } from './index.js';
2
+ import { metadataFromHeaders, sourceRegionFromEdgePop } from './server.js';
2
3
  const TYPES = {
3
4
  auth: 'identity',
4
5
  session: 'identity',
@@ -39,6 +40,35 @@ export function trafficType(path) {
39
40
  parts.shift();
40
41
  return TYPES[parts[0] ?? ''] ?? 'platform';
41
42
  }
43
+ /**
44
+ * Every path a reporter does not observe, in ONE table. Each entry says which
45
+ * directions it applies to: the inbound and outbound sets differ today (an
46
+ * outbound call to a probe or to `/platform-stats` IS recorded), and that is
47
+ * preserved exactly. Unifying them is a separate, deliberate decision.
48
+ */
49
+ export const UNOBSERVED_PATHS = [
50
+ {
51
+ directions: 'inbound',
52
+ matches: (path) => /^\/(?:api\/)?(?:health|ready|live|platform-stats|platform-activity|infra-status)(?:\/|$)/.test(path),
53
+ },
54
+ {
55
+ directions: 'inbound',
56
+ matches: (path) => /^\/(?:api\/)?platform-infrastructure(?:\/|$)/.test(path),
57
+ },
58
+ { directions: 'outbound', matches: (path) => path === '/platform-infrastructure' },
59
+ { directions: 'both', matches: (path) => path.startsWith('/internal/activity') },
60
+ { directions: 'both', matches: (path) => path.startsWith('/auth/service-token') },
61
+ { directions: 'inbound', matches: (path) => path.startsWith('/cdn-cgi/') },
62
+ ];
63
+ /**
64
+ * Is this path part of what a reporter observes in this direction? Collection
65
+ * itself, service-token minting and (inbound) probes and the dashboard
66
+ * transport are control traffic: observing them would feed back into the very
67
+ * activity they carry.
68
+ */
69
+ export function isObservedPath(path, direction) {
70
+ return !UNOBSERVED_PATHS.some((entry) => (entry.directions === 'both' || entry.directions === direction) && entry.matches(path));
71
+ }
42
72
  export function createTrafficCollector(publish, now = Date.now) {
43
73
  const pending = new Map();
44
74
  let flushing = null;
@@ -79,13 +109,10 @@ export function createTrafficCollector(publish, now = Date.now) {
79
109
  }
80
110
  export function trafficMiddleware(collector, service, region) {
81
111
  return (request, response, next) => {
82
- response.setHeader?.('X-Oxy-Region', region);
112
+ response.setHeader?.(OXY_REGION_HEADER, region);
83
113
  const path = request.path ?? request.url ?? '/';
84
114
  // Never report collection itself, probes or the dashboard transport.
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/')) {
115
+ if (!isObservedPath(path, 'inbound')) {
89
116
  next();
90
117
  return;
91
118
  }
@@ -99,12 +126,10 @@ export function trafficMiddleware(collector, service, region) {
99
126
  .replace(/[^a-z0-9-]/g, '-')
100
127
  .slice(0, 40);
101
128
  const internal = Boolean(name);
102
- const forwardedRegion = request.headers['x-oxy-source-region'];
129
+ const forwardedRegion = request.headers[OXY_SOURCE_REGION_HEADER.toLowerCase()];
103
130
  const peerRegion = internal
104
131
  ? normalizeInfrastructureRegion(forwardedRegion)
105
- : edgePop
106
- ? `edge-${edgePop}`
107
- : undefined;
132
+ : sourceRegionFromEdgePop(edgePop);
108
133
  return {
109
134
  region,
110
135
  service,
@@ -148,9 +173,7 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
148
173
  return Object.assign(async (input, init) => {
149
174
  const url = new URL(typeof input === 'string' ? input : input instanceof URL ? input.href : input.url);
150
175
  // Collection/auth control traffic cannot generate a feedback loop.
151
- if (url.pathname.startsWith('/internal/activity') ||
152
- url.pathname.startsWith('/auth/service-token') ||
153
- url.pathname === '/platform-infrastructure')
176
+ if (!isObservedPath(url.pathname, 'outbound'))
154
177
  return fetcher(input, init);
155
178
  const peer = resolveEndpoint(url);
156
179
  const flow = {
@@ -166,7 +189,7 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
166
189
  };
167
190
  const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
168
191
  if (peer)
169
- headers.set('X-Oxy-Source-Region', region);
192
+ headers.set(OXY_SOURCE_REGION_HEADER, region);
170
193
  let response;
171
194
  try {
172
195
  response = await fetcher(input, { ...init, headers });
@@ -176,7 +199,7 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
176
199
  throw error;
177
200
  }
178
201
  const targetRegion = peer
179
- ? normalizeInfrastructureRegion(response.headers.get('X-Oxy-Region') ?? undefined)
202
+ ? normalizeInfrastructureRegion(response.headers.get(OXY_REGION_HEADER) ?? undefined)
180
203
  : undefined;
181
204
  collector.record({ ...flow, targetRegion });
182
205
  collector.record({
@@ -194,11 +217,12 @@ export function instrumentTrafficFetch(fetcher, collector, service, region, reso
194
217
  // to be an internal service. Region is infrastructure metadata, never IP geo.
195
218
  const OXY_API_SERVICES = {
196
219
  'api.oxy.so': 'oxy-api',
197
- 'api.website.oxy.so': 'website',
198
220
  'website-api.oxy.so': 'website',
199
221
  'api.mention.earth': 'mention',
200
222
  'api.mercaria.co': 'mercaria',
223
+ // Old Alia app builds still call it; goes when api.alia.onl is retired (Gemies ADR 0034).
201
224
  'api.alia.onl': 'alia',
225
+ 'api.gemies.ai': 'gemies',
202
226
  'kaana.ai': 'kaana',
203
227
  'api.homiio.com': 'homiio',
204
228
  'api.syra.fm': 'syra',
package/dist/esm/index.js CHANGED
@@ -1,2 +1,23 @@
1
+ /** Request header a browser sends: the ephemeral anonymous activity ID. */
1
2
  export const OXY_ACTIVITY_ID_HEADER = 'X-Oxy-Activity-Id';
3
+ /** Request header a browser sends: the three-letter Cloudflare PoP code. */
2
4
  export const OXY_EDGE_REGION_HEADER = 'X-Oxy-Edge-Region';
5
+ /** Response header a reporting service sets: the region that processed the request. */
6
+ export const OXY_REGION_HEADER = 'X-Oxy-Region';
7
+ /** Request header one Oxy service sends another: the caller's infrastructure region. */
8
+ export 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 const OXY_TELEMETRY_REQUEST_HEADERS = Object.freeze([
16
+ OXY_ACTIVITY_ID_HEADER,
17
+ OXY_EDGE_REGION_HEADER,
18
+ ]);
19
+ /**
20
+ * Response headers a browser client may need to read
21
+ * (`Access-Control-Expose-Headers`).
22
+ */
23
+ export const OXY_TELEMETRY_EXPOSED_HEADERS = Object.freeze([OXY_REGION_HEADER]);
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Operational metrics for Oxy services, published as CloudWatch Embedded Metric
3
+ * Format (EMF) log lines.
4
+ *
5
+ * WHY EMF ON STDOUT: every Oxy backend runs on ECS with the `awslogs` driver, and
6
+ * CloudWatch Logs extracts metrics from any log event shaped as EMF — no agent,
7
+ * no sidecar, no scraper, no IAM grant beyond the logging the task already has.
8
+ * A registry that only an HTTP scrape can read (Prometheus) measures into a void
9
+ * when nothing scrapes it, which is how Mention's counters lost every value on
10
+ * every deploy.
11
+ *
12
+ * WHY AGGREGATED: values are folded in memory and flushed once per interval as
13
+ * ONE line per (metric, label set), not one line per observation. A latency is
14
+ * kept as a distribution (`Values`/`Counts`), so CloudWatch still answers
15
+ * SampleCount, p50 and p99 for it — the request COUNT is the distribution's
16
+ * SampleCount, so a service needs no separate counter for it.
17
+ *
18
+ * WHY MOST LABELS ARE NOT DIMENSIONS: every distinct combination of dimension
19
+ * values is a billed custom metric. Route x method as dimensions once measured
20
+ * 405 metrics and ~80 % of the account's CloudWatch bill (Mention #1334). So a
21
+ * label becomes a DIMENSION only when a metric definition names it in
22
+ * `dimensionSets`, and only labels with a small, closed value set belong there.
23
+ * Every other label is written as a plain property of the line: free to store,
24
+ * and grouped in Logs Insights (`stats sum(RequestLatency.Count) by Route`).
25
+ */
26
+ /** One HTTP request: SampleCount is the request count, percentiles the latency. */
27
+ export const HTTP_REQUEST_LATENCY = {
28
+ name: 'RequestLatency',
29
+ unit: 'Milliseconds',
30
+ kind: 'distribution',
31
+ dimensionSets: [['StatusClass'], ['Caller']],
32
+ };
33
+ /** One request a rate limiter refused (429), per calling application. */
34
+ export const HTTP_RATE_LIMITED = {
35
+ name: 'RateLimited',
36
+ unit: 'Count',
37
+ kind: 'sum',
38
+ dimensionSets: [['Caller']],
39
+ };
40
+ /** `2xx`, `4xx`, … — a closed set, safe as a dimension. */
41
+ export function statusClass(status) {
42
+ return Number.isInteger(status) && status >= 100 && status <= 599
43
+ ? `${Math.floor(status / 100)}xx`
44
+ : 'other';
45
+ }
46
+ /** EMF allows at most 100 distinct values per metric on one line. */
47
+ const MAX_DISTRIBUTION_VALUES = 100;
48
+ /** Neighbouring values within ~5 % share a bucket, which bounds the set. */
49
+ const BUCKET_BASE = 1.05;
50
+ const LABEL_NAME = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
51
+ const MAX_LABEL_VALUE_LENGTH = 128;
52
+ const RESERVED = new Set(['_aws', 'Service']);
53
+ /** Representative value of a bucket: values within ~5 % collapse together. */
54
+ function bucketOf(value) {
55
+ if (value <= 0)
56
+ return 0;
57
+ const exponent = Math.round(Math.log(value) / Math.log(BUCKET_BASE));
58
+ return Number((BUCKET_BASE ** exponent).toPrecision(3));
59
+ }
60
+ /** Merge adjacent buckets (keeping the larger value) until EMF accepts them. */
61
+ function boundedDistribution(buckets) {
62
+ let entries = [...buckets.entries()].sort(([left], [right]) => left - right);
63
+ while (entries.length > MAX_DISTRIBUTION_VALUES) {
64
+ const merged = [];
65
+ for (let index = 0; index < entries.length; index += 2) {
66
+ const [value, count] = entries[index];
67
+ const next = entries[index + 1];
68
+ merged.push(next ? [next[0], count + next[1]] : [value, count]);
69
+ }
70
+ entries = merged;
71
+ }
72
+ return { values: entries.map(([value]) => value), counts: entries.map(([, count]) => count) };
73
+ }
74
+ function cleanLabels(metric, labels) {
75
+ const clean = {};
76
+ for (const [name, raw] of Object.entries(labels ?? {})) {
77
+ if (!LABEL_NAME.test(name) || RESERVED.has(name) || name === metric.name)
78
+ continue;
79
+ const value = String(raw).slice(0, MAX_LABEL_VALUE_LENGTH);
80
+ clean[name] = value.length > 0 ? value : 'unknown';
81
+ }
82
+ // A dimension named by the definition must be present on every line, or
83
+ // CloudWatch drops the metric from that line.
84
+ for (const set of metric.dimensionSets ?? []) {
85
+ for (const name of set)
86
+ clean[name] ?? (clean[name] = 'unknown');
87
+ }
88
+ return clean;
89
+ }
90
+ function seriesKey(metric, labels) {
91
+ const pairs = Object.keys(labels)
92
+ .sort()
93
+ .map((name) => `${name}=${labels[name]}`);
94
+ return `${metric.name}\u0000${pairs.join('\u0000')}`;
95
+ }
96
+ /** One line on stdout. Reached through `globalThis`: this package carries no Node types. */
97
+ function defaultWrite(line) {
98
+ globalThis.process?.stdout?.write?.(`${line}\n`);
99
+ }
100
+ /** Set by ECS on every task: the one reliable signal that `awslogs` is capturing stdout. */
101
+ const ECS_METADATA_ENV = 'ECS_CONTAINER_METADATA_URI_V4';
102
+ /**
103
+ * `createEmfPublisher` on ECS, `null` everywhere else (a laptop, CI, a test):
104
+ * the gate oxy-api and Mention each repeated. `env` is injectable for tests.
105
+ */
106
+ export function createEmfPublisherIfDeployed(options, env = globalThis.process?.env ?? {}) {
107
+ return env[ECS_METADATA_ENV] ? createEmfPublisher(options) : null;
108
+ }
109
+ export function createEmfPublisher(options) {
110
+ const flushIntervalMs = options.flushIntervalMs ?? 60000;
111
+ const maxSeries = options.maxSeries ?? 2000;
112
+ const write = options.write ?? defaultWrite;
113
+ const now = options.now ?? Date.now;
114
+ let series = new Map();
115
+ const line = (entry, timestamp) => {
116
+ const { metric } = entry;
117
+ const dimensions = [
118
+ ['Service'],
119
+ ...(metric.dimensionSets ?? []).map((set) => ['Service', ...set]),
120
+ ];
121
+ let value;
122
+ if (metric.kind === 'sum')
123
+ value = entry.sum;
124
+ else if (metric.kind === 'gauge')
125
+ value = entry.last;
126
+ else {
127
+ const { values, counts } = boundedDistribution(entry.buckets);
128
+ value = {
129
+ Values: values,
130
+ Counts: counts,
131
+ Max: entry.max,
132
+ Min: entry.min,
133
+ Count: entry.count,
134
+ Sum: entry.sum,
135
+ };
136
+ }
137
+ return JSON.stringify({
138
+ _aws: {
139
+ Timestamp: timestamp,
140
+ CloudWatchMetrics: [
141
+ {
142
+ Namespace: options.namespace,
143
+ Dimensions: dimensions,
144
+ Metrics: [{ Name: metric.name, Unit: metric.unit }],
145
+ },
146
+ ],
147
+ },
148
+ ...entry.labels,
149
+ Service: options.service,
150
+ [metric.name]: value,
151
+ });
152
+ };
153
+ const flush = () => {
154
+ if (series.size === 0)
155
+ return;
156
+ const current = series;
157
+ series = new Map();
158
+ const timestamp = now();
159
+ for (const entry of current.values()) {
160
+ try {
161
+ write(line(entry, timestamp));
162
+ }
163
+ catch {
164
+ // Metrics are best effort; a failed write must never reach a request.
165
+ }
166
+ }
167
+ };
168
+ const timer = setInterval(flush, flushIntervalMs);
169
+ timer.unref?.();
170
+ return {
171
+ record(metric, value, labels) {
172
+ if (!Number.isFinite(value))
173
+ return;
174
+ if (metric.kind !== 'gauge' && value < 0)
175
+ return;
176
+ let clean = cleanLabels(metric, labels);
177
+ let key = seriesKey(metric, clean);
178
+ if (!series.has(key) && series.size >= maxSeries) {
179
+ clean = Object.fromEntries(Object.keys(clean).map((name) => [name, 'other']));
180
+ key = seriesKey(metric, clean);
181
+ }
182
+ let entry = series.get(key);
183
+ if (!entry) {
184
+ entry = {
185
+ metric,
186
+ labels: clean,
187
+ sum: 0,
188
+ last: 0,
189
+ count: 0,
190
+ min: Number.POSITIVE_INFINITY,
191
+ max: Number.NEGATIVE_INFINITY,
192
+ buckets: new Map(),
193
+ };
194
+ series.set(key, entry);
195
+ }
196
+ entry.sum += value;
197
+ entry.last = value;
198
+ entry.count += 1;
199
+ entry.min = Math.min(entry.min, value);
200
+ entry.max = Math.max(entry.max, value);
201
+ if (metric.kind === 'distribution') {
202
+ const bucket = bucketOf(value);
203
+ entry.buckets.set(bucket, (entry.buckets.get(bucket) ?? 0) + 1);
204
+ }
205
+ },
206
+ flush,
207
+ stop() {
208
+ clearInterval(timer);
209
+ flush();
210
+ },
211
+ };
212
+ }
@@ -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
  }