@haystackeditor/cli 0.24.1 → 0.25.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/dist/assets/capture/capture.cb4204fcc997d8e8.js +2 -0
- package/dist/assets/capture/release.json +4 -0
- package/dist/assets/telemetry/runtime.cjs +829 -1254
- package/dist/capture/adapters/client-routes.js +383 -0
- package/dist/capture/adapters/django.js +127 -0
- package/dist/capture/adapters/files.js +77 -0
- package/dist/capture/adapters/index.js +64 -0
- package/dist/capture/adapters/jsx-edit.js +81 -0
- package/dist/capture/adapters/next.js +327 -0
- package/dist/capture/adapters/nuxt.js +192 -0
- package/dist/capture/adapters/rails.js +171 -0
- package/dist/capture/adapters/react-router.js +432 -0
- package/dist/capture/adapters/sveltekit.js +102 -0
- package/dist/capture/adapters/types.js +4 -0
- package/dist/capture/adapters/vite.js +121 -0
- package/dist/capture/app-config.js +106 -0
- package/dist/capture/consent.js +127 -0
- package/dist/capture/csp.js +331 -0
- package/dist/capture/html.js +74 -0
- package/dist/capture/js-ast.js +400 -0
- package/dist/capture/manifest.js +95 -0
- package/dist/capture/project.js +177 -0
- package/dist/capture/route-pattern.js +119 -0
- package/dist/capture/script-release.js +47 -0
- package/dist/capture/tag.js +74 -0
- package/dist/capture-step.js +53 -0
- package/dist/commands/capture-brief.js +92 -0
- package/dist/commands/capture-contract.js +46 -0
- package/dist/commands/capture-manifest.js +78 -0
- package/dist/commands/init-capture.js +409 -0
- package/dist/commands/init-telemetry.js +1011 -0
- package/dist/commands/init.js +68 -5
- package/dist/commands/server-telemetry-contract.d.ts +66 -0
- package/dist/commands/server-telemetry-contract.js +127 -0
- package/dist/commands/telemetry-token.js +238 -0
- package/dist/commands/telemetry.d.ts +161 -8
- package/dist/commands/telemetry.js +940 -158
- package/dist/commands/verify-onboarding.js +5 -1
- package/dist/commands/verify.js +54 -7
- package/dist/index.js +83 -6
- package/dist/schema.js +2 -2
- package/dist/telemetry/next-loader.cjs +66 -9
- package/dist/telemetry/next.d.ts +11 -3
- package/dist/telemetry/next.js +95 -15
- package/dist/telemetry/typed-source.d.ts +47 -0
- package/dist/telemetry/typed-source.js +379 -0
- package/package.json +4 -2
- package/schemas/init.v1.json +63 -4
- package/schemas/pre-verify.v1.json +60 -3
|
@@ -1,379 +1,152 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/*
|
|
4
|
-
* Haystack's automatically injected Node telemetry runtime
|
|
4
|
+
* Haystack's automatically injected Node telemetry runtime: sampler version 5
|
|
5
|
+
* (CAPTURE-V1 rule 9, infra/lambda/haystack-design-verifier-shared/capture_contracts.ts).
|
|
5
6
|
*
|
|
6
|
-
* This file is copied beside compiled application code by
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* This file is copied beside compiled application code by `haystack telemetry
|
|
8
|
+
* instrument` (or the Next.js wrapper). It has no package dependencies, never
|
|
9
|
+
* records a value (only its shape and size, or an approved setting literal),
|
|
10
|
+
* and is inert unless all three HAYSTACK_TELEMETRY_* variables are present.
|
|
11
|
+
*
|
|
12
|
+
* Rule 9(a): every call of a site is counted (one increment); the first
|
|
13
|
+
* SERVER_SAMPLE_AFTER_CALLS calls of a site in this process are classified
|
|
14
|
+
* (the census), later ones with probability 1/SERVER_SAMPLE_RATE from a per-site
|
|
15
|
+
* PRNG; a per-second budget bounds classification and counts what it skipped.
|
|
16
|
+
* A flush sends per site its window's calls, census, sampled and skipped deltas.
|
|
17
|
+
*
|
|
18
|
+
* Rule 13(b), do no harm:
|
|
19
|
+
* - every probe returns its argument unchanged and no exception leaves this
|
|
20
|
+
* file; classification never runs customer code (no getters, proxy traps,
|
|
21
|
+
* valueOf or Symbol.toPrimitive), and nothing it sends is serialized by
|
|
22
|
+
* JSON.stringify of an object (which would look up an inherited toJSON):
|
|
23
|
+
* bodies are assembled from primitives only;
|
|
24
|
+
* - every byte it keeps (site metadata, field keys, counters, a window being
|
|
25
|
+
* serialized, bodies waiting to be sent) is accounted against
|
|
26
|
+
* SERVER_RUNTIME_MAX_BYTES: new categories past the counters' share become
|
|
27
|
+
* `overflow`, and anything that would cross the cap turns telemetry off;
|
|
28
|
+
* - flushes run on an unref'd timer, one send in flight, each bounded by
|
|
29
|
+
* SERVER_SEND_TIMEOUT_MS; three failed sends stop sending and counting;
|
|
30
|
+
* - the process exits as it would without telemetry: the beforeExit flush has
|
|
31
|
+
* one absolute SERVER_EXIT_FLUSH_MS deadline across everything it does, and
|
|
32
|
+
* the exit and crash paths make at most one bounded synchronous write;
|
|
33
|
+
* - no process handlers but the passive uncaughtExceptionMonitor, and no
|
|
34
|
+
* output at all (a stalled stderr pipe could block the event loop); why it
|
|
35
|
+
* turned itself off is in status().
|
|
11
36
|
*/
|
|
12
37
|
|
|
13
38
|
const { createHash, randomBytes } = require('node:crypto');
|
|
14
|
-
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
15
|
-
const diagnosticsChannel = require('node:diagnostics_channel');
|
|
16
39
|
const { types: utilTypes } = require('node:util');
|
|
17
|
-
const {
|
|
40
|
+
const { readFileSync, writeFileSync, promises: fsPromises } = require('node:fs');
|
|
18
41
|
const { tmpdir } = require('node:os');
|
|
19
42
|
const { join } = require('node:path');
|
|
20
43
|
|
|
21
44
|
// Capture the few mutable language intrinsics used on customer values before
|
|
22
45
|
// application code runs. Calling a customer-patched prototype method from a
|
|
23
46
|
// probe would itself be an application-visible side effect.
|
|
24
|
-
const safeStringTrim = Function.call.bind(String.prototype.trim);
|
|
25
47
|
const safeString = String;
|
|
26
|
-
const safeStringStartsWith = Function.call.bind(String.prototype.startsWith);
|
|
27
|
-
const safeStringEndsWith = Function.call.bind(String.prototype.endsWith);
|
|
28
|
-
const safeStringIndexOf = Function.call.bind(String.prototype.indexOf);
|
|
29
48
|
const safeStringSlice = Function.call.bind(String.prototype.slice);
|
|
30
49
|
const safeStringCharCodeAt = Function.call.bind(String.prototype.charCodeAt);
|
|
50
|
+
const safeStringStartsWith = Function.call.bind(String.prototype.startsWith);
|
|
31
51
|
const safeRegExpTest = Function.call.bind(RegExp.prototype.test);
|
|
32
52
|
const safeJsonParse = JSON.parse;
|
|
53
|
+
// Only ever called on primitives (strings, finite numbers): JSON.stringify
|
|
54
|
+
// looks up toJSON on objects, never on primitives.
|
|
33
55
|
const safeJsonStringify = JSON.stringify;
|
|
34
56
|
const safeArrayIsArray = Array.isArray;
|
|
35
|
-
const
|
|
36
|
-
const
|
|
37
|
-
const safeArrayFlatMap = Function.call.bind(Array.prototype.flatMap);
|
|
38
|
-
const safeArrayMap = Function.call.bind(Array.prototype.map);
|
|
39
|
-
const safeArrayReduce = Function.call.bind(Array.prototype.reduce);
|
|
40
|
-
const safeArrayShift = Function.call.bind(Array.prototype.shift);
|
|
41
|
-
const safeArraySlice = Function.call.bind(Array.prototype.slice);
|
|
57
|
+
const safeArrayPush = Function.call.bind(Array.prototype.push);
|
|
58
|
+
const safeArrayJoin = Function.call.bind(Array.prototype.join);
|
|
42
59
|
const safeArraySort = Function.call.bind(Array.prototype.sort);
|
|
43
|
-
const
|
|
44
|
-
const safeNumberIsFinite = Number.isFinite;
|
|
60
|
+
const safeArrayShift = Function.call.bind(Array.prototype.shift);
|
|
45
61
|
const safeNumberIsSafeInteger = Number.isSafeInteger;
|
|
46
62
|
const safeNumberParseInt = Number.parseInt;
|
|
47
63
|
const safeMathImul = Math.imul;
|
|
48
|
-
const safeMathMax = Math.max;
|
|
49
64
|
const safeMathMin = Math.min;
|
|
50
65
|
const safeBufferByteLength = Buffer.byteLength;
|
|
51
|
-
const safeObjectKeys = Object.keys;
|
|
52
|
-
const safeObjectEntries = Object.entries;
|
|
53
66
|
const safeObjectCreate = Object.create;
|
|
54
67
|
const safeObjectFreeze = Object.freeze;
|
|
55
68
|
const safeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
56
69
|
const safeGetPrototypeOf = Object.getPrototypeOf;
|
|
57
70
|
const safeHasOwn = Function.call.bind(Object.prototype.hasOwnProperty);
|
|
71
|
+
const ObjectPrototype = Object.prototype;
|
|
72
|
+
const SafeMap = Map;
|
|
58
73
|
const safeMapGet = Function.call.bind(Map.prototype.get);
|
|
59
74
|
const safeMapSet = Function.call.bind(Map.prototype.set);
|
|
60
|
-
const safeMapHas = Function.call.bind(Map.prototype.has);
|
|
61
|
-
const safeMapSize = Function.call.bind(Object.getOwnPropertyDescriptor(Map.prototype, 'size').get);
|
|
62
75
|
const safeMapEntries = Function.call.bind(Map.prototype.entries);
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const safeSetAdd = Function.call.bind(Set.prototype.add);
|
|
67
|
-
const safeSetValues = Function.call.bind(Set.prototype.values);
|
|
68
|
-
const safeArrayPush = Function.call.bind(Array.prototype.push);
|
|
69
|
-
const SafeMap = Map;
|
|
70
|
-
const SafeSet = Set;
|
|
71
|
-
const SafeWeakMap = WeakMap;
|
|
76
|
+
const safeMapSize = Function.call.bind(Object.getOwnPropertyDescriptor(Map.prototype, 'size').get);
|
|
77
|
+
const safeSetSize = Function.call.bind(Object.getOwnPropertyDescriptor(Set.prototype, 'size').get);
|
|
78
|
+
const safeMapIteratorNext = Function.call.bind(Object.getPrototypeOf(safeMapEntries(new SafeMap())).next);
|
|
72
79
|
const SafeDate = Date;
|
|
73
80
|
const safeDateNow = Date.now;
|
|
74
81
|
const safeDateToISOString = Function.call.bind(Date.prototype.toISOString);
|
|
75
|
-
const safeMapIteratorNext = Function.call.bind(
|
|
76
|
-
Object.getPrototypeOf(safeMapEntries(new SafeMap())).next,
|
|
77
|
-
);
|
|
78
|
-
const safeSetIteratorNext = Function.call.bind(
|
|
79
|
-
Object.getPrototypeOf(safeSetValues(new SafeSet())).next,
|
|
80
|
-
);
|
|
81
82
|
const hashPrototype = Object.getPrototypeOf(createHash('sha256'));
|
|
82
83
|
const safeHashUpdate = Function.call.bind(hashPrototype.update);
|
|
83
84
|
const safeHashDigest = Function.call.bind(hashPrototype.digest);
|
|
84
85
|
const safeIsProxy = utilTypes.isProxy;
|
|
86
|
+
const safeIsMap = utilTypes.isMap;
|
|
87
|
+
const safeIsSet = utilTypes.isSet;
|
|
85
88
|
const safeIsNativeError = utilTypes.isNativeError;
|
|
86
|
-
const
|
|
87
|
-
const safeActionEnterWith = Function.call.bind(AsyncLocalStorage.prototype.enterWith);
|
|
89
|
+
const SafePromise = Promise;
|
|
88
90
|
const safePromiseFinally = Function.call.bind(Promise.prototype.finally);
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
const safePromiseThen = Function.call.bind(Promise.prototype.then);
|
|
92
|
+
// Sends are one HTTP/1.1 request written on a socket of their own: the socket
|
|
93
|
+
// is unref'd, so a send in flight never keeps the process alive (fetch's would,
|
|
94
|
+
// up to its timeout, past the application's own end), and no HTTP client code
|
|
95
|
+
// runs (node:http checks parser results with `instanceof Error`, which would
|
|
96
|
+
// call a customer's Error[Symbol.hasInstance]).
|
|
97
|
+
const { connect: safeNetConnect, isIP: safeIsIP } = require('node:net');
|
|
98
|
+
const { connect: safeTlsConnect } = require('node:tls');
|
|
99
|
+
const safeBufferToString = Function.call.bind(Buffer.prototype.toString);
|
|
100
|
+
const safeStringIndexOf = Function.call.bind(String.prototype.indexOf);
|
|
101
|
+
const safeSetTimeout = setTimeout;
|
|
102
|
+
const safeClearTimeout = clearTimeout;
|
|
103
|
+
const safeSetInterval = setInterval;
|
|
97
104
|
const safeClearInterval = clearInterval;
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
syntax: SyntaxError.prototype,
|
|
103
|
-
reference: ReferenceError.prototype,
|
|
104
|
-
aggregate: typeof AggregateError === 'function' ? AggregateError.prototype : null,
|
|
105
|
-
uri: URIError.prototype,
|
|
106
|
-
eval: EvalError.prototype,
|
|
107
|
-
generic: Error.prototype,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
function compareStrings(left, right) {
|
|
111
|
-
return left < right ? -1 : left > right ? 1 : 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function mapEntriesArray(map) {
|
|
115
|
-
const result = [];
|
|
116
|
-
const iterator = safeMapEntries(map);
|
|
117
|
-
while (true) {
|
|
118
|
-
const step = safeMapIteratorNext(iterator);
|
|
119
|
-
if (step.done) return result;
|
|
120
|
-
safeArrayPush(result, step.value);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function mapValuesArray(map) {
|
|
125
|
-
const entries = mapEntriesArray(map);
|
|
126
|
-
const result = [];
|
|
127
|
-
for (let index = 0; index < entries.length; index++) safeArrayPush(result, entries[index][1]);
|
|
128
|
-
return result;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function setValuesArray(set) {
|
|
132
|
-
const result = [];
|
|
133
|
-
const iterator = safeSetValues(set);
|
|
134
|
-
while (true) {
|
|
135
|
-
const step = safeSetIteratorNext(iterator);
|
|
136
|
-
if (step.done) return result;
|
|
137
|
-
safeArrayPush(result, step.value);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function objectFromEntryArray(entries) {
|
|
142
|
-
const result = safeObjectCreate(null);
|
|
143
|
-
for (let index = 0; index < entries.length; index++) {
|
|
144
|
-
const entry = entries[index];
|
|
145
|
-
result[entry[0]] = entry[1];
|
|
146
|
-
}
|
|
147
|
-
return result;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function objectFromMap(map) {
|
|
151
|
-
return objectFromEntryArray(mapEntriesArray(map));
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
function sha256Hex(value) {
|
|
155
|
-
const hash = createHash('sha256');
|
|
156
|
-
safeHashUpdate(hash, value);
|
|
157
|
-
return safeHashDigest(hash, 'hex');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function responseAccepted(response) {
|
|
161
|
-
try {
|
|
162
|
-
return safeResponseOk ? safeResponseOk(response) : false;
|
|
163
|
-
} catch {
|
|
164
|
-
return false;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
105
|
+
const safeSetImmediate = setImmediate;
|
|
106
|
+
const noop = () => {};
|
|
107
|
+
const nextTurn = () => new SafePromise(resolve => safeSetImmediate(resolve));
|
|
108
|
+
const processHost = require('node:process');
|
|
167
109
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
} catch {
|
|
172
|
-
return 0;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
110
|
+
// The contract's sampler constants and category vocabulary, substituted from
|
|
111
|
+
// the CLI's mirror of capture_contracts.ts when the runtime is authenticated.
|
|
112
|
+
const CONTRACT = /* __HAYSTACK_SERVER_CONTRACT__ */ null;
|
|
175
113
|
|
|
176
|
-
const RUNTIME_VERSION =
|
|
177
|
-
const SAMPLER_VERSION = 4;
|
|
114
|
+
const RUNTIME_VERSION = 2;
|
|
178
115
|
const DEFAULT_FLUSH_INTERVAL_MS = 60_000;
|
|
179
|
-
const DEFAULT_SITE_OBSERVATIONS = 8;
|
|
180
|
-
const MAX_SITE_OBSERVATIONS = 4_096;
|
|
181
|
-
const DEFAULT_PROCESS_PROBE_OBSERVATIONS = 4_096;
|
|
182
|
-
const MAX_PROCESS_PROBE_OBSERVATIONS = 65_536;
|
|
183
|
-
const MAX_SAFE_COUNT = Number.MAX_SAFE_INTEGER;
|
|
184
|
-
const MAX_COUNTER_KEYS = 50_000;
|
|
185
|
-
const MAX_EXECUTION_FAMILIES = 64;
|
|
186
|
-
const MAX_EXECUTION_EVENTS = 48;
|
|
187
|
-
const MAX_EXECUTION_ACTION_BYTES = 24 * 1024;
|
|
188
|
-
const MAX_EXECUTION_BYTES = 1024 * 1024;
|
|
189
116
|
const MAX_PAYLOAD_BYTES = 4 * 1024 * 1024;
|
|
190
117
|
const PAYLOAD_CHUNK_BUDGET = MAX_PAYLOAD_BYTES - 256 * 1024;
|
|
118
|
+
// Crash and exit spool: at most MAX_SPOOL_FILES fixed slots of at most
|
|
119
|
+
// SPOOL_FILE_BYTES each, so a slot needs no directory listing to stay bounded.
|
|
191
120
|
const MAX_SPOOL_FILES = 8;
|
|
192
|
-
const
|
|
193
|
-
const SAMPLE_FIRST_ACTIONS = 4;
|
|
194
|
-
const DEFAULT_PROCESS_SAMPLE_RATE = 1;
|
|
195
|
-
const DEFAULT_ACTION_SAMPLE_RATE = 64;
|
|
196
|
-
const MAX_ACTION_SAMPLE_RATE = 4_096;
|
|
121
|
+
const SPOOL_FILE_BYTES = 2 * 1024 * 1024;
|
|
197
122
|
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
123
|
+
// Sites serialized per event-loop turn by an interval flush.
|
|
124
|
+
const FLUSH_SLICE_SITES = 500;
|
|
125
|
+
// Memory accounting (bytes): what a site's state, metadata and lookup entry
|
|
126
|
+
// keep beyond their strings; a string's header; a Map's and a Map entry's
|
|
127
|
+
// footprint. Estimates on the high side, so the cap is a true upper bound.
|
|
128
|
+
const SITE_BYTES = 480;
|
|
129
|
+
const STRING_BYTES = 24;
|
|
130
|
+
const MAP_BYTES = 160;
|
|
131
|
+
const ENTRY_BYTES = 96;
|
|
132
|
+
// Of the cap: what a window's counters may use before new categories become
|
|
133
|
+
// overflow (the rest serializes them into bodies), and what is kept back for
|
|
134
|
+
// overflow entries themselves.
|
|
135
|
+
const COUNTER_SHARE = 0.4;
|
|
136
|
+
const RESERVE_BYTES = 1024 * 1024;
|
|
198
137
|
const CONTROL_GLOBAL = '__haystackTelemetryControlV1';
|
|
199
138
|
const CONTROL_CHANNEL = '__HAYSTACK_TELEMETRY_CONTROL_CHANNEL__';
|
|
200
139
|
const RUNTIME_INTEGRITY = '__HAYSTACK_TELEMETRY_RUNTIME_INTEGRITY__';
|
|
201
|
-
const
|
|
140
|
+
const MANIFEST_SCHEMA_VERSION = 'haystack-node-telemetry-instrumentation-v4';
|
|
202
141
|
|
|
203
142
|
let installed = false;
|
|
143
|
+
let installResult = null;
|
|
204
144
|
let activeControl = null;
|
|
205
|
-
const processHost = require('node:process');
|
|
206
|
-
|
|
207
|
-
function selectProbe(name, fallback) {
|
|
208
|
-
try {
|
|
209
|
-
const selected = activeControl?.selectProbe(name);
|
|
210
|
-
return typeof selected === 'function' ? selected : fallback;
|
|
211
|
-
} catch {
|
|
212
|
-
return fallback;
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
145
|
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
Object.defineProperty(trustedSelectProbe, 'haystackRuntimeIntegrity', {
|
|
221
|
-
configurable: false,
|
|
222
|
-
enumerable: false,
|
|
223
|
-
writable: false,
|
|
224
|
-
value: RUNTIME_INTEGRITY,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
function safeSelectProbe(expectedIntegrity, name, fallback) {
|
|
228
|
-
try {
|
|
229
|
-
if (expectedIntegrity !== RUNTIME_INTEGRITY) return fallback;
|
|
230
|
-
return trustedSelectProbe(name, fallback);
|
|
231
|
-
} catch {
|
|
232
|
-
return fallback;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function safeSelectSiteProbe(expectedIntegrity, name, site, fallback) {
|
|
237
|
-
try {
|
|
238
|
-
if (expectedIntegrity !== RUNTIME_INTEGRITY) return fallback;
|
|
239
|
-
const selected = activeControl?.selectSiteProbe(name, site);
|
|
240
|
-
return typeof selected === 'function' ? selected : fallback;
|
|
241
|
-
} catch {
|
|
242
|
-
return fallback;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function debug(message, error) {
|
|
247
|
-
if (process.env.HAYSTACK_TELEMETRY_DEBUG !== '1') return;
|
|
248
|
-
const detail = safeIsNativeError(error) ? ': native error' : error ? ': telemetry error' : '';
|
|
249
|
-
process.stderr.write(`[haystack-telemetry] ${message}${detail}\n`);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function safeSegment(value, fallback) {
|
|
253
|
-
return typeof value === 'string' && safeRegExpTest(/^[A-Za-z0-9._-]{1,128}$/, value) ? value : fallback;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function sizeBucket(length) {
|
|
257
|
-
if (length === 0) return 'empty';
|
|
258
|
-
if (length === 1) return 'one';
|
|
259
|
-
if (length <= 4) return 'few';
|
|
260
|
-
return 'many';
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function stringForm(value) {
|
|
264
|
-
if (value.length === 0) return 'empty';
|
|
265
|
-
const trimmed = safeStringTrim(value);
|
|
266
|
-
if (!trimmed) return 'whitespace';
|
|
267
|
-
// Bound parsing work. Long strings still report their generic form/size;
|
|
268
|
-
// their contents are never retained or transmitted.
|
|
269
|
-
if (trimmed.length <= 4_096 && safeStringStartsWith(trimmed, '```') && safeStringEndsWith(trimmed, '```')) {
|
|
270
|
-
const newline = safeStringIndexOf(trimmed, '\n');
|
|
271
|
-
if (newline >= 0) {
|
|
272
|
-
const body = safeStringTrim(safeStringSlice(trimmed, newline + 1, -3));
|
|
273
|
-
try {
|
|
274
|
-
const parsed = safeJsonParse(body);
|
|
275
|
-
return safeArrayIsArray(parsed)
|
|
276
|
-
? 'markdown-fenced-json-array'
|
|
277
|
-
: parsed !== null && typeof parsed === 'object'
|
|
278
|
-
? 'markdown-fenced-json-object'
|
|
279
|
-
: 'markdown-fenced-json-scalar';
|
|
280
|
-
} catch {
|
|
281
|
-
return 'markdown-fenced-text';
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
return 'markdown-fenced-text';
|
|
285
|
-
}
|
|
286
|
-
if (trimmed.length <= 4_096) {
|
|
287
|
-
try {
|
|
288
|
-
const parsed = safeJsonParse(trimmed);
|
|
289
|
-
return safeArrayIsArray(parsed)
|
|
290
|
-
? 'json-array'
|
|
291
|
-
: parsed !== null && typeof parsed === 'object'
|
|
292
|
-
? 'json-object'
|
|
293
|
-
: 'json-scalar';
|
|
294
|
-
} catch {
|
|
295
|
-
// Ordinary text is the expected case.
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
return 'text';
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function valueShape(value) {
|
|
302
|
-
if (value === null) return 'null';
|
|
303
|
-
if (value === undefined) return 'undefined';
|
|
304
|
-
if (typeof value === 'string') return `string:${stringForm(value)}:${sizeBucket(value.length)}`;
|
|
305
|
-
if (typeof value === 'number') return safeNumberIsFinite(value) ? 'number:finite' : 'number:nonfinite';
|
|
306
|
-
if (typeof value === 'boolean') return 'boolean';
|
|
307
|
-
if (typeof value === 'object' && safeIsProxy(value)) return 'object:uninspectable';
|
|
308
|
-
if (safeArrayIsArray(value)) return `array:${sizeBucket(value.length)}`;
|
|
309
|
-
if (typeof value === 'object') {
|
|
310
|
-
try {
|
|
311
|
-
if (safeIsNativeError(value)) return `error:${errorClass(value)}`;
|
|
312
|
-
return `object:${sizeBucket(safeObjectKeys(value).length)}`;
|
|
313
|
-
} catch {
|
|
314
|
-
return 'object:uninspectable';
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
return `nonjson:${typeof value}`;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
function stableIdentity(prefix, parts) {
|
|
321
|
-
let framed = '';
|
|
322
|
-
for (let index = 0; index < parts.length; index++) {
|
|
323
|
-
if (index > 0) framed += '|';
|
|
324
|
-
const part = parts[index];
|
|
325
|
-
framed += `${safeBufferByteLength(part, 'utf8')}:${part}`;
|
|
326
|
-
}
|
|
327
|
-
return `${prefix}-${safeStringSlice(sha256Hex(framed), 0, 24)}`;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function canonicalJson(value) {
|
|
331
|
-
if (value === null || typeof value !== 'object') return safeJsonStringify(value);
|
|
332
|
-
if (safeArrayIsArray(value)) {
|
|
333
|
-
let result = '[';
|
|
334
|
-
for (let index = 0; index < value.length; index++) {
|
|
335
|
-
if (index > 0) result += ',';
|
|
336
|
-
result += canonicalJson(value[index]);
|
|
337
|
-
}
|
|
338
|
-
return `${result}]`;
|
|
339
|
-
}
|
|
340
|
-
const keys = safeArraySort(safeObjectKeys(value), compareStrings);
|
|
341
|
-
let result = '{';
|
|
342
|
-
for (let index = 0; index < keys.length; index++) {
|
|
343
|
-
if (index > 0) result += ',';
|
|
344
|
-
const key = keys[index];
|
|
345
|
-
result += `${safeJsonStringify(key)}:${canonicalJson(value[key])}`;
|
|
346
|
-
}
|
|
347
|
-
return `${result}}`;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function errorClass(value) {
|
|
351
|
-
try {
|
|
352
|
-
if (value !== null && typeof value === 'object' && safeIsProxy(value)) return 'uninspectable';
|
|
353
|
-
if (safeIsNativeError(value)) {
|
|
354
|
-
// Compare the direct native prototype with references captured before
|
|
355
|
-
// application code. This retains common built-in classes without
|
|
356
|
-
// invoking mutable Symbol.hasInstance/name/tag hooks.
|
|
357
|
-
const prototype = safeGetPrototypeOf(value);
|
|
358
|
-
if (prototype === nativeErrorPrototypes.type) return 'type';
|
|
359
|
-
if (prototype === nativeErrorPrototypes.range) return 'range';
|
|
360
|
-
if (prototype === nativeErrorPrototypes.syntax) return 'syntax';
|
|
361
|
-
if (prototype === nativeErrorPrototypes.reference) return 'reference';
|
|
362
|
-
if (prototype === nativeErrorPrototypes.aggregate) return 'aggregate';
|
|
363
|
-
if (prototype === nativeErrorPrototypes.uri) return 'uri';
|
|
364
|
-
if (prototype === nativeErrorPrototypes.eval) return 'eval';
|
|
365
|
-
return 'generic';
|
|
366
|
-
}
|
|
367
|
-
} catch {
|
|
368
|
-
return 'uninspectable';
|
|
369
|
-
}
|
|
370
|
-
if (value === null) return 'non-error-null';
|
|
371
|
-
return `non-error-${typeof value}`;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function httpMethod(value) {
|
|
375
|
-
return value === 'GET' || value === 'POST' || value === 'PUT' || value === 'PATCH' ||
|
|
376
|
-
value === 'DELETE' || value === 'HEAD' || value === 'OPTIONS' ? value : 'OTHER';
|
|
146
|
+
function sha256Hex(value) {
|
|
147
|
+
const hash = createHash('sha256');
|
|
148
|
+
safeHashUpdate(hash, value);
|
|
149
|
+
return safeHashDigest(hash, 'hex');
|
|
377
150
|
}
|
|
378
151
|
|
|
379
152
|
function hashSeed(value) {
|
|
@@ -382,15 +155,24 @@ function hashSeed(value) {
|
|
|
382
155
|
hash ^= safeStringCharCodeAt(value, index);
|
|
383
156
|
hash = safeMathImul(hash, 0x01000193);
|
|
384
157
|
}
|
|
158
|
+
// xorshift must never be seeded with 0.
|
|
385
159
|
return (hash >>> 0) || 1;
|
|
386
160
|
}
|
|
387
161
|
|
|
162
|
+
function stringBytes(value) {
|
|
163
|
+
return STRING_BYTES + value.length * 2;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function safeSegment(value, fallback) {
|
|
167
|
+
return typeof value === 'string' && safeRegExpTest(/^[A-Za-z0-9._-]{1,128}$/, value) ? value : fallback;
|
|
168
|
+
}
|
|
169
|
+
|
|
388
170
|
function validEndpoint(raw) {
|
|
389
171
|
try {
|
|
390
172
|
const endpoint = new URL(raw);
|
|
391
173
|
if (endpoint.protocol === 'https:') return endpoint.toString();
|
|
392
174
|
if (endpoint.protocol === 'http:' && (endpoint.hostname === '127.0.0.1' ||
|
|
393
|
-
endpoint.hostname === 'localhost' || endpoint.hostname === '::1')) {
|
|
175
|
+
endpoint.hostname === 'localhost' || endpoint.hostname === '[::1]')) {
|
|
394
176
|
return endpoint.toString();
|
|
395
177
|
}
|
|
396
178
|
} catch {
|
|
@@ -399,82 +181,137 @@ function validEndpoint(raw) {
|
|
|
399
181
|
return null;
|
|
400
182
|
}
|
|
401
183
|
|
|
184
|
+
/** A JSON string literal of a primitive string. */
|
|
185
|
+
function quote(value) {
|
|
186
|
+
return safeJsonStringify(value);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Rule 9(c): an approved literal is a boolean, a safe integer or a short
|
|
190
|
+
* printable string, and its category is `setting:` plus exactly this JSON. */
|
|
191
|
+
function literalJson(literal) {
|
|
192
|
+
if (literal === true) return 'true';
|
|
193
|
+
if (literal === false) return 'false';
|
|
194
|
+
if (typeof literal === 'number') return `${literal}`;
|
|
195
|
+
return quote(literal);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validSettingLiteral(literal) {
|
|
199
|
+
if (typeof literal === 'boolean') return true;
|
|
200
|
+
if (typeof literal === 'number') return safeNumberIsSafeInteger(literal);
|
|
201
|
+
if (typeof literal !== 'string' || literal.length < 1 || literal.length > CONTRACT.SETTINGS_MAX_LITERAL_CHARS) return false;
|
|
202
|
+
for (let index = 0; index < literal.length; index++) {
|
|
203
|
+
const code = safeStringCharCodeAt(literal, index);
|
|
204
|
+
if (code < 0x20 || (code >= 0xd800 && code <= 0xdfff)) return false;
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** An own data property of a parsed manifest object (never an inherited one). */
|
|
210
|
+
function own(object, key) {
|
|
211
|
+
if (object === null || typeof object !== 'object' || !safeHasOwn(object, key)) return undefined;
|
|
212
|
+
const descriptor = safeGetOwnPropertyDescriptor(object, key);
|
|
213
|
+
return descriptor && safeHasOwn(descriptor, 'value') ? descriptor.value : undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
402
216
|
function loadInstrumentation() {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
217
|
+
const FIELD_NAME = new RegExp(CONTRACT.SERVER_FIELD_NAME_PATTERN);
|
|
218
|
+
const manifest = safeJsonParse(readFileSync(join(__dirname, 'instrumentation.json'), 'utf8'));
|
|
219
|
+
const sitesIn = own(manifest, 'sites');
|
|
220
|
+
if (own(manifest, 'schema_version') !== MANIFEST_SCHEMA_VERSION || !safeArrayIsArray(sitesIn)) return null;
|
|
221
|
+
const sites = [];
|
|
222
|
+
let bytes = 0;
|
|
223
|
+
for (let index = 0; index < sitesIn.length; index++) {
|
|
224
|
+
const site = sitesIn[index];
|
|
225
|
+
const strings = ['probe_key', 'site_id', 'source_path', 'qualified_name', 'probe_kind', 'owner_key', 'structural_key'];
|
|
226
|
+
const values = [];
|
|
227
|
+
for (let position = 0; position < strings.length; position++) safeArrayPush(values, own(site, strings[position]));
|
|
228
|
+
let valid = true;
|
|
229
|
+
for (let position = 0; position < values.length; position++) if (typeof values[position] !== 'string') valid = false;
|
|
230
|
+
if (!valid) continue;
|
|
231
|
+
const label = own(site, 'label');
|
|
232
|
+
const line = own(site, 'line');
|
|
233
|
+
const column = own(site, 'column');
|
|
234
|
+
const allowed = own(site, 'allowed_fields');
|
|
235
|
+
const fields = [];
|
|
236
|
+
if (safeArrayIsArray(allowed)) {
|
|
237
|
+
for (let position = 0; position < allowed.length && fields.length < CONTRACT.SERVER_FIELDS_PER_SITE; position++) {
|
|
238
|
+
const field = allowed[position];
|
|
239
|
+
if (typeof field === 'string' && safeRegExpTest(FIELD_NAME, field)) safeArrayPush(fields, field);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const declared = own(site, 'settings');
|
|
243
|
+
let settings = null;
|
|
244
|
+
if (safeArrayIsArray(declared) && declared.length >= 1 && declared.length <= CONTRACT.SETTINGS_MAX_DOMAIN + 2) {
|
|
245
|
+
settings = [];
|
|
246
|
+
for (let position = 0; position < declared.length; position++) {
|
|
247
|
+
if (!validSettingLiteral(declared[position])) {
|
|
248
|
+
settings = null;
|
|
249
|
+
break;
|
|
250
|
+
}
|
|
251
|
+
safeArrayPush(settings, declared[position]);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// A telemetry-owned, null-prototype record of primitives: what a flush
|
|
255
|
+
// serializes for the site (ServerSiteV5) without any object reaching
|
|
256
|
+
// JSON.stringify.
|
|
257
|
+
const metadata = safeObjectCreate(null);
|
|
258
|
+
metadata.probeKey = values[0];
|
|
259
|
+
metadata.siteId = values[1];
|
|
260
|
+
metadata.sourcePath = values[2];
|
|
261
|
+
metadata.qualifiedName = values[3];
|
|
262
|
+
metadata.probeKind = values[4];
|
|
263
|
+
metadata.ownerKey = values[5];
|
|
264
|
+
metadata.structuralKey = values[6];
|
|
265
|
+
metadata.label = typeof label === 'string' && label.length > 0 ? label : null;
|
|
266
|
+
metadata.line = safeNumberIsSafeInteger(line) && line >= 1 ? line : null;
|
|
267
|
+
metadata.column = metadata.line !== null && safeNumberIsSafeInteger(column) && column >= 0 ? column : null;
|
|
268
|
+
metadata.fields = fields;
|
|
269
|
+
metadata.settings = settings;
|
|
270
|
+
safeArrayPush(sites, metadata);
|
|
271
|
+
bytes += SITE_BYTES;
|
|
272
|
+
for (let position = 0; position < values.length; position++) bytes += stringBytes(values[position]);
|
|
273
|
+
if (metadata.label !== null) bytes += stringBytes(metadata.label);
|
|
274
|
+
for (let position = 0; position < fields.length; position++) bytes += 8 + stringBytes(fields[position]);
|
|
275
|
+
if (settings !== null) {
|
|
276
|
+
bytes += MAP_BYTES;
|
|
277
|
+
for (let position = 0; position < settings.length; position++) {
|
|
278
|
+
bytes += ENTRY_BYTES + stringBytes(`setting:${literalJson(settings[position])}`);
|
|
279
|
+
}
|
|
431
280
|
}
|
|
432
|
-
const entryFile = safeArrayIsArray(manifest.instrumented_files)
|
|
433
|
-
? safeArrayFind(manifest.instrumented_files, file => file.output_path === manifest.entry)
|
|
434
|
-
: null;
|
|
435
|
-
return {
|
|
436
|
-
sites,
|
|
437
|
-
buildId: safeSegment(manifest.build_id, 'unknown'),
|
|
438
|
-
entrySourcePath: typeof entryFile?.source_path === 'string' ? entryFile.source_path : null,
|
|
439
|
-
};
|
|
440
|
-
} catch (error) {
|
|
441
|
-
debug('instrumentation manifest could not be read; telemetry stayed disabled', error);
|
|
442
|
-
return { sites: new SafeMap(), buildId: 'unknown', entrySourcePath: null };
|
|
443
281
|
}
|
|
282
|
+
return { sites, bytes, buildId: safeSegment(own(manifest, 'build_id'), 'unknown') };
|
|
444
283
|
}
|
|
445
284
|
|
|
446
|
-
function installRuntime(
|
|
447
|
-
if (installed) return activeControl ?? { enabled: false, reason: 'already-installed' };
|
|
285
|
+
function installRuntime() {
|
|
286
|
+
if (installed) return activeControl ?? installResult ?? { enabled: false, reason: 'already-installed' };
|
|
448
287
|
installed = true;
|
|
449
288
|
|
|
450
289
|
const endpoint = validEndpoint(process.env.HAYSTACK_TELEMETRY_ENDPOINT ?? '');
|
|
451
290
|
const token = process.env.HAYSTACK_TELEMETRY_TOKEN;
|
|
452
|
-
if (process.env.HAYSTACK_TELEMETRY !== '1' || !endpoint || !token ||
|
|
291
|
+
if (process.env.HAYSTACK_TELEMETRY !== '1' || !endpoint || !token || CONTRACT === null) {
|
|
453
292
|
return { enabled: false, reason: 'not-configured' };
|
|
454
293
|
}
|
|
455
|
-
if (CONTROL_CHANNEL in processHost) {
|
|
456
|
-
debug('telemetry control name is already owned; telemetry stayed disabled');
|
|
457
|
-
return { enabled: false, reason: 'global-conflict' };
|
|
458
|
-
}
|
|
294
|
+
if (CONTROL_CHANNEL in processHost) return { enabled: false, reason: 'global-conflict' };
|
|
459
295
|
|
|
460
|
-
// The manifest-derived build identity is part of every
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
safeMapSet(sitesById, metadata.siteId, metadata);
|
|
471
|
-
safeMapSet(probeStates, metadata.probeKey, {
|
|
472
|
-
metadata,
|
|
473
|
-
calls: 0,
|
|
474
|
-
saturated: false,
|
|
475
|
-
});
|
|
296
|
+
// The manifest-derived build identity is part of every raw flush key. Do not
|
|
297
|
+
// accept a runtime override that could merge unrelated builds.
|
|
298
|
+
let instrumentation;
|
|
299
|
+
try {
|
|
300
|
+
instrumentation = loadInstrumentation();
|
|
301
|
+
} catch {
|
|
302
|
+
instrumentation = null;
|
|
303
|
+
}
|
|
304
|
+
if (!instrumentation || instrumentation.sites.length === 0) {
|
|
305
|
+
return { enabled: false, reason: 'missing-instrumentation-manifest' };
|
|
476
306
|
}
|
|
477
|
-
|
|
307
|
+
const MAX_BYTES = CONTRACT.SERVER_RUNTIME_MAX_BYTES;
|
|
308
|
+
// Site metadata is kept for the life of the process; a build whose sites
|
|
309
|
+
// alone would take half the cap leaves too little to count with, and
|
|
310
|
+
// installs nothing.
|
|
311
|
+
if (instrumentation.bytes > MAX_BYTES / 2) {
|
|
312
|
+
return { enabled: false, reason: 'instrumentation-exceeds-memory-cap' };
|
|
313
|
+
}
|
|
314
|
+
const { buildId } = instrumentation;
|
|
478
315
|
const runId = safeSegment(
|
|
479
316
|
process.env.HAYSTACK_TELEMETRY_RUN_ID,
|
|
480
317
|
`${safeDateNow()}-${process.pid}-${randomBytes(4).toString('hex')}`,
|
|
@@ -483,987 +320,691 @@ function installRuntime(options = {}) {
|
|
|
483
320
|
const flushIntervalMs = safeNumberIsSafeInteger(intervalRequested) && intervalRequested >= 100
|
|
484
321
|
? intervalRequested
|
|
485
322
|
: DEFAULT_FLUSH_INTERVAL_MS;
|
|
486
|
-
const requestedSiteObservations = safeNumberParseInt(
|
|
487
|
-
process.env.HAYSTACK_TELEMETRY_SITE_OBSERVATIONS ?? '',
|
|
488
|
-
10,
|
|
489
|
-
);
|
|
490
|
-
const siteObservationLimit = safeNumberIsSafeInteger(requestedSiteObservations) &&
|
|
491
|
-
requestedSiteObservations >= 1 && requestedSiteObservations <= MAX_SITE_OBSERVATIONS
|
|
492
|
-
? requestedSiteObservations
|
|
493
|
-
: DEFAULT_SITE_OBSERVATIONS;
|
|
494
|
-
const requestedProcessProbeObservations = safeNumberParseInt(
|
|
495
|
-
process.env.HAYSTACK_TELEMETRY_PROCESS_PROBE_OBSERVATIONS ?? '',
|
|
496
|
-
10,
|
|
497
|
-
);
|
|
498
|
-
const processProbeObservationLimit = safeNumberIsSafeInteger(requestedProcessProbeObservations) &&
|
|
499
|
-
requestedProcessProbeObservations >= 1 &&
|
|
500
|
-
requestedProcessProbeObservations <= MAX_PROCESS_PROBE_OBSERVATIONS
|
|
501
|
-
? requestedProcessProbeObservations
|
|
502
|
-
: DEFAULT_PROCESS_PROBE_OBSERVATIONS;
|
|
503
|
-
const requestedProcessSampleRate = safeNumberParseInt(
|
|
504
|
-
process.env.HAYSTACK_TELEMETRY_PROCESS_SAMPLE_RATE ?? '',
|
|
505
|
-
10,
|
|
506
|
-
);
|
|
507
|
-
const processSampleRate = safeNumberIsSafeInteger(requestedProcessSampleRate) &&
|
|
508
|
-
requestedProcessSampleRate >= 0 && requestedProcessSampleRate <= MAX_ACTION_SAMPLE_RATE
|
|
509
|
-
? requestedProcessSampleRate
|
|
510
|
-
: DEFAULT_PROCESS_SAMPLE_RATE;
|
|
511
|
-
const requestedActionSampleRate = safeNumberParseInt(
|
|
512
|
-
process.env.HAYSTACK_TELEMETRY_ACTION_SAMPLE_RATE ?? '',
|
|
513
|
-
10,
|
|
514
|
-
);
|
|
515
|
-
const actionSampleRate = safeNumberIsSafeInteger(requestedActionSampleRate) &&
|
|
516
|
-
requestedActionSampleRate >= 1 && requestedActionSampleRate <= MAX_ACTION_SAMPLE_RATE
|
|
517
|
-
? requestedActionSampleRate
|
|
518
|
-
: DEFAULT_ACTION_SAMPLE_RATE;
|
|
519
323
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
324
|
+
const SAMPLE_AFTER_CALLS = CONTRACT.SERVER_SAMPLE_AFTER_CALLS;
|
|
325
|
+
const SAMPLE_RATE = CONTRACT.SERVER_SAMPLE_RATE;
|
|
326
|
+
// The draw stays in integer arithmetic: a power-of-two rate keeps the low
|
|
327
|
+
// bits (a mask); any other rate takes an unsigned remainder.
|
|
328
|
+
const SAMPLE_MASK = (SAMPLE_RATE & (SAMPLE_RATE - 1)) === 0 ? SAMPLE_RATE - 1 : -1;
|
|
329
|
+
const OBSERVATIONS_PER_SECOND = CONTRACT.SERVER_OBSERVATIONS_PER_SECOND;
|
|
330
|
+
const SEND_TIMEOUT_MS = CONTRACT.SERVER_SEND_TIMEOUT_MS;
|
|
331
|
+
const EXIT_FLUSH_MS = CONTRACT.SERVER_EXIT_FLUSH_MS;
|
|
332
|
+
const C = CONTRACT.categories;
|
|
333
|
+
const OVERFLOW = C.overflow;
|
|
334
|
+
const FIELD_STATE_PRESENT = 0;
|
|
335
|
+
const FIELD_STATE_MISSING = 1;
|
|
336
|
+
const FIELD_STATE_NULL = 2;
|
|
337
|
+
const FIELD_STATE_UNDEFINED = 3;
|
|
338
|
+
const FIELD_STATE_OPAQUE = 4;
|
|
339
|
+
const FIELD_STATES = CONTRACT.SERVER_FIELD_STATES;
|
|
340
|
+
const KIND_BRANCH = 0;
|
|
341
|
+
const KIND_VALUE = 1;
|
|
342
|
+
|
|
343
|
+
let counting = true;
|
|
344
|
+
let offReason = null;
|
|
523
345
|
let sequence = 0;
|
|
524
346
|
let consecutiveFailures = 0;
|
|
525
|
-
let
|
|
526
|
-
let currentBranch = new SafeMap();
|
|
527
|
-
let currentExecutions = new SafeMap();
|
|
528
|
-
let currentExecutionBytes = 0;
|
|
529
|
-
let omittedExecutionFamilyObservations = 0;
|
|
530
|
-
let sampledActions = 0;
|
|
531
|
-
let unsampledActions = 0;
|
|
532
|
-
let currentOmissions = new SafeMap();
|
|
347
|
+
let budgetUsed = 0;
|
|
533
348
|
let pending = null;
|
|
534
349
|
let pendingQueue = [];
|
|
535
350
|
let flushInFlight = null;
|
|
536
|
-
let
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
let
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
351
|
+
let spoolReplayed = false;
|
|
352
|
+
let exiting = false;
|
|
353
|
+
let exitDeadline = 0;
|
|
354
|
+
let spooled = false;
|
|
355
|
+
let currentSend = null;
|
|
356
|
+
|
|
357
|
+
// Every retained byte, by what holds it (rule 13b: the cap covers them all).
|
|
358
|
+
const memory = {
|
|
359
|
+
sites: instrumentation.bytes,
|
|
360
|
+
fieldKeys: 0,
|
|
361
|
+
counters: 0,
|
|
362
|
+
serializing: 0,
|
|
363
|
+
queued: 0,
|
|
364
|
+
};
|
|
365
|
+
const used = () => memory.sites + memory.fieldKeys + memory.counters + memory.serializing + memory.queued;
|
|
366
|
+
const COUNTER_CAP = (MAX_BYTES - memory.sites) * COUNTER_SHARE;
|
|
367
|
+
const allocatable = bytes => used() + bytes <= MAX_BYTES - RESERVE_BYTES;
|
|
368
|
+
|
|
369
|
+
const statesByProbe = new SafeMap();
|
|
370
|
+
const states = [];
|
|
371
|
+
for (let index = 0; index < instrumentation.sites.length; index++) {
|
|
372
|
+
const metadata = instrumentation.sites[index];
|
|
373
|
+
let settings = null;
|
|
374
|
+
if (metadata.settings !== null) {
|
|
375
|
+
// Map keys compare by SameValueZero: 'dark' and 1 never meet, and a
|
|
376
|
+
// lookup never coerces the customer's value.
|
|
377
|
+
settings = new SafeMap();
|
|
378
|
+
for (let position = 0; position < metadata.settings.length; position++) {
|
|
379
|
+
const literal = metadata.settings[position];
|
|
380
|
+
safeMapSet(settings, literal, `${CONTRACT.SERVER_SETTING_CATEGORY_PREFIX}${literalJson(literal)}`);
|
|
381
|
+
}
|
|
553
382
|
}
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
eventBytes: 0,
|
|
572
|
-
truncated: false,
|
|
573
|
-
omittedEvents: 0,
|
|
574
|
-
finalized: false,
|
|
575
|
-
outcome: null,
|
|
576
|
-
terminalError: 'none',
|
|
383
|
+
const state = {
|
|
384
|
+
metadata,
|
|
385
|
+
kind: metadata.probeKind === 'condition' ? KIND_BRANCH : KIND_VALUE,
|
|
386
|
+
probeName: metadata.probeKind === 'condition'
|
|
387
|
+
? '__hstBranch'
|
|
388
|
+
: metadata.probeKind === 'throw' ? '__hstThrow' : '__hstValue',
|
|
389
|
+
settings,
|
|
390
|
+
// Lifetime calls (the census is the process's first SERVER_SAMPLE_AFTER_CALLS); a window's calls are seen
|
|
391
|
+
// minus the mark its last flush left.
|
|
392
|
+
seen: 0,
|
|
393
|
+
flushedSeen: 0,
|
|
394
|
+
index,
|
|
395
|
+
skipped: 0,
|
|
396
|
+
census: null,
|
|
397
|
+
sampled: null,
|
|
398
|
+
// Bytes this site's window counters hold (part of memory.counters).
|
|
399
|
+
bytes: 0,
|
|
577
400
|
};
|
|
401
|
+
safeMapSet(statesByProbe, metadata.probeKey, state);
|
|
402
|
+
safeArrayPush(states, state);
|
|
578
403
|
}
|
|
579
404
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
405
|
+
// Per-site PRNG state in an Int32Array: a 32-bit state kept on an object
|
|
406
|
+
// would be boxed as a heap number on every draw.
|
|
407
|
+
const randomState = new Int32Array(states.length);
|
|
408
|
+
for (let index = 0; index < states.length; index++) {
|
|
409
|
+
randomState[index] = hashSeed(`${runId}:${states[index].metadata.siteId}`) | 0;
|
|
410
|
+
}
|
|
411
|
+
memory.sites += states.length * 4;
|
|
412
|
+
|
|
413
|
+
// Field keys (`.<field>:<state>`), built on first use and shared by every
|
|
414
|
+
// site declaring the same field.
|
|
415
|
+
const fieldKeysByName = new SafeMap();
|
|
416
|
+
function fieldKeys(field) {
|
|
417
|
+
let keys = safeMapGet(fieldKeysByName, field);
|
|
418
|
+
if (keys !== undefined) return keys;
|
|
419
|
+
const cost = ENTRY_BYTES + 5 * stringBytes(`.${field}:undefined`);
|
|
420
|
+
if (!allocatable(cost)) return null;
|
|
421
|
+
keys = [`.${field}:${FIELD_STATES[0]}`, `.${field}:${FIELD_STATES[1]}`, `.${field}:${FIELD_STATES[2]}`,
|
|
422
|
+
`.${field}:${FIELD_STATES[3]}`, `.${field}:${FIELD_STATES[4]}`];
|
|
423
|
+
safeMapSet(fieldKeysByName, field, keys);
|
|
424
|
+
memory.fieldKeys += cost;
|
|
425
|
+
return keys;
|
|
583
426
|
}
|
|
584
427
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
428
|
+
// The fields' states and keys for the call being classified, reused across calls.
|
|
429
|
+
const fieldScratch = new Uint8Array(CONTRACT.SERVER_FIELDS_PER_SITE);
|
|
430
|
+
const keyScratch = [];
|
|
431
|
+
|
|
432
|
+
function turnOff(reason) {
|
|
433
|
+
if (!counting) return;
|
|
434
|
+
counting = false;
|
|
435
|
+
offReason = reason;
|
|
436
|
+
safeClearInterval(timer);
|
|
437
|
+
safeClearInterval(budgetTimer);
|
|
438
|
+
if (currentSend !== null) currentSend();
|
|
439
|
+
pending = null;
|
|
440
|
+
pendingQueue = [];
|
|
441
|
+
for (let index = 0; index < states.length; index++) {
|
|
442
|
+
const state = states[index];
|
|
443
|
+
state.flushedSeen = state.seen;
|
|
444
|
+
state.skipped = 0;
|
|
445
|
+
state.census = null;
|
|
446
|
+
state.sampled = null;
|
|
447
|
+
state.bytes = 0;
|
|
448
|
+
}
|
|
449
|
+
memory.counters = 0;
|
|
450
|
+
memory.serializing = 0;
|
|
451
|
+
memory.queued = 0;
|
|
589
452
|
}
|
|
590
453
|
|
|
591
|
-
function
|
|
592
|
-
|
|
593
|
-
return action && !action.suppressed ? action : null;
|
|
454
|
+
function sizeIndex(size) {
|
|
455
|
+
return size === 0 ? 0 : size === 1 ? 1 : size <= 5 ? 2 : size <= 20 ? 3 : size <= 100 ? 4 : 5;
|
|
594
456
|
}
|
|
595
457
|
|
|
596
|
-
function
|
|
597
|
-
return
|
|
458
|
+
function numberCategory(value) {
|
|
459
|
+
if (value !== value || value === Infinity || value === -Infinity) return C.numberNonfinite;
|
|
460
|
+
if (value === 0) return C.numberZero;
|
|
461
|
+
const negative = value < 0;
|
|
462
|
+
const magnitude = negative ? -value : value;
|
|
463
|
+
const index = magnitude < 1 ? 0 : magnitude < 10 ? 1 : magnitude < 100 ? 2 : magnitude < 1000 ? 3 : 4;
|
|
464
|
+
return negative ? C.numberNegative[index] : C.numberPositive[index];
|
|
598
465
|
}
|
|
599
466
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
return;
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
siteId: metadata.siteId,
|
|
617
|
-
kind,
|
|
618
|
-
scope,
|
|
619
|
-
outcome,
|
|
620
|
-
observations: 1,
|
|
621
|
-
};
|
|
622
|
-
const eventBytes = safeBufferByteLength(safeJsonStringify({
|
|
623
|
-
...event,
|
|
624
|
-
sourcePath: metadata.sourcePath,
|
|
625
|
-
qualifiedName: metadata.qualifiedName,
|
|
626
|
-
probeKind: metadata.probeKind,
|
|
627
|
-
}), 'utf8');
|
|
628
|
-
if (action.events.length >= MAX_EXECUTION_EVENTS ||
|
|
629
|
-
action.eventBytes + eventBytes > MAX_EXECUTION_ACTION_BYTES) {
|
|
630
|
-
action.truncated = true;
|
|
631
|
-
omitExecutionEvents(action);
|
|
632
|
-
return;
|
|
467
|
+
/** Rule 9(b2): a field's state through property descriptors only, up the
|
|
468
|
+
* prototype chain to Object.prototype; an accessor or a proxy anywhere on
|
|
469
|
+
* the way is opaque, never invoked. */
|
|
470
|
+
function fieldState(object, field) {
|
|
471
|
+
let holder = object;
|
|
472
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
473
|
+
const descriptor = safeGetOwnPropertyDescriptor(holder, field);
|
|
474
|
+
if (descriptor !== undefined) {
|
|
475
|
+
if (!safeHasOwn(descriptor, 'value')) return FIELD_STATE_OPAQUE;
|
|
476
|
+
const value = descriptor.value;
|
|
477
|
+
return value === undefined ? FIELD_STATE_UNDEFINED : value === null ? FIELD_STATE_NULL : FIELD_STATE_PRESENT;
|
|
478
|
+
}
|
|
479
|
+
const prototype = safeGetPrototypeOf(holder);
|
|
480
|
+
if (prototype === null || prototype === ObjectPrototype) return FIELD_STATE_MISSING;
|
|
481
|
+
if (safeIsProxy(prototype)) return FIELD_STATE_OPAQUE;
|
|
482
|
+
holder = prototype;
|
|
633
483
|
}
|
|
634
|
-
|
|
635
|
-
action.eventBytes += eventBytes;
|
|
484
|
+
return FIELD_STATE_OPAQUE;
|
|
636
485
|
}
|
|
637
486
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (
|
|
643
|
-
|
|
644
|
-
|
|
487
|
+
/** Rule 9(b), (b2), (c): exactly one value category per call, never a value
|
|
488
|
+
* except an approved literal, and no customer code run to find it. */
|
|
489
|
+
function valueCategory(state, value) {
|
|
490
|
+
if (value === undefined) return C.undefined;
|
|
491
|
+
if (value === null) return C.null;
|
|
492
|
+
const type = typeof value;
|
|
493
|
+
if (state.settings !== null) {
|
|
494
|
+
if (type === 'string' || type === 'number' || type === 'boolean') {
|
|
495
|
+
const literal = safeMapGet(state.settings, value);
|
|
496
|
+
return literal === undefined ? C.other : literal;
|
|
645
497
|
}
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
const sketch = {
|
|
649
|
-
actionKind: action.actionKind,
|
|
650
|
-
actionSiteId: action.actionSiteId,
|
|
651
|
-
actionMethod: action.actionMethod,
|
|
652
|
-
outcome,
|
|
653
|
-
terminalError: action.terminalError,
|
|
654
|
-
truncated: action.truncated,
|
|
655
|
-
omittedEvents: action.omittedEvents,
|
|
656
|
-
events: action.events,
|
|
657
|
-
};
|
|
658
|
-
const familyId = stableIdentity('execution-local-family', [canonicalJson(sketch)]);
|
|
659
|
-
const existing = safeMapGet(currentExecutions, familyId);
|
|
660
|
-
if (existing) {
|
|
661
|
-
existing.observations++;
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
const sketchBytes = action.eventBytes + safeBufferByteLength(safeJsonStringify(sketch), 'utf8');
|
|
665
|
-
if (safeMapSize(currentExecutions) >= MAX_EXECUTION_FAMILIES ||
|
|
666
|
-
currentExecutionBytes + sketchBytes > MAX_EXECUTION_BYTES) {
|
|
667
|
-
omittedExecutionFamilyObservations++;
|
|
668
|
-
return;
|
|
498
|
+
return (type === 'object' || type === 'function') && safeIsProxy(value) ? C.opaque : C.other;
|
|
669
499
|
}
|
|
670
|
-
|
|
671
|
-
|
|
500
|
+
if (type === 'string') return C.string[sizeIndex(value.length)];
|
|
501
|
+
if (type === 'number') return numberCategory(value);
|
|
502
|
+
if (type === 'boolean') return C.boolean;
|
|
503
|
+
if (type === 'bigint') return C.bigint;
|
|
504
|
+
if (type === 'symbol') return C.symbol;
|
|
505
|
+
if (safeIsProxy(value)) return C.opaque;
|
|
506
|
+
if (type === 'function') return C.function;
|
|
507
|
+
// A genuine array's length is an own data property; Map and Set sizes come
|
|
508
|
+
// from the built-in getters captured above, never the value's own.
|
|
509
|
+
if (safeArrayIsArray(value)) return C.array[sizeIndex(value.length)];
|
|
510
|
+
if (safeIsMap(value)) return C.map[sizeIndex(safeMapSize(value))];
|
|
511
|
+
if (safeIsSet(value)) return C.set[sizeIndex(safeSetSize(value))];
|
|
512
|
+
if (safeIsNativeError(value)) return C.error;
|
|
513
|
+
return C.object;
|
|
672
514
|
}
|
|
673
515
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
'NONE',
|
|
678
|
-
);
|
|
679
|
-
safeActionEnterWith(actionStorage, processAction);
|
|
516
|
+
function entryCost(key) {
|
|
517
|
+
return ENTRY_BYTES + key.length * 2;
|
|
518
|
+
}
|
|
680
519
|
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
events: [],
|
|
691
|
-
eventBytes: 0,
|
|
692
|
-
truncated: false,
|
|
693
|
-
omittedEvents: 0,
|
|
694
|
-
finalized: false,
|
|
695
|
-
outcome: null,
|
|
696
|
-
terminalError: 'none',
|
|
697
|
-
};
|
|
698
|
-
if (!hasChildActions || !active || active === previous) {
|
|
699
|
-
safeActionEnterWith(actionStorage, processAction);
|
|
520
|
+
function bump(state, counters, key) {
|
|
521
|
+
const existing = safeMapGet(counters, key);
|
|
522
|
+
if (existing === undefined) {
|
|
523
|
+
const cost = entryCost(key);
|
|
524
|
+
state.bytes += cost;
|
|
525
|
+
memory.counters += cost;
|
|
526
|
+
safeMapSet(counters, key, 1);
|
|
527
|
+
} else {
|
|
528
|
+
safeMapSet(counters, key, existing + 1);
|
|
700
529
|
}
|
|
701
530
|
}
|
|
702
531
|
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
532
|
+
function classify(state, value, census) {
|
|
533
|
+
let counters = census ? state.census : state.sampled;
|
|
534
|
+
if (counters === null) {
|
|
535
|
+
if (!allocatable(MAP_BYTES + ENTRY_BYTES)) {
|
|
536
|
+
// Not even an overflow counter fits: the call is unclassified.
|
|
537
|
+
state.skipped++;
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
counters = new SafeMap();
|
|
541
|
+
state.bytes += MAP_BYTES;
|
|
542
|
+
memory.counters += MAP_BYTES;
|
|
543
|
+
if (census) state.census = counters;
|
|
544
|
+
else state.sampled = counters;
|
|
545
|
+
}
|
|
546
|
+
let category;
|
|
547
|
+
let fieldCount = 0;
|
|
548
|
+
try {
|
|
549
|
+
if (state.kind === KIND_BRANCH) {
|
|
550
|
+
// ToBoolean never runs customer code.
|
|
551
|
+
category = value ? C.true : C.false;
|
|
552
|
+
} else {
|
|
553
|
+
category = valueCategory(state, value);
|
|
554
|
+
const fields = state.metadata.fields;
|
|
555
|
+
if (category === C.object && fields.length > 0) {
|
|
556
|
+
fieldCount = fields.length;
|
|
557
|
+
for (let index = 0; index < fieldCount; index++) {
|
|
558
|
+
fieldScratch[index] = fieldState(value, fields[index]);
|
|
559
|
+
keyScratch[index] = fieldKeys(fields[index]);
|
|
560
|
+
}
|
|
717
561
|
}
|
|
718
|
-
} catch (error) {
|
|
719
|
-
debug('HTTP action start failed open', error);
|
|
720
562
|
}
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
: status >= 500
|
|
737
|
-
? 'server-error'
|
|
738
|
-
: status >= 400
|
|
739
|
-
? 'client-error'
|
|
740
|
-
: 'success';
|
|
741
|
-
finalizeAction(action, outcome);
|
|
742
|
-
} catch (error) {
|
|
743
|
-
debug('HTTP action finish failed open', error);
|
|
563
|
+
} catch {
|
|
564
|
+
// An exotic object refused inspection; it is opaque, and nothing escapes.
|
|
565
|
+
category = state.kind === KIND_BRANCH ? (value ? C.true : C.false) : C.opaque;
|
|
566
|
+
fieldCount = 0;
|
|
567
|
+
}
|
|
568
|
+
// Past the counters' share of the cap (or with a field key that could not
|
|
569
|
+
// be built), a call that needs a counter this window has not allocated is
|
|
570
|
+
// counted as overflow alone, its fields unrecorded, so every classified
|
|
571
|
+
// call still adds exactly one value category.
|
|
572
|
+
let complete = true;
|
|
573
|
+
for (let index = 0; complete && index < fieldCount; index++) complete = keyScratch[index] !== null;
|
|
574
|
+
if (complete && memory.counters >= COUNTER_CAP) {
|
|
575
|
+
complete = safeMapGet(counters, category) !== undefined;
|
|
576
|
+
for (let index = 0; complete && index < fieldCount; index++) {
|
|
577
|
+
complete = safeMapGet(counters, keyScratch[index][fieldScratch[index]]) !== undefined;
|
|
744
578
|
}
|
|
745
|
-
});
|
|
746
|
-
} catch (error) {
|
|
747
|
-
debug('HTTP action correlation unavailable', error);
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
function recordOmission(site, field) {
|
|
751
|
-
const current = safeMapGet(currentOmissions, site) || {
|
|
752
|
-
counterObservations: 0,
|
|
753
|
-
probeObservations: 0,
|
|
754
|
-
};
|
|
755
|
-
current[field]++;
|
|
756
|
-
safeMapSet(currentOmissions, site, current);
|
|
757
|
-
}
|
|
758
|
-
|
|
759
|
-
function disable(reason, error) {
|
|
760
|
-
enabled = false;
|
|
761
|
-
safeClearInterval(timer);
|
|
762
|
-
debug(`disabled after ${reason}`, error);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function sampleWeight(state) {
|
|
766
|
-
if (processProbeObservations >= processProbeObservationLimit) {
|
|
767
|
-
// Attribute the one observation that exhausted the process-wide budget.
|
|
768
|
-
// Later probes are deliberately not inspected or counted: the sampler
|
|
769
|
-
// version declares that this is a bounded, lossy collection mode.
|
|
770
|
-
recordOmission(state.metadata.probeKey, 'probeObservations');
|
|
771
|
-
collecting = false;
|
|
772
|
-
return -2;
|
|
773
579
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
state
|
|
778
|
-
|
|
580
|
+
if (!complete) {
|
|
581
|
+
const existing = safeMapGet(counters, OVERFLOW);
|
|
582
|
+
if (existing !== undefined) safeMapSet(counters, OVERFLOW, existing + 1);
|
|
583
|
+
else if (allocatable(entryCost(OVERFLOW))) bump(state, counters, OVERFLOW);
|
|
584
|
+
else state.skipped++;
|
|
585
|
+
return;
|
|
779
586
|
}
|
|
780
|
-
|
|
587
|
+
bump(state, counters, category);
|
|
588
|
+
for (let index = 0; index < fieldCount; index++) bump(state, counters, keyScratch[index][fieldScratch[index]]);
|
|
589
|
+
if (used() > MAX_BYTES) turnOff('memory cap');
|
|
781
590
|
}
|
|
782
591
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
592
|
+
/** The rare path of a call: past the draw (or in the census), within the
|
|
593
|
+
* second's budget, classified; never throws into the probe. */
|
|
594
|
+
function sample(state, value, census) {
|
|
595
|
+
if (budgetUsed >= OBSERVATIONS_PER_SECOND) {
|
|
596
|
+
state.skipped++;
|
|
787
597
|
return;
|
|
788
598
|
}
|
|
789
|
-
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
function observeState(state, value, fields) {
|
|
793
|
-
// Saturation is deliberately lossy: the first over-limit observation
|
|
794
|
-
// records an explicit saturation marker, then this probe becomes a
|
|
795
|
-
// constant-time pass-through. Counting every later omission caused severe
|
|
796
|
-
// amplification in customer hot loops while adding no new shape evidence.
|
|
797
|
-
if (!enabled || !collecting || state.saturated) return value;
|
|
599
|
+
budgetUsed++;
|
|
798
600
|
try {
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
const weight = sampleWeight(state);
|
|
805
|
-
if (weight === -2) {
|
|
806
|
-
if (executionValue) {
|
|
807
|
-
const skippedEvents = value !== null && typeof value === 'object' && !safeArrayIsArray(value)
|
|
808
|
-
? 1 + safeMathMin(fields.length, 48)
|
|
809
|
-
: 1;
|
|
810
|
-
omitExecutionEvents(action, skippedEvents);
|
|
811
|
-
}
|
|
812
|
-
return value;
|
|
813
|
-
}
|
|
814
|
-
if (weight < 0) {
|
|
815
|
-
if (weight === -1) increment(currentShape, `${site}|__saturated__|info`, 1);
|
|
816
|
-
if (executionValue) {
|
|
817
|
-
const skippedEvents = value !== null && typeof value === 'object' && !safeArrayIsArray(value)
|
|
818
|
-
? 1 + safeMathMin(fields.length, 48)
|
|
819
|
-
: 1;
|
|
820
|
-
omitExecutionEvents(action, skippedEvents);
|
|
821
|
-
}
|
|
822
|
-
return value;
|
|
823
|
-
}
|
|
824
|
-
const fieldEventCount = safeArrayIsArray(fields) && value !== null &&
|
|
825
|
-
typeof value === 'object' && !safeArrayIsArray(value)
|
|
826
|
-
? safeMathMin(fields.length, 48)
|
|
827
|
-
: 0;
|
|
828
|
-
const correlated = executionValue && !action.truncated;
|
|
829
|
-
if (executionValue && action.truncated) omitExecutionEvents(action, 1 + fieldEventCount);
|
|
830
|
-
if (weight > 0) {
|
|
831
|
-
const selfOutcome = value === null ? 'null' : value === undefined ? 'undefined' : 'ok';
|
|
832
|
-
increment(currentShape, `${site}|__self__|${selfOutcome}`, weight);
|
|
833
|
-
increment(currentShape, `${site}|__self__|shape:${valueShape(value)}`, weight);
|
|
834
|
-
}
|
|
835
|
-
if (correlated && probeKind === 'parameter') {
|
|
836
|
-
addExecutionEvent(site, 'input', 'whole-value', `shape:${valueShape(value)}`);
|
|
837
|
-
} else if (correlated && probeKind === 'return') {
|
|
838
|
-
addExecutionEvent(site, 'output', 'whole-value', `shape:${valueShape(value)}`);
|
|
839
|
-
}
|
|
601
|
+
classify(state, value, census);
|
|
602
|
+
} catch {
|
|
603
|
+
// Nothing a probe does may reach the application.
|
|
604
|
+
}
|
|
605
|
+
}
|
|
840
606
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
try {
|
|
859
|
-
descriptor = safeGetOwnPropertyDescriptor(value, field);
|
|
860
|
-
} catch {
|
|
861
|
-
if (weight > 0) increment(currentShape, `${site}|${field}|uninspectable`, weight);
|
|
862
|
-
if (correlated) {
|
|
863
|
-
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'uninspectable');
|
|
864
|
-
}
|
|
865
|
-
continue;
|
|
866
|
-
}
|
|
867
|
-
if (!descriptor) {
|
|
868
|
-
if (weight > 0) increment(currentShape, `${site}|${field}|absent`, weight);
|
|
869
|
-
if (correlated) {
|
|
870
|
-
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'absent');
|
|
871
|
-
}
|
|
872
|
-
} else if (!safeHasOwn(descriptor, 'value')) {
|
|
873
|
-
if (weight > 0) increment(currentShape, `${site}|${field}|accessor`, weight);
|
|
874
|
-
if (correlated) {
|
|
875
|
-
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'accessor');
|
|
876
|
-
}
|
|
877
|
-
} else {
|
|
878
|
-
const fieldValue = descriptor.value;
|
|
879
|
-
const outcome = fieldValue === null ? 'null' : fieldValue === undefined ? 'undefined' : 'ok';
|
|
880
|
-
if (weight > 0) {
|
|
881
|
-
increment(currentShape, `${site}|${field}|${outcome}`, weight);
|
|
882
|
-
increment(currentShape, `${site}|${field}|shape:${valueShape(fieldValue)}`, weight);
|
|
883
|
-
}
|
|
884
|
-
if (correlated) {
|
|
885
|
-
addExecutionEvent(
|
|
886
|
-
site,
|
|
887
|
-
probeKind === 'parameter' ? 'input' : 'output',
|
|
888
|
-
fieldScope,
|
|
889
|
-
`shape:${valueShape(fieldValue)}`,
|
|
890
|
-
);
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
607
|
+
/** One site's probe: rule 9(a)'s single increment, then the census or a
|
|
608
|
+
* Bernoulli draw (xorshift32, decorrelated from any predicate's period,
|
|
609
|
+
* where a fixed stride would alias with it). */
|
|
610
|
+
function siteProbe(state) {
|
|
611
|
+
const index = state.index;
|
|
612
|
+
return (value) => {
|
|
613
|
+
if (!counting) return value;
|
|
614
|
+
if (++state.seen > SAMPLE_AFTER_CALLS) {
|
|
615
|
+
let random = randomState[index];
|
|
616
|
+
random ^= random << 13;
|
|
617
|
+
random ^= random >>> 17;
|
|
618
|
+
random ^= random << 5;
|
|
619
|
+
randomState[index] = random;
|
|
620
|
+
if (SAMPLE_MASK >= 0 ? (random & SAMPLE_MASK) !== 0 : (random >>> 0) % SAMPLE_RATE !== 0) return value;
|
|
621
|
+
sample(state, value, false);
|
|
622
|
+
} else {
|
|
623
|
+
sample(state, value, true);
|
|
894
624
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
debug('value probe failed open', error);
|
|
898
|
-
}
|
|
899
|
-
return value;
|
|
625
|
+
return value;
|
|
626
|
+
};
|
|
900
627
|
}
|
|
901
628
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
629
|
+
/** Sites that counted a call since the last flush (a scan off the hot path). */
|
|
630
|
+
function activeStates() {
|
|
631
|
+
const active = [];
|
|
632
|
+
for (let index = 0; index < states.length; index++) {
|
|
633
|
+
const state = states[index];
|
|
634
|
+
if (state.seen !== state.flushedSeen || state.skipped > 0) safeArrayPush(active, state);
|
|
635
|
+
}
|
|
636
|
+
return active;
|
|
905
637
|
}
|
|
906
638
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
} catch (error) {
|
|
929
|
-
debug('branch probe failed open', error);
|
|
639
|
+
/** The window so far, taken atomically: each active site's deltas and its
|
|
640
|
+
* counters handed over and reset (pointer swaps). The handed-over counters
|
|
641
|
+
* stay accounted, as serializing, until each site's JSON replaces them. */
|
|
642
|
+
function takeWindow() {
|
|
643
|
+
const active = activeStates();
|
|
644
|
+
const taken = [];
|
|
645
|
+
for (let index = 0; index < active.length; index++) {
|
|
646
|
+
const state = active[index];
|
|
647
|
+
const entry = safeObjectCreate(null);
|
|
648
|
+
entry.state = state;
|
|
649
|
+
entry.calls = state.seen - state.flushedSeen;
|
|
650
|
+
entry.skipped = state.skipped;
|
|
651
|
+
entry.census = state.census;
|
|
652
|
+
entry.sampled = state.sampled;
|
|
653
|
+
entry.bytes = state.bytes;
|
|
654
|
+
safeArrayPush(taken, entry);
|
|
655
|
+
state.flushedSeen = state.seen;
|
|
656
|
+
state.skipped = 0;
|
|
657
|
+
state.census = null;
|
|
658
|
+
state.sampled = null;
|
|
659
|
+
state.bytes = 0;
|
|
930
660
|
}
|
|
931
|
-
|
|
661
|
+
memory.serializing += memory.counters;
|
|
662
|
+
memory.counters = 0;
|
|
663
|
+
return taken;
|
|
932
664
|
}
|
|
933
665
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
666
|
+
/** `{"<category>":<count>,...}` from a telemetry-owned Map of strings to integers. */
|
|
667
|
+
function countsJson(counters) {
|
|
668
|
+
if (counters === null) return '{}';
|
|
669
|
+
const parts = [];
|
|
670
|
+
const iterator = safeMapEntries(counters);
|
|
671
|
+
while (true) {
|
|
672
|
+
const step = safeMapIteratorNext(iterator);
|
|
673
|
+
if (step.done) break;
|
|
674
|
+
safeArrayPush(parts, `${quote(step.value[0])}:${step.value[1]}`);
|
|
675
|
+
}
|
|
676
|
+
return `{${safeArrayJoin(parts, ',')}}`;
|
|
937
677
|
}
|
|
938
678
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
} catch (error) {
|
|
956
|
-
debug('throw probe failed open', error);
|
|
679
|
+
/** ServerSiteV5 from the site's null-prototype record of primitives. */
|
|
680
|
+
function siteJson(metadata) {
|
|
681
|
+
const fields = [];
|
|
682
|
+
for (let index = 0; index < metadata.fields.length; index++) safeArrayPush(fields, quote(metadata.fields[index]));
|
|
683
|
+
let json = `{"probeKey":${quote(metadata.probeKey)},"siteId":${quote(metadata.siteId)},` +
|
|
684
|
+
`"sourcePath":${quote(metadata.sourcePath)},"qualifiedName":${quote(metadata.qualifiedName)},` +
|
|
685
|
+
`"probeKind":${quote(metadata.probeKind)},"ownerKey":${quote(metadata.ownerKey)},` +
|
|
686
|
+
`"structuralKey":${quote(metadata.structuralKey)}`;
|
|
687
|
+
if (metadata.label !== null) json += `,"label":${quote(metadata.label)}`;
|
|
688
|
+
json += `,"allowedFields":[${safeArrayJoin(fields, ',')}]`;
|
|
689
|
+
if (metadata.line !== null) json += `,"line":${metadata.line}`;
|
|
690
|
+
if (metadata.column !== null) json += `,"column":${metadata.column}`;
|
|
691
|
+
if (metadata.settings !== null) {
|
|
692
|
+
const literals = [];
|
|
693
|
+
for (let index = 0; index < metadata.settings.length; index++) safeArrayPush(literals, literalJson(metadata.settings[index]));
|
|
694
|
+
json += `,"settings":[${safeArrayJoin(literals, ',')}]`;
|
|
957
695
|
}
|
|
958
|
-
return
|
|
696
|
+
return `${json}}`;
|
|
959
697
|
}
|
|
960
698
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
699
|
+
/** One site's part of a body, its bytes moved from serializing to queued. */
|
|
700
|
+
function unitOf(entry) {
|
|
701
|
+
const site = siteJson(entry.state.metadata);
|
|
702
|
+
const counts = `{"site":${quote(entry.state.metadata.siteId)},"calls":${entry.calls},` +
|
|
703
|
+
`"census":${countsJson(entry.census)},"sampled":${countsJson(entry.sampled)},` +
|
|
704
|
+
`"sampleRate":${SAMPLE_RATE},"skipped":${entry.skipped}}`;
|
|
705
|
+
const bytes = safeBufferByteLength(site, 'utf8') + safeBufferByteLength(counts, 'utf8') + 2;
|
|
706
|
+
memory.serializing -= entry.bytes;
|
|
707
|
+
entry.census = null;
|
|
708
|
+
entry.sampled = null;
|
|
709
|
+
memory.queued += bytes * 2;
|
|
710
|
+
if (used() > MAX_BYTES) throw new Error('memory cap');
|
|
711
|
+
return { site, counts, bytes };
|
|
964
712
|
}
|
|
965
713
|
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
mapValuesArray(currentExecutions),
|
|
977
|
-
(left, right) => compareStrings(left.familyId, right.familyId),
|
|
978
|
-
);
|
|
979
|
-
currentExecutions = new SafeMap();
|
|
980
|
-
currentExecutionBytes = 0;
|
|
981
|
-
const executionOmissions = {
|
|
982
|
-
sampledActions,
|
|
983
|
-
unsampledActions,
|
|
984
|
-
omittedFamilyObservations: omittedExecutionFamilyObservations,
|
|
985
|
-
};
|
|
986
|
-
sampledActions = 0;
|
|
987
|
-
unsampledActions = 0;
|
|
988
|
-
omittedExecutionFamilyObservations = 0;
|
|
989
|
-
const omittedEntries = safeArraySort(
|
|
990
|
-
mapEntriesArray(currentOmissions),
|
|
991
|
-
(leftEntry, rightEntry) => compareStrings(leftEntry[0], rightEntry[0]),
|
|
992
|
-
);
|
|
993
|
-
const omittedSites = safeArrayMap(omittedEntries, entry => ({
|
|
994
|
-
probeKey: entry[0],
|
|
995
|
-
counterObservations: entry[1].counterObservations,
|
|
996
|
-
probeObservations: entry[1].probeObservations,
|
|
997
|
-
}));
|
|
998
|
-
currentOmissions = new SafeMap();
|
|
999
|
-
const counterGroups = new SafeMap();
|
|
1000
|
-
const groupFor = (probeKey) => {
|
|
1001
|
-
let group = safeMapGet(counterGroups, probeKey);
|
|
1002
|
-
if (!group) {
|
|
1003
|
-
group = { probeKey, shape: [], branch: [], omission: null };
|
|
1004
|
-
safeMapSet(counterGroups, probeKey, group);
|
|
1005
|
-
}
|
|
1006
|
-
return group;
|
|
1007
|
-
};
|
|
1008
|
-
const shapeEntries = safeObjectEntries(shape);
|
|
1009
|
-
for (let index = 0; index < shapeEntries.length; index++) {
|
|
1010
|
-
const entry = shapeEntries[index];
|
|
1011
|
-
safeArrayPush(groupFor(safeStringSlice(entry[0], 0, safeStringIndexOf(entry[0], '|'))).shape, entry);
|
|
1012
|
-
}
|
|
1013
|
-
const branchEntries = safeObjectEntries(branchCounters);
|
|
1014
|
-
for (let index = 0; index < branchEntries.length; index++) {
|
|
1015
|
-
const entry = branchEntries[index];
|
|
1016
|
-
safeArrayPush(groupFor(safeStringSlice(entry[0], 0, safeStringIndexOf(entry[0], '|'))).branch, entry);
|
|
1017
|
-
}
|
|
1018
|
-
for (let index = 0; index < omittedSites.length; index++) {
|
|
1019
|
-
const omission = omittedSites[index];
|
|
1020
|
-
groupFor(omission.probeKey).omission = omission;
|
|
714
|
+
/** A ServerFlushV5 body, its sequence reserved now: every body built gets a
|
|
715
|
+
* sequence of its own, whether it is sent, retried, abandoned or spooled. */
|
|
716
|
+
function bodyOf(units, observedAt, reason) {
|
|
717
|
+
const sites = [];
|
|
718
|
+
const counts = [];
|
|
719
|
+
let bytes = 0;
|
|
720
|
+
for (let index = 0; index < units.length; index++) {
|
|
721
|
+
safeArrayPush(sites, units[index].site);
|
|
722
|
+
safeArrayPush(counts, units[index].counts);
|
|
723
|
+
bytes += units[index].bytes;
|
|
1021
724
|
}
|
|
725
|
+
const seq = sequence++;
|
|
726
|
+
const body = `{"v":1,"samplerVersion":${CONTRACT.SERVER_SAMPLER_VERSION},"buildId":${quote(buildId)},` +
|
|
727
|
+
`"runId":${quote(runId)},"seq":${seq},"ts":${quote(observedAt)},` +
|
|
728
|
+
`"reason":${quote(safeStringSlice(safeString(reason), 0, 64))},` +
|
|
729
|
+
`"sites":[${safeArrayJoin(sites, ',')}],"counts":[${safeArrayJoin(counts, ',')}]}`;
|
|
730
|
+
// The units' strings are released once joined; the body's are now held.
|
|
731
|
+
const held = safeBufferByteLength(body, 'utf8');
|
|
732
|
+
memory.queued += held - bytes * 2;
|
|
733
|
+
return { seq, body, bytes: held };
|
|
734
|
+
}
|
|
1022
735
|
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
let bytes = 0;
|
|
1027
|
-
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
1028
|
-
const unit = units[unitIndex];
|
|
1029
|
-
let unitBytes = safeBufferByteLength(safeJsonStringify(unit), 'utf8') + 2_048;
|
|
1030
|
-
if (unit.type === 'counter') {
|
|
1031
|
-
unitBytes += safeBufferByteLength(safeJsonStringify(safeMapGet(sitesByProbe, unit.group.probeKey)), 'utf8');
|
|
1032
|
-
} else if (unit.type === 'execution') {
|
|
1033
|
-
unitBytes += safeArrayReduce(unit.unit.eventSites, (total, probeKey) =>
|
|
1034
|
-
total + safeBufferByteLength(safeJsonStringify(safeMapGet(sitesByProbe, probeKey)), 'utf8'), 0);
|
|
1035
|
-
}
|
|
1036
|
-
if (current.length > 0 && bytes + unitBytes > PAYLOAD_CHUNK_BUDGET) {
|
|
1037
|
-
safeArrayPush(chunks, current);
|
|
1038
|
-
current = [];
|
|
1039
|
-
bytes = 0;
|
|
1040
|
-
}
|
|
1041
|
-
safeArrayPush(current, unit);
|
|
1042
|
-
bytes += unitBytes;
|
|
1043
|
-
}
|
|
1044
|
-
if (current.length > 0) safeArrayPush(chunks, current);
|
|
1045
|
-
return chunks;
|
|
1046
|
-
};
|
|
736
|
+
function release(entry) {
|
|
737
|
+
if (entry !== null) memory.queued -= entry.bytes;
|
|
738
|
+
}
|
|
1047
739
|
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
const eventSites = safeArraySort(setValuesArray(eventSiteSet), compareStrings);
|
|
1056
|
-
return { execution, eventSites };
|
|
1057
|
-
});
|
|
1058
|
-
const counterUnits = safeArrayMap(
|
|
1059
|
-
safeArraySort(mapValuesArray(counterGroups),
|
|
1060
|
-
(left, right) => compareStrings(left.probeKey, right.probeKey)),
|
|
1061
|
-
group => ({ type: 'counter', group }),
|
|
1062
|
-
);
|
|
1063
|
-
const projectedExecutionUnits = safeArrayMap(executionUnits, unit => ({ type: 'execution', unit }));
|
|
1064
|
-
const unitsToPack = [];
|
|
1065
|
-
for (let index = 0; index < counterUnits.length; index++) safeArrayPush(unitsToPack, counterUnits[index]);
|
|
1066
|
-
for (let index = 0; index < projectedExecutionUnits.length; index++) {
|
|
1067
|
-
safeArrayPush(unitsToPack, projectedExecutionUnits[index]);
|
|
1068
|
-
}
|
|
1069
|
-
const packed = packUnits(unitsToPack);
|
|
1070
|
-
const chunkInputs = safeArrayMap(packed, units => ({
|
|
1071
|
-
groups: safeArrayMap(safeArrayFilter(units, unit => unit.type === 'counter'), unit => unit.group),
|
|
1072
|
-
executionUnits: safeArrayMap(safeArrayFilter(units, unit => unit.type === 'execution'), unit => unit.unit),
|
|
1073
|
-
}));
|
|
1074
|
-
if (chunkInputs.length === 0) safeArrayPush(chunkInputs, { groups: [], executionUnits: [] });
|
|
740
|
+
/** Bodies of the window so far, chunked under the upload limit. Interval
|
|
741
|
+
* flushes yield to the event loop between slices; the exit flush stops at
|
|
742
|
+
* its deadline (what it has not serialized is abandoned). */
|
|
743
|
+
async function makePending(reason) {
|
|
744
|
+
if (pending !== null || pendingQueue.length > 0) return;
|
|
745
|
+
const taken = takeWindow();
|
|
746
|
+
if (taken.length === 0) return;
|
|
1075
747
|
const observedAt = safeDateToISOString(new SafeDate());
|
|
1076
|
-
const
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
const usedProbeKeys = new SafeSet();
|
|
1089
|
-
for (let groupIndex = 0; groupIndex < chunk.groups.length; groupIndex++) {
|
|
1090
|
-
safeSetAdd(usedProbeKeys, chunk.groups[groupIndex].probeKey);
|
|
1091
|
-
}
|
|
1092
|
-
for (let unitIndex = 0; unitIndex < chunk.executionUnits.length; unitIndex++) {
|
|
1093
|
-
const unit = chunk.executionUnits[unitIndex];
|
|
1094
|
-
for (let siteIndex = 0; siteIndex < unit.eventSites.length; siteIndex++) {
|
|
1095
|
-
safeSetAdd(usedProbeKeys, unit.eventSites[siteIndex]);
|
|
748
|
+
const bodies = [];
|
|
749
|
+
let units = [];
|
|
750
|
+
let unitsBytes = 0;
|
|
751
|
+
try {
|
|
752
|
+
for (let index = 0; index < taken.length; index++) {
|
|
753
|
+
if (exiting && safeDateNow() >= exitDeadline) break;
|
|
754
|
+
const unit = unitOf(taken[index]);
|
|
755
|
+
taken[index] = null;
|
|
756
|
+
if (units.length > 0 && unitsBytes + unit.bytes > PAYLOAD_CHUNK_BUDGET) {
|
|
757
|
+
safeArrayPush(bodies, bodyOf(units, observedAt, reason));
|
|
758
|
+
units = [];
|
|
759
|
+
unitsBytes = 0;
|
|
1096
760
|
}
|
|
761
|
+
safeArrayPush(units, unit);
|
|
762
|
+
unitsBytes += unit.bytes;
|
|
763
|
+
if (index % FLUSH_SLICE_SITES === FLUSH_SLICE_SITES - 1) await nextTurn();
|
|
1097
764
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
765
|
+
if (units.length > 0) safeArrayPush(bodies, bodyOf(units, observedAt, reason));
|
|
766
|
+
} finally {
|
|
767
|
+
// Whatever was taken and not serialized is released (abandoned at the deadline).
|
|
768
|
+
for (let index = 0; index < taken.length; index++) if (taken[index] !== null) memory.serializing -= taken[index].bytes;
|
|
769
|
+
}
|
|
770
|
+
pending = safeArrayShift(bodies) ?? null;
|
|
771
|
+
pendingQueue = bodies;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
const target = new URL(endpoint);
|
|
775
|
+
const secure = target.protocol === 'https:';
|
|
776
|
+
const host = safeStringStartsWith(target.hostname, '[') ? safeStringSlice(target.hostname, 1, -1) : target.hostname;
|
|
777
|
+
const port = target.port ? safeNumberParseInt(target.port, 10) : secure ? 443 : 80;
|
|
778
|
+
const requestHead = `POST ${target.pathname}${target.search} HTTP/1.1\r\nHost: ${target.host}\r\n` +
|
|
779
|
+
`Authorization: Bearer ${token}\r\nContent-Type: application/json\r\nConnection: close\r\n`;
|
|
780
|
+
|
|
781
|
+
/** POST one body; resolves the response status (0: no response). Only the
|
|
782
|
+
* status line is read, then the socket is destroyed. */
|
|
783
|
+
function send(body, timeoutMs) {
|
|
784
|
+
return new SafePromise((resolve) => {
|
|
785
|
+
let settled = false;
|
|
786
|
+
let socket = null;
|
|
787
|
+
let timeout = null;
|
|
788
|
+
let head = '';
|
|
789
|
+
const finish = (status) => {
|
|
790
|
+
if (settled) return;
|
|
791
|
+
settled = true;
|
|
792
|
+
if (timeout !== null) safeClearTimeout(timeout);
|
|
793
|
+
if (currentSend === cancel) currentSend = null;
|
|
794
|
+
try {
|
|
795
|
+
if (socket !== null) socket.destroy();
|
|
796
|
+
} catch {
|
|
797
|
+
// Already gone.
|
|
798
|
+
}
|
|
799
|
+
resolve(status);
|
|
1119
800
|
};
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1122
|
-
|
|
801
|
+
const cancel = () => finish(0);
|
|
802
|
+
try {
|
|
803
|
+
// Null-prototype options: net and tls read only their own members.
|
|
804
|
+
const options = safeObjectCreate(null);
|
|
805
|
+
options.host = host;
|
|
806
|
+
options.port = port;
|
|
807
|
+
if (secure && safeIsIP(host) === 0) options.servername = host;
|
|
808
|
+
socket = secure ? safeTlsConnect(options) : safeNetConnect(options);
|
|
809
|
+
socket.unref();
|
|
810
|
+
socket.on('error', () => finish(0));
|
|
811
|
+
socket.on('close', () => finish(0));
|
|
812
|
+
socket.on('data', (chunk) => {
|
|
813
|
+
head += safeBufferToString(chunk, 'latin1', 0, safeMathMin(chunk.length, 1024));
|
|
814
|
+
const lineEnd = safeStringIndexOf(head, '\r\n');
|
|
815
|
+
if (lineEnd < 0) {
|
|
816
|
+
if (head.length > 1024) finish(0);
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
// `HTTP/1.1 202 Accepted`
|
|
820
|
+
const status = safeStringStartsWith(head, 'HTTP/1.') ? safeNumberParseInt(safeStringSlice(head, 9, 12), 10) : 0;
|
|
821
|
+
finish(status === status ? status : 0);
|
|
822
|
+
});
|
|
823
|
+
socket.once(secure ? 'secureConnect' : 'connect', () => {
|
|
824
|
+
socket.write(`${requestHead}Content-Length: ${safeBufferByteLength(body, 'utf8')}\r\n\r\n`);
|
|
825
|
+
socket.write(body);
|
|
826
|
+
});
|
|
827
|
+
timeout = safeSetTimeout(cancel, timeoutMs);
|
|
828
|
+
timeout.unref();
|
|
829
|
+
currentSend = cancel;
|
|
830
|
+
} catch {
|
|
831
|
+
finish(0);
|
|
1123
832
|
}
|
|
1124
|
-
return { payload, body };
|
|
1125
833
|
});
|
|
1126
|
-
|
|
1127
|
-
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function remaining() {
|
|
837
|
+
return exiting ? exitDeadline - safeDateNow() : SEND_TIMEOUT_MS;
|
|
1128
838
|
}
|
|
1129
839
|
|
|
1130
840
|
async function doFlush(reason) {
|
|
1131
|
-
if (!
|
|
841
|
+
if (!counting) return false;
|
|
842
|
+
// A replay sends what an earlier process of this build left; never on the
|
|
843
|
+
// way out of this one.
|
|
844
|
+
if (!spoolReplayed && !exiting) {
|
|
845
|
+
spoolReplayed = true;
|
|
846
|
+
await replaySpooled();
|
|
847
|
+
if (!counting) return false;
|
|
848
|
+
}
|
|
1132
849
|
try {
|
|
1133
|
-
makePending(reason);
|
|
1134
|
-
} catch
|
|
1135
|
-
|
|
850
|
+
await makePending(reason);
|
|
851
|
+
} catch {
|
|
852
|
+
turnOff('memory cap');
|
|
1136
853
|
return false;
|
|
1137
854
|
}
|
|
1138
|
-
if (
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
body: pending.body,
|
|
1149
|
-
signal: safeAbortTimeout(10_000),
|
|
1150
|
-
});
|
|
1151
|
-
if (!responseAccepted(response)) throw new SafeError(`ingestion returned HTTP ${responseStatus(response)}`);
|
|
1152
|
-
pending = null;
|
|
1153
|
-
sequence++;
|
|
1154
|
-
uploaded = true;
|
|
1155
|
-
pending = safeArrayShift(pendingQueue) ?? null;
|
|
855
|
+
if (pending === null) return true;
|
|
856
|
+
while (pending !== null) {
|
|
857
|
+
const budget = safeMathMin(SEND_TIMEOUT_MS, remaining());
|
|
858
|
+
if (budget <= 0) return false;
|
|
859
|
+
const status = await send(pending.body, budget);
|
|
860
|
+
if (!counting) return false;
|
|
861
|
+
if (status >= 200 && status < 300) {
|
|
862
|
+
release(pending);
|
|
863
|
+
pending = safeArrayShift(pendingQueue) ?? null;
|
|
864
|
+
continue;
|
|
1156
865
|
}
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
866
|
+
// The exact bodies stay pending for the next interval: the same
|
|
867
|
+
// (build, run, seq) key, so a retry the ingestion already stored is a
|
|
868
|
+
// duplicate it recognizes, never a double count.
|
|
869
|
+
if (exiting) return false;
|
|
1160
870
|
consecutiveFailures++;
|
|
1161
|
-
|
|
1162
|
-
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) disable('repeated flush failures', error);
|
|
871
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) turnOff(`${MAX_CONSECUTIVE_FAILURES} failed sends`);
|
|
1163
872
|
return false;
|
|
1164
873
|
}
|
|
874
|
+
consecutiveFailures = 0;
|
|
875
|
+
return true;
|
|
1165
876
|
}
|
|
1166
877
|
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const pendingNames = safeArrayFilter(readdirSync(spoolDirectory),
|
|
1183
|
-
name => safeRegExpTest(/^pending-[0-9a-f]{64}\.json$/, name));
|
|
1184
|
-
const described = safeArrayMap(pendingNames, name => {
|
|
1185
|
-
const path = join(spoolDirectory, name);
|
|
1186
|
-
try {
|
|
1187
|
-
const stats = statSync(path);
|
|
1188
|
-
return { name, path, bytes: stats.size, modified: stats.mtimeMs };
|
|
1189
|
-
} catch {
|
|
1190
|
-
return null;
|
|
1191
|
-
}
|
|
1192
|
-
});
|
|
1193
|
-
const existing = safeArraySort(
|
|
1194
|
-
safeArrayFilter(described, item => item !== null && item !== undefined),
|
|
1195
|
-
(left, right) => left.modified - right.modified || compareStrings(left.name, right.name),
|
|
1196
|
-
);
|
|
1197
|
-
let retainedBytes = safeArrayReduce(existing, (total, file) => total + file.bytes, 0);
|
|
1198
|
-
const receiptPath = join(spoolDirectory, 'dropped.json');
|
|
1199
|
-
let dropped = { chunks: 0, bytes: 0, payload: null };
|
|
1200
|
-
try {
|
|
1201
|
-
const parsed = safeJsonParse(readFileSync(receiptPath, 'utf8'));
|
|
1202
|
-
if (safeNumberIsSafeInteger(parsed.chunks) && parsed.chunks >= 0 &&
|
|
1203
|
-
safeNumberIsSafeInteger(parsed.bytes) && parsed.bytes >= 0 &&
|
|
1204
|
-
(parsed.payload === null || typeof parsed.payload === 'object')) dropped = parsed;
|
|
1205
|
-
} catch {
|
|
1206
|
-
// No prior eviction receipt is the expected case.
|
|
1207
|
-
}
|
|
1208
|
-
while (existing.length >= MAX_SPOOL_FILES || retainedBytes + bodyBytes > MAX_SPOOL_BYTES) {
|
|
1209
|
-
const oldest = safeArrayShift(existing);
|
|
1210
|
-
if (!oldest) break;
|
|
1211
|
-
try {
|
|
1212
|
-
unlinkSync(oldest.path);
|
|
1213
|
-
retainedBytes -= oldest.bytes;
|
|
1214
|
-
dropped.chunks++;
|
|
1215
|
-
dropped.bytes += oldest.bytes;
|
|
1216
|
-
dropped.payload = null;
|
|
1217
|
-
} catch {
|
|
1218
|
-
break;
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
if (retainedBytes + bodyBytes > MAX_SPOOL_BYTES) return;
|
|
1222
|
-
if (dropped.chunks > 0) {
|
|
1223
|
-
writeFileSync(receiptPath, safeJsonStringify(dropped), { encoding: 'utf8', mode: 0o600 });
|
|
1224
|
-
}
|
|
1225
|
-
const temporaryPath = join(spoolDirectory, `.pending-${process.pid}-${digest}.tmp`);
|
|
1226
|
-
writeFileSync(temporaryPath, body, { encoding: 'utf8', mode: 0o600 });
|
|
1227
|
-
renameSync(temporaryPath, finalPath);
|
|
1228
|
-
} catch (error) {
|
|
1229
|
-
debug('crash payload could not be spooled', error);
|
|
878
|
+
// Crash and exit spool: fixed slots in tmpdir (`pending-<0..7>.json`), at
|
|
879
|
+
// most SPOOL_FILE_BYTES each, written once per process with one synchronous
|
|
880
|
+
// exclusive-create write and sent by the next process of the same build and
|
|
881
|
+
// endpoint. The directory is created asynchronously at start; a read-only or
|
|
882
|
+
// full file system just means nothing is spooled.
|
|
883
|
+
const spoolDirectory = join(tmpdir(), `haystack-telemetry-${safeStringSlice(sha256Hex(`${endpoint}\0${token}\0${buildId}`), 0, 24)}`);
|
|
884
|
+
const spoolSlot = join(spoolDirectory, `pending-${hashSeed(runId) % MAX_SPOOL_FILES}.json`);
|
|
885
|
+
safePromiseThen(fsPromises.mkdir(spoolDirectory, { recursive: true, mode: 0o700 }), noop, noop);
|
|
886
|
+
|
|
887
|
+
/** The first queued body that fits a slot, or a fresh one-chunk body of the
|
|
888
|
+
* window bounded by the slot (serialization only; no I/O). */
|
|
889
|
+
function spoolCandidate(reason) {
|
|
890
|
+
if (pending !== null && pending.bytes <= SPOOL_FILE_BYTES) return pending.body;
|
|
891
|
+
for (let index = 0; index < pendingQueue.length; index++) {
|
|
892
|
+
if (pendingQueue[index].bytes <= SPOOL_FILE_BYTES) return pendingQueue[index].body;
|
|
1230
893
|
}
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
}
|
|
1240
|
-
for (let index = 0; index < pendingQueue.length; index++) {
|
|
1241
|
-
const queued = pendingQueue[index];
|
|
1242
|
-
spoolBody(queued.body);
|
|
1243
|
-
sequence = safeMathMax(sequence, queued.payload.seq + 1);
|
|
1244
|
-
}
|
|
1245
|
-
pendingQueue = [];
|
|
1246
|
-
makePending(reason);
|
|
1247
|
-
if (pending) {
|
|
1248
|
-
spoolBody(pending.body);
|
|
1249
|
-
for (let index = 0; index < pendingQueue.length; index++) spoolBody(pendingQueue[index].body);
|
|
1250
|
-
}
|
|
1251
|
-
} catch (error) {
|
|
1252
|
-
debug('crash snapshot failed open', error);
|
|
894
|
+
const taken = takeWindow();
|
|
895
|
+
const units = [];
|
|
896
|
+
let unitsBytes = 0;
|
|
897
|
+
for (let index = 0; index < taken.length; index++) {
|
|
898
|
+
const unit = unitOf(taken[index]);
|
|
899
|
+
if (unitsBytes + unit.bytes > SPOOL_FILE_BYTES - 64 * 1024) break;
|
|
900
|
+
safeArrayPush(units, unit);
|
|
901
|
+
unitsBytes += unit.bytes;
|
|
1253
902
|
}
|
|
903
|
+
return units.length > 0 ? bodyOf(units, safeDateToISOString(new SafeDate()), reason).body : null;
|
|
1254
904
|
}
|
|
1255
905
|
|
|
1256
|
-
|
|
1257
|
-
|
|
906
|
+
function spoolOnce(reason) {
|
|
907
|
+
if (spooled || !counting) return;
|
|
908
|
+
spooled = true;
|
|
1258
909
|
try {
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
safeArrayFilter(readdirSync(spoolDirectory),
|
|
1262
|
-
name => safeRegExpTest(/^pending-[0-9a-f]{64}\.json$/, name)),
|
|
1263
|
-
compareStrings,
|
|
1264
|
-
),
|
|
1265
|
-
0,
|
|
1266
|
-
MAX_SPOOL_FILES,
|
|
1267
|
-
);
|
|
910
|
+
const body = spoolCandidate(reason);
|
|
911
|
+
if (body !== null) writeFileSync(spoolSlot, body, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
1268
912
|
} catch {
|
|
1269
|
-
|
|
913
|
+
// An occupied slot, a read-only or full file system: nothing is spooled.
|
|
1270
914
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
buildId,
|
|
1282
|
-
runId,
|
|
1283
|
-
seq: sequence,
|
|
1284
|
-
ts: safeDateToISOString(new SafeDate()),
|
|
1285
|
-
reason: 'crash-spool-omission',
|
|
1286
|
-
omissions: { counterObservations: 0, probeObservations: 0 },
|
|
1287
|
-
omittedSites: [],
|
|
1288
|
-
executionOmissions: {
|
|
1289
|
-
sampledActions: 0,
|
|
1290
|
-
unsampledActions: 0,
|
|
1291
|
-
omittedFamilyObservations: 0,
|
|
1292
|
-
},
|
|
1293
|
-
deliveryOmissions: {
|
|
1294
|
-
droppedCrashChunks: dropped.chunks,
|
|
1295
|
-
droppedCrashBytes: dropped.bytes,
|
|
1296
|
-
},
|
|
1297
|
-
sites: [],
|
|
1298
|
-
shape: {},
|
|
1299
|
-
branch: {},
|
|
1300
|
-
executions: [],
|
|
1301
|
-
coverage: [],
|
|
1302
|
-
};
|
|
1303
|
-
// Reserve this identity before any asynchronous request. Normal and
|
|
1304
|
-
// beforeExit flushes may run concurrently and must never reuse it.
|
|
1305
|
-
if (payload.runId === runId && safeNumberIsSafeInteger(payload.seq)) {
|
|
1306
|
-
sequence = safeMathMax(sequence, payload.seq + 1);
|
|
1307
|
-
}
|
|
1308
|
-
if (!dropped.payload) {
|
|
1309
|
-
const persisted = { chunks: dropped.chunks, bytes: dropped.bytes, payload };
|
|
1310
|
-
const temporaryPath = join(spoolDirectory, `.dropped-${process.pid}.tmp`);
|
|
1311
|
-
writeFileSync(temporaryPath, safeJsonStringify(persisted), { encoding: 'utf8', mode: 0o600 });
|
|
1312
|
-
renameSync(temporaryPath, receiptPath);
|
|
1313
|
-
}
|
|
1314
|
-
const response = await safeFetch(endpoint, {
|
|
1315
|
-
method: 'POST',
|
|
1316
|
-
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
1317
|
-
body: safeJsonStringify(payload),
|
|
1318
|
-
signal: safeAbortTimeout(10_000),
|
|
1319
|
-
});
|
|
1320
|
-
if (!responseAccepted(response)) return;
|
|
1321
|
-
// The receiver accepted this exact omission count. A retry would add
|
|
1322
|
-
// the same loss again under a new process run, so acknowledge it now.
|
|
1323
|
-
unlinkSync(receiptPath);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async function replaySpooled() {
|
|
918
|
+
for (let slot = 0; slot < MAX_SPOOL_FILES && counting && !exiting; slot++) {
|
|
919
|
+
const path = join(spoolDirectory, `pending-${slot}.json`);
|
|
920
|
+
let body;
|
|
921
|
+
try {
|
|
922
|
+
body = await fsPromises.readFile(path, 'utf8');
|
|
923
|
+
} catch {
|
|
924
|
+
continue;
|
|
1324
925
|
}
|
|
1325
|
-
} catch (error) {
|
|
1326
|
-
if (error?.code !== 'ENOENT') debug('crash spool omission replay failed', error);
|
|
1327
|
-
}
|
|
1328
|
-
for (let index = 0; index < files.length; index++) {
|
|
1329
|
-
const name = files[index];
|
|
1330
|
-
const filePath = join(spoolDirectory, name);
|
|
1331
926
|
try {
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
unlinkSync(filePath);
|
|
1348
|
-
} catch (error) {
|
|
1349
|
-
debug('spooled crash payload replay failed', error);
|
|
1350
|
-
break;
|
|
927
|
+
// A body cut short by a crash mid-write is not JSON: never sendable.
|
|
928
|
+
safeJsonParse(body);
|
|
929
|
+
} catch {
|
|
930
|
+
safePromiseThen(fsPromises.unlink(path), noop, noop);
|
|
931
|
+
continue;
|
|
932
|
+
}
|
|
933
|
+
const status = await send(body, SEND_TIMEOUT_MS);
|
|
934
|
+
// Accepted, or refused for good (a 4xx other than 408 timeout, 425 too
|
|
935
|
+
// early and 429 rate limit, which say "later"): the slot is free.
|
|
936
|
+
// Otherwise it waits for a later process.
|
|
937
|
+
const retryLater = status === 408 || status === 425 || status === 429;
|
|
938
|
+
if ((status >= 200 && status < 300) || (status >= 400 && status < 500 && !retryLater)) {
|
|
939
|
+
await safePromiseThen(fsPromises.unlink(path), noop, noop);
|
|
940
|
+
} else {
|
|
941
|
+
return;
|
|
1351
942
|
}
|
|
1352
943
|
}
|
|
1353
944
|
}
|
|
1354
945
|
|
|
1355
946
|
function flush(reason = 'manual') {
|
|
1356
947
|
if (flushInFlight) return flushInFlight;
|
|
1357
|
-
|
|
1358
|
-
finalizeAction(processAction, processAction.outcome || (process.exitCode ? 'error' : 'success'));
|
|
1359
|
-
if (reason !== 'before-exit' && reason !== 'process-exit' &&
|
|
1360
|
-
reason !== 'uncaught-exception' && reason !== 'unhandled-rejection') {
|
|
1361
|
-
continueProcessAction();
|
|
1362
|
-
}
|
|
1363
|
-
}
|
|
1364
|
-
flushInFlight = safePromiseFinally(doFlush(reason), () => {
|
|
948
|
+
flushInFlight = safePromiseFinally(safePromiseThen(doFlush(reason), result => result, () => false), () => {
|
|
1365
949
|
flushInFlight = null;
|
|
1366
950
|
});
|
|
1367
951
|
return flushInFlight;
|
|
1368
952
|
}
|
|
1369
953
|
|
|
1370
|
-
const timer =
|
|
954
|
+
const timer = safeSetInterval(() => {
|
|
955
|
+
void flush('interval');
|
|
956
|
+
}, flushIntervalMs);
|
|
1371
957
|
timer.unref();
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
spoolCurrent('uncaught-exception');
|
|
958
|
+
// The classification budget refills each second; a blocked event loop keeps
|
|
959
|
+
// it spent, which is exactly when classification should not add CPU.
|
|
960
|
+
const budgetTimer = safeSetInterval(() => {
|
|
961
|
+
budgetUsed = 0;
|
|
962
|
+
}, 1_000);
|
|
963
|
+
budgetTimer.unref();
|
|
964
|
+
processHost.on('uncaughtExceptionMonitor', () => {
|
|
965
|
+
spoolOnce('uncaught-exception');
|
|
1381
966
|
});
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
processAction.terminalError = code ? 'error:generic' : processAction.terminalError;
|
|
1385
|
-
finalizeAction(processAction, code ? 'error' : processAction.outcome || 'success');
|
|
1386
|
-
}
|
|
1387
|
-
if (pending || pendingQueue.length || safeMapSize(currentExecutions) ||
|
|
1388
|
-
safeMapSize(currentShape) || safeMapSize(currentBranch)) {
|
|
1389
|
-
spoolCurrent('process-exit');
|
|
1390
|
-
}
|
|
967
|
+
processHost.once('exit', () => {
|
|
968
|
+
spoolOnce('process-exit');
|
|
1391
969
|
});
|
|
1392
|
-
|
|
1393
|
-
if (
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
970
|
+
processHost.once('beforeExit', () => {
|
|
971
|
+
if (!counting || exiting) return;
|
|
972
|
+
// One absolute deadline for everything the way out does: a send in flight,
|
|
973
|
+
// serialization, and every send after it. At the deadline whatever is left
|
|
974
|
+
// is abandoned (and the exit handler may spool one bounded body).
|
|
975
|
+
exiting = true;
|
|
976
|
+
exitDeadline = safeDateNow() + EXIT_FLUSH_MS;
|
|
977
|
+
// Sends never keep the process alive; this one timer does, until the exit
|
|
978
|
+
// flush settles or the deadline, whichever is first.
|
|
979
|
+
const cut = safeSetTimeout(() => {
|
|
980
|
+
if (currentSend !== null) currentSend();
|
|
981
|
+
}, EXIT_FLUSH_MS);
|
|
982
|
+
const settled = flushInFlight ?? flush('before-exit');
|
|
983
|
+
safePromiseFinally(settled, () => safeClearTimeout(cut));
|
|
1399
984
|
});
|
|
1400
985
|
|
|
1401
986
|
const control = safeObjectFreeze({
|
|
1402
987
|
enabled: true,
|
|
1403
988
|
runtimeVersion: RUNTIME_VERSION,
|
|
989
|
+
samplerVersion: CONTRACT.SERVER_SAMPLER_VERSION,
|
|
1404
990
|
buildId,
|
|
1405
991
|
runId,
|
|
1406
992
|
flush,
|
|
1407
|
-
selectProbe(name) {
|
|
1408
|
-
if (!enabled || processAction.suppressed) return passThrough;
|
|
1409
|
-
if (name === '__hstValue') return observe;
|
|
1410
|
-
if (name === '__hstBranch') return branch;
|
|
1411
|
-
if (name === '__hstThrow') return thrown;
|
|
1412
|
-
return null;
|
|
1413
|
-
},
|
|
1414
993
|
selectSiteProbe(name, site) {
|
|
1415
|
-
if (!
|
|
1416
|
-
const state = safeMapGet(
|
|
1417
|
-
if (!state) return null;
|
|
1418
|
-
|
|
1419
|
-
const fields = state.metadata.allowedFields;
|
|
1420
|
-
return (value) => {
|
|
1421
|
-
if (!enabled || !collecting || state.saturated) return value;
|
|
1422
|
-
return observeState(state, value, fields);
|
|
1423
|
-
};
|
|
1424
|
-
}
|
|
1425
|
-
if (name === '__hstBranch') return (value) => {
|
|
1426
|
-
if (!enabled || !collecting || state.saturated) return value;
|
|
1427
|
-
return branchState(state, value);
|
|
1428
|
-
};
|
|
1429
|
-
if (name === '__hstThrow') return (value) => {
|
|
1430
|
-
if (!enabled || !collecting || state.saturated) return value;
|
|
1431
|
-
return thrownState(state, value);
|
|
1432
|
-
};
|
|
1433
|
-
return null;
|
|
1434
|
-
},
|
|
1435
|
-
snapshotCurrentAction() {
|
|
1436
|
-
try {
|
|
1437
|
-
const action = currentAction();
|
|
1438
|
-
if (!action || action.actionKind !== 'http-server') return null;
|
|
1439
|
-
return safeObjectFreeze({
|
|
1440
|
-
actionKind: action.actionKind,
|
|
1441
|
-
truncated: action.truncated,
|
|
1442
|
-
omittedEvents: action.omittedEvents,
|
|
1443
|
-
events: safeObjectFreeze(safeArrayMap(action.events, event => safeObjectFreeze({
|
|
1444
|
-
siteId: event.siteId,
|
|
1445
|
-
kind: event.kind,
|
|
1446
|
-
scope: event.scope,
|
|
1447
|
-
outcome: event.outcome,
|
|
1448
|
-
observations: event.observations,
|
|
1449
|
-
}))),
|
|
1450
|
-
});
|
|
1451
|
-
} catch (error) {
|
|
1452
|
-
debug('current action snapshot failed open', error);
|
|
1453
|
-
return null;
|
|
1454
|
-
}
|
|
994
|
+
if (!counting) return null;
|
|
995
|
+
const state = safeMapGet(statesByProbe, site);
|
|
996
|
+
if (!state || state.probeName !== name) return null;
|
|
997
|
+
return siteProbe(state);
|
|
1455
998
|
},
|
|
1456
999
|
status() {
|
|
1457
1000
|
return {
|
|
1458
|
-
|
|
1459
|
-
|
|
1001
|
+
counting,
|
|
1002
|
+
offReason,
|
|
1460
1003
|
sequence,
|
|
1461
|
-
|
|
1462
|
-
|
|
1004
|
+
activeSites: activeStates().length,
|
|
1005
|
+
memory: { ...memory, total: used(), cap: MAX_BYTES },
|
|
1006
|
+
consecutiveFailures,
|
|
1463
1007
|
pending: pending !== null || pendingQueue.length > 0,
|
|
1464
|
-
shapeKeys: safeMapSize(currentShape),
|
|
1465
|
-
branchKeys: safeMapSize(currentBranch),
|
|
1466
|
-
executionFamilies: safeMapSize(currentExecutions),
|
|
1467
1008
|
};
|
|
1468
1009
|
},
|
|
1469
1010
|
});
|
|
@@ -1482,18 +1023,52 @@ function installRuntime(options = {}) {
|
|
|
1482
1023
|
value: control,
|
|
1483
1024
|
});
|
|
1484
1025
|
}
|
|
1485
|
-
debug(`enabled for build ${buildId}`);
|
|
1486
1026
|
return control;
|
|
1487
1027
|
}
|
|
1488
1028
|
|
|
1489
|
-
function
|
|
1029
|
+
function selectProbe(_name, fallback) {
|
|
1030
|
+
// Sampler 5 binds per site (safeSelectSiteProbe); the shared probe is a
|
|
1031
|
+
// pass-through kept for generated fallback objects.
|
|
1032
|
+
return fallback;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function trustedSelectSiteProbe(name, site, fallback) {
|
|
1036
|
+
try {
|
|
1037
|
+
const selected = activeControl?.selectSiteProbe(name, site);
|
|
1038
|
+
return typeof selected === 'function' ? selected : fallback;
|
|
1039
|
+
} catch {
|
|
1040
|
+
return fallback;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
Object.defineProperty(trustedSelectSiteProbe, 'haystackRuntimeIntegrity', {
|
|
1045
|
+
configurable: false,
|
|
1046
|
+
enumerable: false,
|
|
1047
|
+
writable: false,
|
|
1048
|
+
value: RUNTIME_INTEGRITY,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
function safeSelectProbe(_expectedIntegrity, _name, fallback) {
|
|
1052
|
+
return fallback;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function safeSelectSiteProbe(expectedIntegrity, name, site, fallback) {
|
|
1056
|
+
try {
|
|
1057
|
+
if (expectedIntegrity !== RUNTIME_INTEGRITY) return fallback;
|
|
1058
|
+
return trustedSelectSiteProbe(name, site, fallback);
|
|
1059
|
+
} catch {
|
|
1060
|
+
return fallback;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function install() {
|
|
1490
1065
|
try {
|
|
1491
|
-
|
|
1492
|
-
|
|
1066
|
+
installResult = installRuntime();
|
|
1067
|
+
return installResult;
|
|
1068
|
+
} catch {
|
|
1493
1069
|
// Bootstrap is injected into the application entry point, so this final
|
|
1494
1070
|
// boundary must catch every setup failure. Telemetry is never allowed to
|
|
1495
1071
|
// prevent the host application from starting.
|
|
1496
|
-
debug('installation failed open', error);
|
|
1497
1072
|
return { enabled: false, reason: 'installation-failed' };
|
|
1498
1073
|
}
|
|
1499
1074
|
}
|