@haystackeditor/cli 0.15.26 → 0.15.27
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/README.md +39 -3
- package/dist/assets/hooks/package-lock.json +598 -0
- package/dist/assets/skills/map-cloud-verifier-universe/SKILL.md +2051 -0
- package/dist/assets/skills/map-cloud-verifier-universe/agents/openai.yaml +4 -0
- package/dist/assets/skills/map-cloud-verifier-universe/references/output-contract.md +3411 -0
- package/dist/assets/telemetry/browser-runtime.js +861 -0
- package/dist/assets/telemetry/runtime.cjs +1509 -0
- package/dist/commands/cloud-verifier-behaviors.d.ts +27 -0
- package/dist/commands/cloud-verifier-behaviors.js +218 -0
- package/dist/commands/cloud-verifier-data-store-census.d.ts +47 -0
- package/dist/commands/cloud-verifier-data-store-census.js +539 -0
- package/dist/commands/cloud-verifier-data-store-drift.d.ts +42 -0
- package/dist/commands/cloud-verifier-data-store-drift.js +158 -0
- package/dist/commands/cloud-verifier-identity-census.d.ts +88 -0
- package/dist/commands/cloud-verifier-identity-census.js +4057 -0
- package/dist/commands/cloud-verifier-materialization.d.ts +16 -0
- package/dist/commands/cloud-verifier-materialization.js +704 -0
- package/dist/commands/cloud-verifier-pascal-selector-census.d.ts +29 -0
- package/dist/commands/cloud-verifier-pascal-selector-census.js +1382 -0
- package/dist/commands/cloud-verifier-python-manifest-selector-census.d.ts +27 -0
- package/dist/commands/cloud-verifier-python-manifest-selector-census.js +2015 -0
- package/dist/commands/cloud-verifier-specialized-operational-census.d.ts +51 -0
- package/dist/commands/cloud-verifier-specialized-operational-census.js +11432 -0
- package/dist/commands/cloud-verifier-universe.d.ts +31 -0
- package/dist/commands/cloud-verifier-universe.js +10178 -0
- package/dist/commands/design-verify.d.ts +23 -0
- package/dist/commands/design-verify.js +142 -0
- package/dist/commands/hooks.js +2 -2
- package/dist/commands/prepare-universe-review.d.ts +115 -0
- package/dist/commands/prepare-universe-review.js +1092 -0
- package/dist/commands/production-source-deny-policy.d.ts +15 -0
- package/dist/commands/production-source-deny-policy.js +100 -0
- package/dist/commands/scaffold-provisional-universe.d.ts +468 -0
- package/dist/commands/scaffold-provisional-universe.js +808 -0
- package/dist/commands/schema-cmd.js +32 -0
- package/dist/commands/skills.d.ts +2 -2
- package/dist/commands/skills.js +415 -47
- package/dist/commands/telemetry.d.ts +53 -0
- package/dist/commands/telemetry.js +832 -0
- package/dist/index.js +134 -12
- package/dist/triage/runner.js +7 -6
- package/dist/utils/design-verifier-api.d.ts +129 -0
- package/dist/utils/design-verifier-api.js +175 -0
- package/dist/utils/design-verifier-result.d.ts +52 -0
- package/dist/utils/design-verifier-result.js +299 -0
- package/dist/utils/haystack-api.d.ts +2 -0
- package/dist/utils/haystack-api.js +10 -3
- package/dist/utils/hooks.js +2 -1
- package/package.json +7 -2
|
@@ -0,0 +1,1509 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Haystack's automatically injected Node telemetry runtime.
|
|
5
|
+
*
|
|
6
|
+
* This file is copied beside compiled application code by
|
|
7
|
+
* `haystack telemetry instrument`. It has no package dependencies, records no
|
|
8
|
+
* raw values, and is inert unless all three HAYSTACK_TELEMETRY_* activation
|
|
9
|
+
* variables are present. Every application-facing hook returns the original
|
|
10
|
+
* value and catches its own errors so telemetry cannot change app behavior.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { createHash, randomBytes } = require('node:crypto');
|
|
14
|
+
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
15
|
+
const diagnosticsChannel = require('node:diagnostics_channel');
|
|
16
|
+
const { types: utilTypes } = require('node:util');
|
|
17
|
+
const { mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } = require('node:fs');
|
|
18
|
+
const { tmpdir } = require('node:os');
|
|
19
|
+
const { join } = require('node:path');
|
|
20
|
+
|
|
21
|
+
// Capture the few mutable language intrinsics used on customer values before
|
|
22
|
+
// application code runs. Calling a customer-patched prototype method from a
|
|
23
|
+
// probe would itself be an application-visible side effect.
|
|
24
|
+
const safeStringTrim = Function.call.bind(String.prototype.trim);
|
|
25
|
+
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
|
+
const safeStringSlice = Function.call.bind(String.prototype.slice);
|
|
30
|
+
const safeStringCharCodeAt = Function.call.bind(String.prototype.charCodeAt);
|
|
31
|
+
const safeRegExpTest = Function.call.bind(RegExp.prototype.test);
|
|
32
|
+
const safeJsonParse = JSON.parse;
|
|
33
|
+
const safeJsonStringify = JSON.stringify;
|
|
34
|
+
const safeArrayIsArray = Array.isArray;
|
|
35
|
+
const safeArrayFilter = Function.call.bind(Array.prototype.filter);
|
|
36
|
+
const safeArrayFind = Function.call.bind(Array.prototype.find);
|
|
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);
|
|
42
|
+
const safeArraySort = Function.call.bind(Array.prototype.sort);
|
|
43
|
+
const safeNumber = Number;
|
|
44
|
+
const safeNumberIsFinite = Number.isFinite;
|
|
45
|
+
const safeNumberIsSafeInteger = Number.isSafeInteger;
|
|
46
|
+
const safeNumberParseInt = Number.parseInt;
|
|
47
|
+
const safeMathImul = Math.imul;
|
|
48
|
+
const safeMathMax = Math.max;
|
|
49
|
+
const safeMathMin = Math.min;
|
|
50
|
+
const safeBufferByteLength = Buffer.byteLength;
|
|
51
|
+
const safeObjectKeys = Object.keys;
|
|
52
|
+
const safeObjectEntries = Object.entries;
|
|
53
|
+
const safeObjectCreate = Object.create;
|
|
54
|
+
const safeGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
55
|
+
const safeGetPrototypeOf = Object.getPrototypeOf;
|
|
56
|
+
const safeHasOwn = Function.call.bind(Object.prototype.hasOwnProperty);
|
|
57
|
+
const safeMapGet = Function.call.bind(Map.prototype.get);
|
|
58
|
+
const safeMapSet = Function.call.bind(Map.prototype.set);
|
|
59
|
+
const safeMapHas = Function.call.bind(Map.prototype.has);
|
|
60
|
+
const safeMapSize = Function.call.bind(Object.getOwnPropertyDescriptor(Map.prototype, 'size').get);
|
|
61
|
+
const safeMapEntries = Function.call.bind(Map.prototype.entries);
|
|
62
|
+
const safeMapValues = Function.call.bind(Map.prototype.values);
|
|
63
|
+
const safeWeakMapGet = Function.call.bind(WeakMap.prototype.get);
|
|
64
|
+
const safeWeakMapSet = Function.call.bind(WeakMap.prototype.set);
|
|
65
|
+
const safeSetAdd = Function.call.bind(Set.prototype.add);
|
|
66
|
+
const safeSetValues = Function.call.bind(Set.prototype.values);
|
|
67
|
+
const safeArrayPush = Function.call.bind(Array.prototype.push);
|
|
68
|
+
const SafeMap = Map;
|
|
69
|
+
const SafeSet = Set;
|
|
70
|
+
const SafeWeakMap = WeakMap;
|
|
71
|
+
const SafeDate = Date;
|
|
72
|
+
const safeDateNow = Date.now;
|
|
73
|
+
const safeDateToISOString = Function.call.bind(Date.prototype.toISOString);
|
|
74
|
+
const safeMapIteratorNext = Function.call.bind(
|
|
75
|
+
Object.getPrototypeOf(safeMapEntries(new SafeMap())).next,
|
|
76
|
+
);
|
|
77
|
+
const safeSetIteratorNext = Function.call.bind(
|
|
78
|
+
Object.getPrototypeOf(safeSetValues(new SafeSet())).next,
|
|
79
|
+
);
|
|
80
|
+
const hashPrototype = Object.getPrototypeOf(createHash('sha256'));
|
|
81
|
+
const safeHashUpdate = Function.call.bind(hashPrototype.update);
|
|
82
|
+
const safeHashDigest = Function.call.bind(hashPrototype.digest);
|
|
83
|
+
const safeIsProxy = utilTypes.isProxy;
|
|
84
|
+
const safeIsNativeError = utilTypes.isNativeError;
|
|
85
|
+
const safeActionGetStore = Function.call.bind(AsyncLocalStorage.prototype.getStore);
|
|
86
|
+
const safeActionEnterWith = Function.call.bind(AsyncLocalStorage.prototype.enterWith);
|
|
87
|
+
const safePromiseFinally = Function.call.bind(Promise.prototype.finally);
|
|
88
|
+
const safeFetch = typeof fetch === 'function' ? fetch : null;
|
|
89
|
+
const safeAbortTimeout = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout : null;
|
|
90
|
+
const safeResponseOk = typeof Response === 'function'
|
|
91
|
+
? Function.call.bind(Object.getOwnPropertyDescriptor(Response.prototype, 'ok').get)
|
|
92
|
+
: null;
|
|
93
|
+
const safeResponseStatus = typeof Response === 'function'
|
|
94
|
+
? Function.call.bind(Object.getOwnPropertyDescriptor(Response.prototype, 'status').get)
|
|
95
|
+
: null;
|
|
96
|
+
const safeClearInterval = clearInterval;
|
|
97
|
+
const SafeError = Error;
|
|
98
|
+
const nativeErrorPrototypes = Object.freeze({
|
|
99
|
+
type: TypeError.prototype,
|
|
100
|
+
range: RangeError.prototype,
|
|
101
|
+
syntax: SyntaxError.prototype,
|
|
102
|
+
reference: ReferenceError.prototype,
|
|
103
|
+
aggregate: typeof AggregateError === 'function' ? AggregateError.prototype : null,
|
|
104
|
+
uri: URIError.prototype,
|
|
105
|
+
eval: EvalError.prototype,
|
|
106
|
+
generic: Error.prototype,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
function compareStrings(left, right) {
|
|
110
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function mapEntriesArray(map) {
|
|
114
|
+
const result = [];
|
|
115
|
+
const iterator = safeMapEntries(map);
|
|
116
|
+
while (true) {
|
|
117
|
+
const step = safeMapIteratorNext(iterator);
|
|
118
|
+
if (step.done) return result;
|
|
119
|
+
safeArrayPush(result, step.value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function mapValuesArray(map) {
|
|
124
|
+
const entries = mapEntriesArray(map);
|
|
125
|
+
const result = [];
|
|
126
|
+
for (let index = 0; index < entries.length; index++) safeArrayPush(result, entries[index][1]);
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function setValuesArray(set) {
|
|
131
|
+
const result = [];
|
|
132
|
+
const iterator = safeSetValues(set);
|
|
133
|
+
while (true) {
|
|
134
|
+
const step = safeSetIteratorNext(iterator);
|
|
135
|
+
if (step.done) return result;
|
|
136
|
+
safeArrayPush(result, step.value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function objectFromEntryArray(entries) {
|
|
141
|
+
const result = safeObjectCreate(null);
|
|
142
|
+
for (let index = 0; index < entries.length; index++) {
|
|
143
|
+
const entry = entries[index];
|
|
144
|
+
result[entry[0]] = entry[1];
|
|
145
|
+
}
|
|
146
|
+
return result;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function objectFromMap(map) {
|
|
150
|
+
return objectFromEntryArray(mapEntriesArray(map));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function sha256Hex(value) {
|
|
154
|
+
const hash = createHash('sha256');
|
|
155
|
+
safeHashUpdate(hash, value);
|
|
156
|
+
return safeHashDigest(hash, 'hex');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function responseAccepted(response) {
|
|
160
|
+
try {
|
|
161
|
+
return safeResponseOk ? safeResponseOk(response) : false;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function responseStatus(response) {
|
|
168
|
+
try {
|
|
169
|
+
return safeResponseStatus ? safeResponseStatus(response) : 0;
|
|
170
|
+
} catch {
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const RUNTIME_VERSION = 1;
|
|
176
|
+
const SAMPLER_VERSION = 3;
|
|
177
|
+
const DEFAULT_FLUSH_INTERVAL_MS = 60_000;
|
|
178
|
+
const DEFAULT_SITE_OBSERVATIONS = 8;
|
|
179
|
+
const MAX_SITE_OBSERVATIONS = 4_096;
|
|
180
|
+
const MAX_SAFE_COUNT = Number.MAX_SAFE_INTEGER;
|
|
181
|
+
const MAX_COUNTER_KEYS = 50_000;
|
|
182
|
+
const MAX_EXECUTION_FAMILIES = 64;
|
|
183
|
+
const MAX_EXECUTION_EVENTS = 48;
|
|
184
|
+
const MAX_EXECUTION_ACTION_BYTES = 24 * 1024;
|
|
185
|
+
const MAX_EXECUTION_BYTES = 1024 * 1024;
|
|
186
|
+
const MAX_PAYLOAD_BYTES = 4 * 1024 * 1024;
|
|
187
|
+
const PAYLOAD_CHUNK_BUDGET = MAX_PAYLOAD_BYTES - 256 * 1024;
|
|
188
|
+
const MAX_SPOOL_FILES = 8;
|
|
189
|
+
const MAX_SPOOL_BYTES = 16 * 1024 * 1024;
|
|
190
|
+
const SAMPLE_FIRST_ACTIONS = 4;
|
|
191
|
+
const DEFAULT_PROCESS_SAMPLE_RATE = 1;
|
|
192
|
+
const DEFAULT_ACTION_SAMPLE_RATE = 64;
|
|
193
|
+
const MAX_ACTION_SAMPLE_RATE = 4_096;
|
|
194
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
195
|
+
const CONTROL_GLOBAL = '__haystackTelemetryControlV1';
|
|
196
|
+
const CONTROL_CHANNEL = '__HAYSTACK_TELEMETRY_CONTROL_CHANNEL__';
|
|
197
|
+
const RUNTIME_INTEGRITY = '__HAYSTACK_TELEMETRY_RUNTIME_INTEGRITY__';
|
|
198
|
+
const SUPPRESSED_ACTION = Object.freeze({ suppressed: true });
|
|
199
|
+
|
|
200
|
+
let installed = false;
|
|
201
|
+
let activeControl = null;
|
|
202
|
+
const processHost = require('node:process');
|
|
203
|
+
|
|
204
|
+
function selectProbe(name, fallback) {
|
|
205
|
+
try {
|
|
206
|
+
const selected = activeControl?.selectProbe(name);
|
|
207
|
+
return typeof selected === 'function' ? selected : fallback;
|
|
208
|
+
} catch {
|
|
209
|
+
return fallback;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function trustedSelectProbe(name, fallback) {
|
|
214
|
+
return selectProbe(name, fallback);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
Object.defineProperty(trustedSelectProbe, 'haystackRuntimeIntegrity', {
|
|
218
|
+
configurable: false,
|
|
219
|
+
enumerable: false,
|
|
220
|
+
writable: false,
|
|
221
|
+
value: RUNTIME_INTEGRITY,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
function safeSelectProbe(expectedIntegrity, name, fallback) {
|
|
225
|
+
try {
|
|
226
|
+
if (expectedIntegrity !== RUNTIME_INTEGRITY) return fallback;
|
|
227
|
+
return trustedSelectProbe(name, fallback);
|
|
228
|
+
} catch {
|
|
229
|
+
return fallback;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function safeSelectSiteProbe(expectedIntegrity, name, site, fallback) {
|
|
234
|
+
try {
|
|
235
|
+
if (expectedIntegrity !== RUNTIME_INTEGRITY) return fallback;
|
|
236
|
+
const selected = activeControl?.selectSiteProbe(name, site);
|
|
237
|
+
return typeof selected === 'function' ? selected : fallback;
|
|
238
|
+
} catch {
|
|
239
|
+
return fallback;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function debug(message, error) {
|
|
244
|
+
if (process.env.HAYSTACK_TELEMETRY_DEBUG !== '1') return;
|
|
245
|
+
const detail = safeIsNativeError(error) ? ': native error' : error ? ': telemetry error' : '';
|
|
246
|
+
process.stderr.write(`[haystack-telemetry] ${message}${detail}\n`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function safeSegment(value, fallback) {
|
|
250
|
+
return typeof value === 'string' && safeRegExpTest(/^[A-Za-z0-9._-]{1,128}$/, value) ? value : fallback;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function sizeBucket(length) {
|
|
254
|
+
if (length === 0) return 'empty';
|
|
255
|
+
if (length === 1) return 'one';
|
|
256
|
+
if (length <= 4) return 'few';
|
|
257
|
+
return 'many';
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function stringForm(value) {
|
|
261
|
+
if (value.length === 0) return 'empty';
|
|
262
|
+
const trimmed = safeStringTrim(value);
|
|
263
|
+
if (!trimmed) return 'whitespace';
|
|
264
|
+
// Bound parsing work. Long strings still report their generic form/size;
|
|
265
|
+
// their contents are never retained or transmitted.
|
|
266
|
+
if (trimmed.length <= 4_096 && safeStringStartsWith(trimmed, '```') && safeStringEndsWith(trimmed, '```')) {
|
|
267
|
+
const newline = safeStringIndexOf(trimmed, '\n');
|
|
268
|
+
if (newline >= 0) {
|
|
269
|
+
const body = safeStringTrim(safeStringSlice(trimmed, newline + 1, -3));
|
|
270
|
+
try {
|
|
271
|
+
const parsed = safeJsonParse(body);
|
|
272
|
+
return safeArrayIsArray(parsed)
|
|
273
|
+
? 'markdown-fenced-json-array'
|
|
274
|
+
: parsed !== null && typeof parsed === 'object'
|
|
275
|
+
? 'markdown-fenced-json-object'
|
|
276
|
+
: 'markdown-fenced-json-scalar';
|
|
277
|
+
} catch {
|
|
278
|
+
return 'markdown-fenced-text';
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return 'markdown-fenced-text';
|
|
282
|
+
}
|
|
283
|
+
if (trimmed.length <= 4_096) {
|
|
284
|
+
try {
|
|
285
|
+
const parsed = safeJsonParse(trimmed);
|
|
286
|
+
return safeArrayIsArray(parsed)
|
|
287
|
+
? 'json-array'
|
|
288
|
+
: parsed !== null && typeof parsed === 'object'
|
|
289
|
+
? 'json-object'
|
|
290
|
+
: 'json-scalar';
|
|
291
|
+
} catch {
|
|
292
|
+
// Ordinary text is the expected case.
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return 'text';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function valueShape(value) {
|
|
299
|
+
if (value === null) return 'null';
|
|
300
|
+
if (value === undefined) return 'undefined';
|
|
301
|
+
if (typeof value === 'string') return `string:${stringForm(value)}:${sizeBucket(value.length)}`;
|
|
302
|
+
if (typeof value === 'number') return safeNumberIsFinite(value) ? 'number:finite' : 'number:nonfinite';
|
|
303
|
+
if (typeof value === 'boolean') return 'boolean';
|
|
304
|
+
if (typeof value === 'object' && safeIsProxy(value)) return 'object:uninspectable';
|
|
305
|
+
if (safeArrayIsArray(value)) return `array:${sizeBucket(value.length)}`;
|
|
306
|
+
if (typeof value === 'object') {
|
|
307
|
+
try {
|
|
308
|
+
if (safeIsNativeError(value)) return `error:${errorClass(value)}`;
|
|
309
|
+
return `object:${sizeBucket(safeObjectKeys(value).length)}`;
|
|
310
|
+
} catch {
|
|
311
|
+
return 'object:uninspectable';
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return `nonjson:${typeof value}`;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function stableIdentity(prefix, parts) {
|
|
318
|
+
let framed = '';
|
|
319
|
+
for (let index = 0; index < parts.length; index++) {
|
|
320
|
+
if (index > 0) framed += '|';
|
|
321
|
+
const part = parts[index];
|
|
322
|
+
framed += `${safeBufferByteLength(part, 'utf8')}:${part}`;
|
|
323
|
+
}
|
|
324
|
+
return `${prefix}-${safeStringSlice(sha256Hex(framed), 0, 24)}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function canonicalJson(value) {
|
|
328
|
+
if (value === null || typeof value !== 'object') return safeJsonStringify(value);
|
|
329
|
+
if (safeArrayIsArray(value)) {
|
|
330
|
+
let result = '[';
|
|
331
|
+
for (let index = 0; index < value.length; index++) {
|
|
332
|
+
if (index > 0) result += ',';
|
|
333
|
+
result += canonicalJson(value[index]);
|
|
334
|
+
}
|
|
335
|
+
return `${result}]`;
|
|
336
|
+
}
|
|
337
|
+
const keys = safeArraySort(safeObjectKeys(value), compareStrings);
|
|
338
|
+
let result = '{';
|
|
339
|
+
for (let index = 0; index < keys.length; index++) {
|
|
340
|
+
if (index > 0) result += ',';
|
|
341
|
+
const key = keys[index];
|
|
342
|
+
result += `${safeJsonStringify(key)}:${canonicalJson(value[key])}`;
|
|
343
|
+
}
|
|
344
|
+
return `${result}}`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function errorClass(value) {
|
|
348
|
+
try {
|
|
349
|
+
if (value !== null && typeof value === 'object' && safeIsProxy(value)) return 'uninspectable';
|
|
350
|
+
if (safeIsNativeError(value)) {
|
|
351
|
+
// Compare the direct native prototype with references captured before
|
|
352
|
+
// application code. This retains common built-in classes without
|
|
353
|
+
// invoking mutable Symbol.hasInstance/name/tag hooks.
|
|
354
|
+
const prototype = safeGetPrototypeOf(value);
|
|
355
|
+
if (prototype === nativeErrorPrototypes.type) return 'type';
|
|
356
|
+
if (prototype === nativeErrorPrototypes.range) return 'range';
|
|
357
|
+
if (prototype === nativeErrorPrototypes.syntax) return 'syntax';
|
|
358
|
+
if (prototype === nativeErrorPrototypes.reference) return 'reference';
|
|
359
|
+
if (prototype === nativeErrorPrototypes.aggregate) return 'aggregate';
|
|
360
|
+
if (prototype === nativeErrorPrototypes.uri) return 'uri';
|
|
361
|
+
if (prototype === nativeErrorPrototypes.eval) return 'eval';
|
|
362
|
+
return 'generic';
|
|
363
|
+
}
|
|
364
|
+
} catch {
|
|
365
|
+
return 'uninspectable';
|
|
366
|
+
}
|
|
367
|
+
if (value === null) return 'non-error-null';
|
|
368
|
+
return `non-error-${typeof value}`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function httpMethod(value) {
|
|
372
|
+
return value === 'GET' || value === 'POST' || value === 'PUT' || value === 'PATCH' ||
|
|
373
|
+
value === 'DELETE' || value === 'HEAD' || value === 'OPTIONS' ? value : 'OTHER';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function hashSeed(value) {
|
|
377
|
+
let hash = 0x811c9dc5;
|
|
378
|
+
for (let index = 0; index < value.length; index++) {
|
|
379
|
+
hash ^= safeStringCharCodeAt(value, index);
|
|
380
|
+
hash = safeMathImul(hash, 0x01000193);
|
|
381
|
+
}
|
|
382
|
+
return (hash >>> 0) || 1;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function validEndpoint(raw) {
|
|
386
|
+
try {
|
|
387
|
+
const endpoint = new URL(raw);
|
|
388
|
+
if (endpoint.protocol === 'https:') return endpoint.toString();
|
|
389
|
+
if (endpoint.protocol === 'http:' && (endpoint.hostname === '127.0.0.1' ||
|
|
390
|
+
endpoint.hostname === 'localhost' || endpoint.hostname === '::1')) {
|
|
391
|
+
return endpoint.toString();
|
|
392
|
+
}
|
|
393
|
+
} catch {
|
|
394
|
+
// Invalid URLs simply leave telemetry disabled.
|
|
395
|
+
}
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function loadInstrumentation() {
|
|
400
|
+
try {
|
|
401
|
+
const manifest = safeJsonParse(readFileSync(join(__dirname, 'instrumentation.json'), 'utf8'));
|
|
402
|
+
if (manifest.schema_version !== 'haystack-node-telemetry-instrumentation-v3' ||
|
|
403
|
+
!safeArrayIsArray(manifest.sites)) return { sites: new SafeMap(), entrySourcePath: null };
|
|
404
|
+
const siteEntries = safeArrayMap(manifest.sites, site => [site.probe_key, Object.freeze({
|
|
405
|
+
probeKey: site.probe_key,
|
|
406
|
+
siteId: site.site_id,
|
|
407
|
+
sourcePath: site.source_path,
|
|
408
|
+
qualifiedName: site.qualified_name,
|
|
409
|
+
probeKind: site.probe_kind,
|
|
410
|
+
ownerKey: site.owner_key,
|
|
411
|
+
structuralKey: site.structural_key,
|
|
412
|
+
...(site.label ? { label: site.label } : {}),
|
|
413
|
+
allowedFields: safeArrayIsArray(site.allowed_fields)
|
|
414
|
+
? safeArraySlice(site.allowed_fields, 0)
|
|
415
|
+
: [],
|
|
416
|
+
})]);
|
|
417
|
+
const sites = new SafeMap();
|
|
418
|
+
for (let index = 0; index < siteEntries.length; index++) {
|
|
419
|
+
safeMapSet(sites, siteEntries[index][0], siteEntries[index][1]);
|
|
420
|
+
}
|
|
421
|
+
const entryFile = safeArrayIsArray(manifest.instrumented_files)
|
|
422
|
+
? safeArrayFind(manifest.instrumented_files, file => file.output_path === manifest.entry)
|
|
423
|
+
: null;
|
|
424
|
+
return {
|
|
425
|
+
sites,
|
|
426
|
+
buildId: safeSegment(manifest.build_id, 'unknown'),
|
|
427
|
+
entrySourcePath: typeof entryFile?.source_path === 'string' ? entryFile.source_path : null,
|
|
428
|
+
};
|
|
429
|
+
} catch (error) {
|
|
430
|
+
debug('instrumentation manifest could not be read; telemetry stayed disabled', error);
|
|
431
|
+
return { sites: new SafeMap(), buildId: 'unknown', entrySourcePath: null };
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function installRuntime(options = {}) {
|
|
436
|
+
if (installed) return activeControl ?? { enabled: false, reason: 'already-installed' };
|
|
437
|
+
installed = true;
|
|
438
|
+
|
|
439
|
+
const endpoint = validEndpoint(process.env.HAYSTACK_TELEMETRY_ENDPOINT ?? '');
|
|
440
|
+
const token = process.env.HAYSTACK_TELEMETRY_TOKEN;
|
|
441
|
+
if (process.env.HAYSTACK_TELEMETRY !== '1' || !endpoint || !token || !safeFetch || !safeAbortTimeout) {
|
|
442
|
+
return { enabled: false, reason: 'not-configured' };
|
|
443
|
+
}
|
|
444
|
+
if (CONTROL_CHANNEL in processHost) {
|
|
445
|
+
debug('telemetry control name is already owned; telemetry stayed disabled');
|
|
446
|
+
return { enabled: false, reason: 'global-conflict' };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// The manifest-derived build identity is part of every exact DDB family ID.
|
|
450
|
+
// Do not accept a runtime override that could merge unrelated builds.
|
|
451
|
+
const instrumentation = loadInstrumentation();
|
|
452
|
+
const buildId = instrumentation.buildId;
|
|
453
|
+
const sitesByProbe = instrumentation.sites;
|
|
454
|
+
const sitesById = new SafeMap();
|
|
455
|
+
const probeStates = new SafeMap();
|
|
456
|
+
const siteValues = mapValuesArray(sitesByProbe);
|
|
457
|
+
for (let index = 0; index < siteValues.length; index++) {
|
|
458
|
+
const metadata = siteValues[index];
|
|
459
|
+
safeMapSet(sitesById, metadata.siteId, metadata);
|
|
460
|
+
safeMapSet(probeStates, metadata.probeKey, {
|
|
461
|
+
metadata,
|
|
462
|
+
calls: 0,
|
|
463
|
+
saturated: false,
|
|
464
|
+
omitted: 0,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
if (safeMapSize(sitesByProbe) === 0) return { enabled: false, reason: 'missing-instrumentation-manifest' };
|
|
468
|
+
const runId = safeSegment(
|
|
469
|
+
process.env.HAYSTACK_TELEMETRY_RUN_ID,
|
|
470
|
+
`${safeDateNow()}-${process.pid}-${randomBytes(4).toString('hex')}`,
|
|
471
|
+
);
|
|
472
|
+
const intervalRequested = safeNumberParseInt(process.env.HAYSTACK_TELEMETRY_FLUSH_INTERVAL_MS ?? '', 10);
|
|
473
|
+
const flushIntervalMs = safeNumberIsSafeInteger(intervalRequested) && intervalRequested >= 100
|
|
474
|
+
? intervalRequested
|
|
475
|
+
: DEFAULT_FLUSH_INTERVAL_MS;
|
|
476
|
+
const requestedSiteObservations = safeNumberParseInt(
|
|
477
|
+
process.env.HAYSTACK_TELEMETRY_SITE_OBSERVATIONS ?? '',
|
|
478
|
+
10,
|
|
479
|
+
);
|
|
480
|
+
const siteObservationLimit = safeNumberIsSafeInteger(requestedSiteObservations) &&
|
|
481
|
+
requestedSiteObservations >= 1 && requestedSiteObservations <= MAX_SITE_OBSERVATIONS
|
|
482
|
+
? requestedSiteObservations
|
|
483
|
+
: DEFAULT_SITE_OBSERVATIONS;
|
|
484
|
+
const requestedProcessSampleRate = safeNumberParseInt(
|
|
485
|
+
process.env.HAYSTACK_TELEMETRY_PROCESS_SAMPLE_RATE ?? '',
|
|
486
|
+
10,
|
|
487
|
+
);
|
|
488
|
+
const processSampleRate = safeNumberIsSafeInteger(requestedProcessSampleRate) &&
|
|
489
|
+
requestedProcessSampleRate >= 0 && requestedProcessSampleRate <= MAX_ACTION_SAMPLE_RATE
|
|
490
|
+
? requestedProcessSampleRate
|
|
491
|
+
: DEFAULT_PROCESS_SAMPLE_RATE;
|
|
492
|
+
const requestedActionSampleRate = safeNumberParseInt(
|
|
493
|
+
process.env.HAYSTACK_TELEMETRY_ACTION_SAMPLE_RATE ?? '',
|
|
494
|
+
10,
|
|
495
|
+
);
|
|
496
|
+
const actionSampleRate = safeNumberIsSafeInteger(requestedActionSampleRate) &&
|
|
497
|
+
requestedActionSampleRate >= 1 && requestedActionSampleRate <= MAX_ACTION_SAMPLE_RATE
|
|
498
|
+
? requestedActionSampleRate
|
|
499
|
+
: DEFAULT_ACTION_SAMPLE_RATE;
|
|
500
|
+
|
|
501
|
+
let enabled = true;
|
|
502
|
+
let sequence = 0;
|
|
503
|
+
let consecutiveFailures = 0;
|
|
504
|
+
let currentShape = new SafeMap();
|
|
505
|
+
let currentBranch = new SafeMap();
|
|
506
|
+
let currentExecutions = new SafeMap();
|
|
507
|
+
let currentExecutionBytes = 0;
|
|
508
|
+
let omittedExecutionFamilyObservations = 0;
|
|
509
|
+
let sampledActions = 0;
|
|
510
|
+
let unsampledActions = 0;
|
|
511
|
+
let currentOmissions = new SafeMap();
|
|
512
|
+
let saturatedProbeOmissions = 0;
|
|
513
|
+
let pending = null;
|
|
514
|
+
let pendingQueue = [];
|
|
515
|
+
let flushInFlight = null;
|
|
516
|
+
let beforeExitStarted = false;
|
|
517
|
+
const actionStorage = new AsyncLocalStorage();
|
|
518
|
+
const httpActions = new SafeWeakMap();
|
|
519
|
+
let actionSequence = 0;
|
|
520
|
+
const actionSeed = hashSeed(`${buildId}:${runId}`);
|
|
521
|
+
let processSelected = true;
|
|
522
|
+
let hasChildActions = false;
|
|
523
|
+
|
|
524
|
+
function shouldSampleAction(actionKind) {
|
|
525
|
+
const sequence = actionSequence++;
|
|
526
|
+
if (actionKind !== 'process' && !processSelected) {
|
|
527
|
+
unsampledActions++;
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
if (actionKind !== 'process' && sequence <= SAMPLE_FIRST_ACTIONS) {
|
|
531
|
+
sampledActions++;
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
const rate = actionKind === 'process' ? processSampleRate : actionSampleRate;
|
|
535
|
+
const sampled = rate === 1 || (rate > 1 &&
|
|
536
|
+
((actionSeed + safeMathImul(sequence + 1, 0x9e3779b1)) >>> 0) % rate === 0);
|
|
537
|
+
if (actionKind === 'process') processSelected = sampled;
|
|
538
|
+
if (sampled) sampledActions++;
|
|
539
|
+
else unsampledActions++;
|
|
540
|
+
return sampled;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function startAction(actionKind, actionSiteId, actionMethod) {
|
|
544
|
+
if (!shouldSampleAction(actionKind)) return SUPPRESSED_ACTION;
|
|
545
|
+
return {
|
|
546
|
+
suppressed: false,
|
|
547
|
+
actionKind,
|
|
548
|
+
actionSiteId,
|
|
549
|
+
actionMethod,
|
|
550
|
+
events: [],
|
|
551
|
+
eventBytes: 0,
|
|
552
|
+
truncated: false,
|
|
553
|
+
omittedEvents: 0,
|
|
554
|
+
finalized: false,
|
|
555
|
+
outcome: null,
|
|
556
|
+
terminalError: 'none',
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function actionForProbe() {
|
|
561
|
+
const action = hasChildActions ? safeActionGetStore(actionStorage) : processAction;
|
|
562
|
+
return action && !action.finalized ? action : null;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function omitExecutionEvents(action, count = 1) {
|
|
566
|
+
if (!action || action.suppressed || count < 1) return;
|
|
567
|
+
action.truncated = true;
|
|
568
|
+
action.omittedEvents = safeMathMin(MAX_SAFE_COUNT, action.omittedEvents + count);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function currentAction() {
|
|
572
|
+
const action = actionForProbe();
|
|
573
|
+
return action && !action.suppressed ? action : null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function passThrough(_site, value) {
|
|
577
|
+
return value;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function addExecutionEvent(site, kind, scope, outcome) {
|
|
581
|
+
const action = currentAction();
|
|
582
|
+
if (!action) return;
|
|
583
|
+
if (action.truncated) {
|
|
584
|
+
omitExecutionEvents(action);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const metadata = safeMapGet(sitesByProbe, site);
|
|
588
|
+
if (!metadata) return;
|
|
589
|
+
const previous = action.events[action.events.length - 1];
|
|
590
|
+
if (previous && previous.siteId === metadata.siteId && previous.kind === kind &&
|
|
591
|
+
previous.scope === scope && previous.outcome === outcome) {
|
|
592
|
+
previous.observations++;
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
const event = {
|
|
596
|
+
siteId: metadata.siteId,
|
|
597
|
+
kind,
|
|
598
|
+
scope,
|
|
599
|
+
outcome,
|
|
600
|
+
observations: 1,
|
|
601
|
+
};
|
|
602
|
+
const eventBytes = safeBufferByteLength(safeJsonStringify({
|
|
603
|
+
...event,
|
|
604
|
+
sourcePath: metadata.sourcePath,
|
|
605
|
+
qualifiedName: metadata.qualifiedName,
|
|
606
|
+
probeKind: metadata.probeKind,
|
|
607
|
+
}), 'utf8');
|
|
608
|
+
if (action.events.length >= MAX_EXECUTION_EVENTS ||
|
|
609
|
+
action.eventBytes + eventBytes > MAX_EXECUTION_ACTION_BYTES) {
|
|
610
|
+
action.truncated = true;
|
|
611
|
+
omitExecutionEvents(action);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
safeArrayPush(action.events, event);
|
|
615
|
+
action.eventBytes += eventBytes;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function finalizeAction(action, outcome) {
|
|
619
|
+
if (!action || action.suppressed || action.finalized) return;
|
|
620
|
+
action.finalized = true;
|
|
621
|
+
action.outcome = outcome;
|
|
622
|
+
if (action.events.length === 0) {
|
|
623
|
+
if (action.omittedEvents > 0 && omittedExecutionFamilyObservations < MAX_SAFE_COUNT) {
|
|
624
|
+
omittedExecutionFamilyObservations++;
|
|
625
|
+
}
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const sketch = {
|
|
629
|
+
actionKind: action.actionKind,
|
|
630
|
+
actionSiteId: action.actionSiteId,
|
|
631
|
+
actionMethod: action.actionMethod,
|
|
632
|
+
outcome,
|
|
633
|
+
terminalError: action.terminalError,
|
|
634
|
+
truncated: action.truncated,
|
|
635
|
+
omittedEvents: action.omittedEvents,
|
|
636
|
+
events: action.events,
|
|
637
|
+
};
|
|
638
|
+
const familyId = stableIdentity('execution-local-family', [canonicalJson(sketch)]);
|
|
639
|
+
const existing = safeMapGet(currentExecutions, familyId);
|
|
640
|
+
if (existing) {
|
|
641
|
+
existing.observations++;
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
const sketchBytes = action.eventBytes + safeBufferByteLength(safeJsonStringify(sketch), 'utf8');
|
|
645
|
+
if (safeMapSize(currentExecutions) >= MAX_EXECUTION_FAMILIES ||
|
|
646
|
+
currentExecutionBytes + sketchBytes > MAX_EXECUTION_BYTES) {
|
|
647
|
+
omittedExecutionFamilyObservations++;
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
safeMapSet(currentExecutions, familyId, { familyId, observations: 1, ...sketch });
|
|
651
|
+
currentExecutionBytes += sketchBytes;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
let processAction = startAction(
|
|
655
|
+
'process',
|
|
656
|
+
stableIdentity('action-site', [instrumentation.entrySourcePath || 'unknown-entry']),
|
|
657
|
+
'NONE',
|
|
658
|
+
);
|
|
659
|
+
safeActionEnterWith(actionStorage, processAction);
|
|
660
|
+
|
|
661
|
+
function continueProcessAction() {
|
|
662
|
+
if (processAction.suppressed) return;
|
|
663
|
+
const previous = processAction;
|
|
664
|
+
const active = safeActionGetStore(actionStorage);
|
|
665
|
+
processAction = {
|
|
666
|
+
suppressed: false,
|
|
667
|
+
actionKind: processAction.actionKind,
|
|
668
|
+
actionSiteId: processAction.actionSiteId,
|
|
669
|
+
actionMethod: processAction.actionMethod,
|
|
670
|
+
events: [],
|
|
671
|
+
eventBytes: 0,
|
|
672
|
+
truncated: false,
|
|
673
|
+
omittedEvents: 0,
|
|
674
|
+
finalized: false,
|
|
675
|
+
outcome: null,
|
|
676
|
+
terminalError: 'none',
|
|
677
|
+
};
|
|
678
|
+
if (!hasChildActions || !active || active === previous) {
|
|
679
|
+
safeActionEnterWith(actionStorage, processAction);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
try {
|
|
684
|
+
const requestStart = diagnosticsChannel.channel('http.server.request.start');
|
|
685
|
+
const responseFinish = diagnosticsChannel.channel('http.server.response.finish');
|
|
686
|
+
requestStart.subscribe((message) => {
|
|
687
|
+
try {
|
|
688
|
+
hasChildActions = true;
|
|
689
|
+
const action = startAction(
|
|
690
|
+
'http-server',
|
|
691
|
+
stableIdentity('action-site', ['node:http-server']),
|
|
692
|
+
httpMethod(message?.request?.method),
|
|
693
|
+
);
|
|
694
|
+
safeActionEnterWith(actionStorage, action);
|
|
695
|
+
if (message?.response && typeof message.response === 'object') {
|
|
696
|
+
safeWeakMapSet(httpActions, message.response, action);
|
|
697
|
+
}
|
|
698
|
+
} catch (error) {
|
|
699
|
+
debug('HTTP action start failed open', error);
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
responseFinish.subscribe((message) => {
|
|
703
|
+
try {
|
|
704
|
+
const action = message?.response && typeof message.response === 'object'
|
|
705
|
+
? safeWeakMapGet(httpActions, message.response)
|
|
706
|
+
: null;
|
|
707
|
+
const response = message?.response;
|
|
708
|
+
const statusDescriptor = response && typeof response === 'object' && !safeIsProxy(response)
|
|
709
|
+
? safeGetOwnPropertyDescriptor(response, 'statusCode')
|
|
710
|
+
: null;
|
|
711
|
+
const status = statusDescriptor && safeHasOwn(statusDescriptor, 'value')
|
|
712
|
+
? safeNumber(statusDescriptor.value)
|
|
713
|
+
: NaN;
|
|
714
|
+
const outcome = !safeNumberIsFinite(status)
|
|
715
|
+
? 'completed'
|
|
716
|
+
: status >= 500
|
|
717
|
+
? 'server-error'
|
|
718
|
+
: status >= 400
|
|
719
|
+
? 'client-error'
|
|
720
|
+
: 'success';
|
|
721
|
+
finalizeAction(action, outcome);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
debug('HTTP action finish failed open', error);
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
} catch (error) {
|
|
727
|
+
debug('HTTP action correlation unavailable', error);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function recordOmission(site, field) {
|
|
731
|
+
const current = safeMapGet(currentOmissions, site) || {
|
|
732
|
+
counterObservations: 0,
|
|
733
|
+
probeObservations: 0,
|
|
734
|
+
};
|
|
735
|
+
current[field]++;
|
|
736
|
+
safeMapSet(currentOmissions, site, current);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function disable(reason, error) {
|
|
740
|
+
enabled = false;
|
|
741
|
+
safeClearInterval(timer);
|
|
742
|
+
debug(`disabled after ${reason}`, error);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function sampleWeight(state) {
|
|
746
|
+
if (state.saturated) {
|
|
747
|
+
if (state.omitted < MAX_SAFE_COUNT) state.omitted++;
|
|
748
|
+
if (saturatedProbeOmissions < MAX_SAFE_COUNT) saturatedProbeOmissions++;
|
|
749
|
+
return -2;
|
|
750
|
+
}
|
|
751
|
+
const calls = ++state.calls;
|
|
752
|
+
if (calls > siteObservationLimit) {
|
|
753
|
+
state.saturated = true;
|
|
754
|
+
return -1;
|
|
755
|
+
}
|
|
756
|
+
return 1;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function increment(map, key, weight) {
|
|
760
|
+
const existing = safeMapGet(map, key);
|
|
761
|
+
if (existing === undefined && safeMapSize(map) >= MAX_COUNTER_KEYS) {
|
|
762
|
+
recordOmission(safeStringSlice(key, 0, safeStringIndexOf(key, '|')), 'counterObservations');
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
safeMapSet(map, key, (existing ?? 0) + weight);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function observeState(state, value, fields) {
|
|
769
|
+
if (!enabled) return value;
|
|
770
|
+
try {
|
|
771
|
+
const site = state.metadata.probeKey;
|
|
772
|
+
const action = actionForProbe();
|
|
773
|
+
if (action?.suppressed) return value;
|
|
774
|
+
const probeKind = state.metadata.probeKind;
|
|
775
|
+
const executionValue = action && (probeKind === 'parameter' || probeKind === 'return');
|
|
776
|
+
const weight = sampleWeight(state);
|
|
777
|
+
if (weight === -2) {
|
|
778
|
+
if (executionValue) {
|
|
779
|
+
const skippedEvents = value !== null && typeof value === 'object' && !safeArrayIsArray(value)
|
|
780
|
+
? 1 + safeMathMin(fields.length, 48)
|
|
781
|
+
: 1;
|
|
782
|
+
omitExecutionEvents(action, skippedEvents);
|
|
783
|
+
}
|
|
784
|
+
return value;
|
|
785
|
+
}
|
|
786
|
+
if (weight < 0) {
|
|
787
|
+
if (weight === -1) increment(currentShape, `${site}|__saturated__|info`, 1);
|
|
788
|
+
if (executionValue) {
|
|
789
|
+
const skippedEvents = value !== null && typeof value === 'object' && !safeArrayIsArray(value)
|
|
790
|
+
? 1 + safeMathMin(fields.length, 48)
|
|
791
|
+
: 1;
|
|
792
|
+
omitExecutionEvents(action, skippedEvents);
|
|
793
|
+
}
|
|
794
|
+
return value;
|
|
795
|
+
}
|
|
796
|
+
const fieldEventCount = safeArrayIsArray(fields) && value !== null &&
|
|
797
|
+
typeof value === 'object' && !safeArrayIsArray(value)
|
|
798
|
+
? safeMathMin(fields.length, 48)
|
|
799
|
+
: 0;
|
|
800
|
+
const correlated = executionValue && !action.truncated;
|
|
801
|
+
if (executionValue && action.truncated) omitExecutionEvents(action, 1 + fieldEventCount);
|
|
802
|
+
if (weight > 0) {
|
|
803
|
+
const selfOutcome = value === null ? 'null' : value === undefined ? 'undefined' : 'ok';
|
|
804
|
+
increment(currentShape, `${site}|__self__|${selfOutcome}`, weight);
|
|
805
|
+
increment(currentShape, `${site}|__self__|shape:${valueShape(value)}`, weight);
|
|
806
|
+
}
|
|
807
|
+
if (correlated && probeKind === 'parameter') {
|
|
808
|
+
addExecutionEvent(site, 'input', 'whole-value', `shape:${valueShape(value)}`);
|
|
809
|
+
} else if (correlated && probeKind === 'return') {
|
|
810
|
+
addExecutionEvent(site, 'output', 'whole-value', `shape:${valueShape(value)}`);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
if (safeArrayIsArray(fields) && value !== null && typeof value === 'object' && !safeArrayIsArray(value)) {
|
|
814
|
+
const proxyValue = safeIsProxy(value);
|
|
815
|
+
for (let fieldIndex = 0; fieldIndex < fields.length && fieldIndex < 48; fieldIndex++) {
|
|
816
|
+
const field = fields[fieldIndex];
|
|
817
|
+
// Manifest loading already validates the static field list. Avoid
|
|
818
|
+
// mutable RegExp/String prototype calls in this hot probe path.
|
|
819
|
+
if (typeof field !== 'string') continue;
|
|
820
|
+
const ordinal = fieldIndex + 1;
|
|
821
|
+
const fieldScope = `named-field-${ordinal < 10 ? `0${ordinal}` : ordinal}`;
|
|
822
|
+
if (proxyValue) {
|
|
823
|
+
if (weight > 0) increment(currentShape, `${site}|${field}|uninspectable`, weight);
|
|
824
|
+
if (correlated) {
|
|
825
|
+
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'uninspectable');
|
|
826
|
+
}
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
829
|
+
let descriptor;
|
|
830
|
+
try {
|
|
831
|
+
descriptor = safeGetOwnPropertyDescriptor(value, field);
|
|
832
|
+
} catch {
|
|
833
|
+
if (weight > 0) increment(currentShape, `${site}|${field}|uninspectable`, weight);
|
|
834
|
+
if (correlated) {
|
|
835
|
+
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'uninspectable');
|
|
836
|
+
}
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
if (!descriptor) {
|
|
840
|
+
if (weight > 0) increment(currentShape, `${site}|${field}|absent`, weight);
|
|
841
|
+
if (correlated) {
|
|
842
|
+
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'absent');
|
|
843
|
+
}
|
|
844
|
+
} else if (!safeHasOwn(descriptor, 'value')) {
|
|
845
|
+
if (weight > 0) increment(currentShape, `${site}|${field}|accessor`, weight);
|
|
846
|
+
if (correlated) {
|
|
847
|
+
addExecutionEvent(site, probeKind === 'parameter' ? 'input' : 'output', fieldScope, 'accessor');
|
|
848
|
+
}
|
|
849
|
+
} else {
|
|
850
|
+
const fieldValue = descriptor.value;
|
|
851
|
+
const outcome = fieldValue === null ? 'null' : fieldValue === undefined ? 'undefined' : 'ok';
|
|
852
|
+
if (weight > 0) {
|
|
853
|
+
increment(currentShape, `${site}|${field}|${outcome}`, weight);
|
|
854
|
+
increment(currentShape, `${site}|${field}|shape:${valueShape(fieldValue)}`, weight);
|
|
855
|
+
}
|
|
856
|
+
if (correlated) {
|
|
857
|
+
addExecutionEvent(
|
|
858
|
+
site,
|
|
859
|
+
probeKind === 'parameter' ? 'input' : 'output',
|
|
860
|
+
fieldScope,
|
|
861
|
+
`shape:${valueShape(fieldValue)}`,
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
} catch (error) {
|
|
868
|
+
// A hostile Proxy or exotic object must never affect the host app.
|
|
869
|
+
debug('value probe failed open', error);
|
|
870
|
+
}
|
|
871
|
+
return value;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function observe(site, value, fields) {
|
|
875
|
+
const state = safeMapGet(probeStates, site);
|
|
876
|
+
return state ? observeState(state, value, fields) : value;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function branchState(state, value) {
|
|
880
|
+
if (!enabled) return value;
|
|
881
|
+
try {
|
|
882
|
+
const site = state.metadata.probeKey;
|
|
883
|
+
const action = actionForProbe();
|
|
884
|
+
if (action?.suppressed) return value;
|
|
885
|
+
const weight = sampleWeight(state);
|
|
886
|
+
if (weight === -2) {
|
|
887
|
+
omitExecutionEvents(action);
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
if (weight < 0) {
|
|
891
|
+
if (weight === -1) increment(currentBranch, `${site}|S`, 1);
|
|
892
|
+
omitExecutionEvents(action);
|
|
893
|
+
}
|
|
894
|
+
else if (weight > 0) {
|
|
895
|
+
const outcome = value ? 'T' : 'F';
|
|
896
|
+
increment(currentBranch, `${site}|${outcome}`, weight);
|
|
897
|
+
if (action?.truncated) omitExecutionEvents(action);
|
|
898
|
+
else if (action) addExecutionEvent(site, 'branch', 'branch-result', outcome);
|
|
899
|
+
}
|
|
900
|
+
} catch (error) {
|
|
901
|
+
debug('branch probe failed open', error);
|
|
902
|
+
}
|
|
903
|
+
return value;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function branch(site, value) {
|
|
907
|
+
const state = safeMapGet(probeStates, site);
|
|
908
|
+
return state ? branchState(state, value) : value;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
function thrownState(state, value) {
|
|
912
|
+
if (!enabled) return value;
|
|
913
|
+
try {
|
|
914
|
+
const site = state.metadata.probeKey;
|
|
915
|
+
const action = actionForProbe();
|
|
916
|
+
if (action?.suppressed) return value;
|
|
917
|
+
const weight = sampleWeight(state);
|
|
918
|
+
if (weight === -2) {
|
|
919
|
+
omitExecutionEvents(action);
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
if (weight < 0) {
|
|
923
|
+
if (weight === -1) increment(currentShape, `${site}|__saturated__|info`, 1);
|
|
924
|
+
omitExecutionEvents(action);
|
|
925
|
+
} else if (action?.truncated) omitExecutionEvents(action);
|
|
926
|
+
else if (action) addExecutionEvent(site, 'error', 'thrown-value', `error:${errorClass(value)}`);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
debug('throw probe failed open', error);
|
|
929
|
+
}
|
|
930
|
+
return value;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function thrown(site, value) {
|
|
934
|
+
const state = safeMapGet(probeStates, site);
|
|
935
|
+
return state ? thrownState(state, value) : value;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function makePending(reason) {
|
|
939
|
+
if (pending || pendingQueue.length > 0 || (safeMapSize(currentShape) === 0 &&
|
|
940
|
+
safeMapSize(currentBranch) === 0 && safeMapSize(currentExecutions) === 0 &&
|
|
941
|
+
omittedExecutionFamilyObservations === 0 && sampledActions === 0 && unsampledActions === 0 &&
|
|
942
|
+
safeMapSize(currentOmissions) === 0 && saturatedProbeOmissions === 0)) return;
|
|
943
|
+
const shape = objectFromMap(currentShape);
|
|
944
|
+
const branchCounters = objectFromMap(currentBranch);
|
|
945
|
+
currentShape = new SafeMap();
|
|
946
|
+
currentBranch = new SafeMap();
|
|
947
|
+
const executions = safeArraySort(
|
|
948
|
+
mapValuesArray(currentExecutions),
|
|
949
|
+
(left, right) => compareStrings(left.familyId, right.familyId),
|
|
950
|
+
);
|
|
951
|
+
currentExecutions = new SafeMap();
|
|
952
|
+
currentExecutionBytes = 0;
|
|
953
|
+
const executionOmissions = {
|
|
954
|
+
sampledActions,
|
|
955
|
+
unsampledActions,
|
|
956
|
+
omittedFamilyObservations: omittedExecutionFamilyObservations,
|
|
957
|
+
};
|
|
958
|
+
sampledActions = 0;
|
|
959
|
+
unsampledActions = 0;
|
|
960
|
+
omittedExecutionFamilyObservations = 0;
|
|
961
|
+
if (saturatedProbeOmissions > 0) {
|
|
962
|
+
const countEntries = mapEntriesArray(probeStates);
|
|
963
|
+
for (let index = 0; index < countEntries.length; index++) {
|
|
964
|
+
const site = countEntries[index][0];
|
|
965
|
+
const state = countEntries[index][1];
|
|
966
|
+
if (!state || state.omitted <= 0) continue;
|
|
967
|
+
const current = safeMapGet(currentOmissions, site) || {
|
|
968
|
+
counterObservations: 0,
|
|
969
|
+
probeObservations: 0,
|
|
970
|
+
};
|
|
971
|
+
current.probeObservations = safeMathMin(
|
|
972
|
+
MAX_SAFE_COUNT,
|
|
973
|
+
current.probeObservations + state.omitted,
|
|
974
|
+
);
|
|
975
|
+
safeMapSet(currentOmissions, site, current);
|
|
976
|
+
state.omitted = 0;
|
|
977
|
+
}
|
|
978
|
+
saturatedProbeOmissions = 0;
|
|
979
|
+
}
|
|
980
|
+
const omittedEntries = safeArraySort(
|
|
981
|
+
mapEntriesArray(currentOmissions),
|
|
982
|
+
(leftEntry, rightEntry) => compareStrings(leftEntry[0], rightEntry[0]),
|
|
983
|
+
);
|
|
984
|
+
const omittedSites = safeArrayMap(omittedEntries, entry => ({
|
|
985
|
+
probeKey: entry[0],
|
|
986
|
+
counterObservations: entry[1].counterObservations,
|
|
987
|
+
probeObservations: entry[1].probeObservations,
|
|
988
|
+
}));
|
|
989
|
+
currentOmissions = new SafeMap();
|
|
990
|
+
const counterGroups = new SafeMap();
|
|
991
|
+
const groupFor = (probeKey) => {
|
|
992
|
+
let group = safeMapGet(counterGroups, probeKey);
|
|
993
|
+
if (!group) {
|
|
994
|
+
group = { probeKey, shape: [], branch: [], omission: null };
|
|
995
|
+
safeMapSet(counterGroups, probeKey, group);
|
|
996
|
+
}
|
|
997
|
+
return group;
|
|
998
|
+
};
|
|
999
|
+
const shapeEntries = safeObjectEntries(shape);
|
|
1000
|
+
for (let index = 0; index < shapeEntries.length; index++) {
|
|
1001
|
+
const entry = shapeEntries[index];
|
|
1002
|
+
safeArrayPush(groupFor(safeStringSlice(entry[0], 0, safeStringIndexOf(entry[0], '|'))).shape, entry);
|
|
1003
|
+
}
|
|
1004
|
+
const branchEntries = safeObjectEntries(branchCounters);
|
|
1005
|
+
for (let index = 0; index < branchEntries.length; index++) {
|
|
1006
|
+
const entry = branchEntries[index];
|
|
1007
|
+
safeArrayPush(groupFor(safeStringSlice(entry[0], 0, safeStringIndexOf(entry[0], '|'))).branch, entry);
|
|
1008
|
+
}
|
|
1009
|
+
for (let index = 0; index < omittedSites.length; index++) {
|
|
1010
|
+
const omission = omittedSites[index];
|
|
1011
|
+
groupFor(omission.probeKey).omission = omission;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const packUnits = (units) => {
|
|
1015
|
+
const chunks = [];
|
|
1016
|
+
let current = [];
|
|
1017
|
+
let bytes = 0;
|
|
1018
|
+
for (let unitIndex = 0; unitIndex < units.length; unitIndex++) {
|
|
1019
|
+
const unit = units[unitIndex];
|
|
1020
|
+
let unitBytes = safeBufferByteLength(safeJsonStringify(unit), 'utf8') + 2_048;
|
|
1021
|
+
if (unit.type === 'counter') {
|
|
1022
|
+
unitBytes += safeBufferByteLength(safeJsonStringify(safeMapGet(sitesByProbe, unit.group.probeKey)), 'utf8');
|
|
1023
|
+
} else if (unit.type === 'execution') {
|
|
1024
|
+
unitBytes += safeArrayReduce(unit.unit.eventSites, (total, probeKey) =>
|
|
1025
|
+
total + safeBufferByteLength(safeJsonStringify(safeMapGet(sitesByProbe, probeKey)), 'utf8'), 0);
|
|
1026
|
+
}
|
|
1027
|
+
if (current.length > 0 && bytes + unitBytes > PAYLOAD_CHUNK_BUDGET) {
|
|
1028
|
+
safeArrayPush(chunks, current);
|
|
1029
|
+
current = [];
|
|
1030
|
+
bytes = 0;
|
|
1031
|
+
}
|
|
1032
|
+
safeArrayPush(current, unit);
|
|
1033
|
+
bytes += unitBytes;
|
|
1034
|
+
}
|
|
1035
|
+
if (current.length > 0) safeArrayPush(chunks, current);
|
|
1036
|
+
return chunks;
|
|
1037
|
+
};
|
|
1038
|
+
|
|
1039
|
+
const executionUnits = safeArrayMap(executions, execution => {
|
|
1040
|
+
const eventSiteSet = new SafeSet();
|
|
1041
|
+
for (let eventIndex = 0; eventIndex < execution.events.length; eventIndex++) {
|
|
1042
|
+
const event = execution.events[eventIndex];
|
|
1043
|
+
const siteMetadata = safeMapGet(sitesById, event.siteId);
|
|
1044
|
+
if (siteMetadata?.probeKey) safeSetAdd(eventSiteSet, siteMetadata.probeKey);
|
|
1045
|
+
}
|
|
1046
|
+
const eventSites = safeArraySort(setValuesArray(eventSiteSet), compareStrings);
|
|
1047
|
+
return { execution, eventSites };
|
|
1048
|
+
});
|
|
1049
|
+
const counterUnits = safeArrayMap(
|
|
1050
|
+
safeArraySort(mapValuesArray(counterGroups),
|
|
1051
|
+
(left, right) => compareStrings(left.probeKey, right.probeKey)),
|
|
1052
|
+
group => ({ type: 'counter', group }),
|
|
1053
|
+
);
|
|
1054
|
+
const projectedExecutionUnits = safeArrayMap(executionUnits, unit => ({ type: 'execution', unit }));
|
|
1055
|
+
const unitsToPack = [];
|
|
1056
|
+
for (let index = 0; index < counterUnits.length; index++) safeArrayPush(unitsToPack, counterUnits[index]);
|
|
1057
|
+
for (let index = 0; index < projectedExecutionUnits.length; index++) {
|
|
1058
|
+
safeArrayPush(unitsToPack, projectedExecutionUnits[index]);
|
|
1059
|
+
}
|
|
1060
|
+
const packed = packUnits(unitsToPack);
|
|
1061
|
+
const chunkInputs = safeArrayMap(packed, units => ({
|
|
1062
|
+
groups: safeArrayMap(safeArrayFilter(units, unit => unit.type === 'counter'), unit => unit.group),
|
|
1063
|
+
executionUnits: safeArrayMap(safeArrayFilter(units, unit => unit.type === 'execution'), unit => unit.unit),
|
|
1064
|
+
}));
|
|
1065
|
+
if (chunkInputs.length === 0) safeArrayPush(chunkInputs, { groups: [], executionUnits: [] });
|
|
1066
|
+
const observedAt = safeDateToISOString(new SafeDate());
|
|
1067
|
+
const serialized = safeArrayMap(chunkInputs, (chunk, index) => {
|
|
1068
|
+
const chunkShape = objectFromEntryArray(safeArrayFlatMap(chunk.groups, group => group.shape));
|
|
1069
|
+
const chunkBranch = objectFromEntryArray(safeArrayFlatMap(chunk.groups, group => group.branch));
|
|
1070
|
+
const chunkOmittedSites = safeArrayFilter(
|
|
1071
|
+
safeArrayMap(chunk.groups, group => group.omission),
|
|
1072
|
+
item => item !== null && item !== undefined,
|
|
1073
|
+
);
|
|
1074
|
+
const chunkOmissions = safeArrayReduce(chunkOmittedSites, (totals, item) => ({
|
|
1075
|
+
counterObservations: totals.counterObservations + item.counterObservations,
|
|
1076
|
+
probeObservations: totals.probeObservations + item.probeObservations,
|
|
1077
|
+
}), { counterObservations: 0, probeObservations: 0 });
|
|
1078
|
+
const chunkExecutions = safeArrayMap(chunk.executionUnits, unit => unit.execution);
|
|
1079
|
+
const usedProbeKeys = new SafeSet();
|
|
1080
|
+
for (let groupIndex = 0; groupIndex < chunk.groups.length; groupIndex++) {
|
|
1081
|
+
safeSetAdd(usedProbeKeys, chunk.groups[groupIndex].probeKey);
|
|
1082
|
+
}
|
|
1083
|
+
for (let unitIndex = 0; unitIndex < chunk.executionUnits.length; unitIndex++) {
|
|
1084
|
+
const unit = chunk.executionUnits[unitIndex];
|
|
1085
|
+
for (let siteIndex = 0; siteIndex < unit.eventSites.length; siteIndex++) {
|
|
1086
|
+
safeSetAdd(usedProbeKeys, unit.eventSites[siteIndex]);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
const payload = {
|
|
1090
|
+
v: 1,
|
|
1091
|
+
samplerVersion: SAMPLER_VERSION,
|
|
1092
|
+
buildId,
|
|
1093
|
+
runId,
|
|
1094
|
+
seq: sequence + index,
|
|
1095
|
+
ts: observedAt,
|
|
1096
|
+
reason: safeStringSlice(safeString(reason), 0, 256),
|
|
1097
|
+
omissions: chunkOmissions,
|
|
1098
|
+
omittedSites: chunkOmittedSites,
|
|
1099
|
+
executionOmissions: index === 0 ? executionOmissions : {
|
|
1100
|
+
sampledActions: 0,
|
|
1101
|
+
unsampledActions: 0,
|
|
1102
|
+
omittedFamilyObservations: 0,
|
|
1103
|
+
},
|
|
1104
|
+
sites: safeArrayMap(safeArraySort(setValuesArray(usedProbeKeys), compareStrings),
|
|
1105
|
+
key => safeMapGet(sitesByProbe, key)),
|
|
1106
|
+
shape: chunkShape,
|
|
1107
|
+
branch: chunkBranch,
|
|
1108
|
+
executions: chunkExecutions,
|
|
1109
|
+
coverage: [],
|
|
1110
|
+
};
|
|
1111
|
+
const body = safeJsonStringify(payload);
|
|
1112
|
+
if (safeBufferByteLength(body, 'utf8') > MAX_PAYLOAD_BYTES) {
|
|
1113
|
+
throw new SafeError('bounded telemetry unit exceeded the upload byte limit');
|
|
1114
|
+
}
|
|
1115
|
+
return { payload, body };
|
|
1116
|
+
});
|
|
1117
|
+
pending = safeArrayShift(serialized) ?? null;
|
|
1118
|
+
pendingQueue = serialized;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function doFlush(reason) {
|
|
1122
|
+
if (!enabled) return false;
|
|
1123
|
+
try {
|
|
1124
|
+
makePending(reason);
|
|
1125
|
+
} catch (error) {
|
|
1126
|
+
disable('payload construction failure', error);
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
if (!pending) return true;
|
|
1130
|
+
try {
|
|
1131
|
+
let uploaded = false;
|
|
1132
|
+
while (pending) {
|
|
1133
|
+
const response = await safeFetch(endpoint, {
|
|
1134
|
+
method: 'POST',
|
|
1135
|
+
headers: {
|
|
1136
|
+
Authorization: `Bearer ${token}`,
|
|
1137
|
+
'Content-Type': 'application/json',
|
|
1138
|
+
},
|
|
1139
|
+
body: pending.body,
|
|
1140
|
+
signal: safeAbortTimeout(10_000),
|
|
1141
|
+
});
|
|
1142
|
+
if (!responseAccepted(response)) throw new SafeError(`ingestion returned HTTP ${responseStatus(response)}`);
|
|
1143
|
+
pending = null;
|
|
1144
|
+
sequence++;
|
|
1145
|
+
uploaded = true;
|
|
1146
|
+
pending = safeArrayShift(pendingQueue) ?? null;
|
|
1147
|
+
}
|
|
1148
|
+
consecutiveFailures = 0;
|
|
1149
|
+
return true;
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
consecutiveFailures++;
|
|
1152
|
+
debug(`flush failed; exact payload retained (${consecutiveFailures}/${MAX_CONSECUTIVE_FAILURES})`, error);
|
|
1153
|
+
if (consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) disable('repeated flush failures', error);
|
|
1154
|
+
return false;
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const spoolDirectory = join(
|
|
1159
|
+
tmpdir(),
|
|
1160
|
+
`haystack-telemetry-${safeStringSlice(stableIdentity('owner', [endpoint, token, buildId]), 'owner-'.length)}`,
|
|
1161
|
+
);
|
|
1162
|
+
|
|
1163
|
+
function spoolBody(body) {
|
|
1164
|
+
try {
|
|
1165
|
+
const bodyBytes = safeBufferByteLength(body, 'utf8');
|
|
1166
|
+
if (bodyBytes > MAX_PAYLOAD_BYTES) {
|
|
1167
|
+
debug('oversized crash payload was not spooled');
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
1170
|
+
mkdirSync(spoolDirectory, { recursive: true, mode: 0o700 });
|
|
1171
|
+
const digest = sha256Hex(body);
|
|
1172
|
+
const finalPath = join(spoolDirectory, `pending-${digest}.json`);
|
|
1173
|
+
const pendingNames = safeArrayFilter(readdirSync(spoolDirectory),
|
|
1174
|
+
name => safeRegExpTest(/^pending-[0-9a-f]{64}\.json$/, name));
|
|
1175
|
+
const described = safeArrayMap(pendingNames, name => {
|
|
1176
|
+
const path = join(spoolDirectory, name);
|
|
1177
|
+
try {
|
|
1178
|
+
const stats = statSync(path);
|
|
1179
|
+
return { name, path, bytes: stats.size, modified: stats.mtimeMs };
|
|
1180
|
+
} catch {
|
|
1181
|
+
return null;
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
const existing = safeArraySort(
|
|
1185
|
+
safeArrayFilter(described, item => item !== null && item !== undefined),
|
|
1186
|
+
(left, right) => left.modified - right.modified || compareStrings(left.name, right.name),
|
|
1187
|
+
);
|
|
1188
|
+
let retainedBytes = safeArrayReduce(existing, (total, file) => total + file.bytes, 0);
|
|
1189
|
+
const receiptPath = join(spoolDirectory, 'dropped.json');
|
|
1190
|
+
let dropped = { chunks: 0, bytes: 0, payload: null };
|
|
1191
|
+
try {
|
|
1192
|
+
const parsed = safeJsonParse(readFileSync(receiptPath, 'utf8'));
|
|
1193
|
+
if (safeNumberIsSafeInteger(parsed.chunks) && parsed.chunks >= 0 &&
|
|
1194
|
+
safeNumberIsSafeInteger(parsed.bytes) && parsed.bytes >= 0 &&
|
|
1195
|
+
(parsed.payload === null || typeof parsed.payload === 'object')) dropped = parsed;
|
|
1196
|
+
} catch {
|
|
1197
|
+
// No prior eviction receipt is the expected case.
|
|
1198
|
+
}
|
|
1199
|
+
while (existing.length >= MAX_SPOOL_FILES || retainedBytes + bodyBytes > MAX_SPOOL_BYTES) {
|
|
1200
|
+
const oldest = safeArrayShift(existing);
|
|
1201
|
+
if (!oldest) break;
|
|
1202
|
+
try {
|
|
1203
|
+
unlinkSync(oldest.path);
|
|
1204
|
+
retainedBytes -= oldest.bytes;
|
|
1205
|
+
dropped.chunks++;
|
|
1206
|
+
dropped.bytes += oldest.bytes;
|
|
1207
|
+
dropped.payload = null;
|
|
1208
|
+
} catch {
|
|
1209
|
+
break;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
if (retainedBytes + bodyBytes > MAX_SPOOL_BYTES) return;
|
|
1213
|
+
if (dropped.chunks > 0) {
|
|
1214
|
+
writeFileSync(receiptPath, safeJsonStringify(dropped), { encoding: 'utf8', mode: 0o600 });
|
|
1215
|
+
}
|
|
1216
|
+
const temporaryPath = join(spoolDirectory, `.pending-${process.pid}-${digest}.tmp`);
|
|
1217
|
+
writeFileSync(temporaryPath, body, { encoding: 'utf8', mode: 0o600 });
|
|
1218
|
+
renameSync(temporaryPath, finalPath);
|
|
1219
|
+
} catch (error) {
|
|
1220
|
+
debug('crash payload could not be spooled', error);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function spoolCurrent(reason) {
|
|
1225
|
+
try {
|
|
1226
|
+
if (pending) {
|
|
1227
|
+
spoolBody(pending.body);
|
|
1228
|
+
sequence = safeMathMax(sequence, pending.payload.seq + 1);
|
|
1229
|
+
pending = null;
|
|
1230
|
+
}
|
|
1231
|
+
for (let index = 0; index < pendingQueue.length; index++) {
|
|
1232
|
+
const queued = pendingQueue[index];
|
|
1233
|
+
spoolBody(queued.body);
|
|
1234
|
+
sequence = safeMathMax(sequence, queued.payload.seq + 1);
|
|
1235
|
+
}
|
|
1236
|
+
pendingQueue = [];
|
|
1237
|
+
makePending(reason);
|
|
1238
|
+
if (pending) {
|
|
1239
|
+
spoolBody(pending.body);
|
|
1240
|
+
for (let index = 0; index < pendingQueue.length; index++) spoolBody(pendingQueue[index].body);
|
|
1241
|
+
}
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
debug('crash snapshot failed open', error);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
async function replaySpooledPayloads() {
|
|
1248
|
+
let files;
|
|
1249
|
+
try {
|
|
1250
|
+
files = safeArraySlice(
|
|
1251
|
+
safeArraySort(
|
|
1252
|
+
safeArrayFilter(readdirSync(spoolDirectory),
|
|
1253
|
+
name => safeRegExpTest(/^pending-[0-9a-f]{64}\.json$/, name)),
|
|
1254
|
+
compareStrings,
|
|
1255
|
+
),
|
|
1256
|
+
0,
|
|
1257
|
+
MAX_SPOOL_FILES,
|
|
1258
|
+
);
|
|
1259
|
+
} catch {
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
const receiptPath = join(spoolDirectory, 'dropped.json');
|
|
1263
|
+
try {
|
|
1264
|
+
const dropped = safeJsonParse(readFileSync(receiptPath, 'utf8'));
|
|
1265
|
+
if (safeNumberIsSafeInteger(dropped.chunks) && dropped.chunks > 0 &&
|
|
1266
|
+
safeNumberIsSafeInteger(dropped.bytes) && dropped.bytes >= 0) {
|
|
1267
|
+
const payload = dropped.payload && typeof dropped.payload === 'object'
|
|
1268
|
+
? dropped.payload
|
|
1269
|
+
: {
|
|
1270
|
+
v: 1,
|
|
1271
|
+
samplerVersion: SAMPLER_VERSION,
|
|
1272
|
+
buildId,
|
|
1273
|
+
runId,
|
|
1274
|
+
seq: sequence,
|
|
1275
|
+
ts: safeDateToISOString(new SafeDate()),
|
|
1276
|
+
reason: 'crash-spool-omission',
|
|
1277
|
+
omissions: { counterObservations: 0, probeObservations: 0 },
|
|
1278
|
+
omittedSites: [],
|
|
1279
|
+
executionOmissions: {
|
|
1280
|
+
sampledActions: 0,
|
|
1281
|
+
unsampledActions: 0,
|
|
1282
|
+
omittedFamilyObservations: 0,
|
|
1283
|
+
},
|
|
1284
|
+
deliveryOmissions: {
|
|
1285
|
+
droppedCrashChunks: dropped.chunks,
|
|
1286
|
+
droppedCrashBytes: dropped.bytes,
|
|
1287
|
+
},
|
|
1288
|
+
sites: [],
|
|
1289
|
+
shape: {},
|
|
1290
|
+
branch: {},
|
|
1291
|
+
executions: [],
|
|
1292
|
+
coverage: [],
|
|
1293
|
+
};
|
|
1294
|
+
// Reserve this identity before any asynchronous request. Normal and
|
|
1295
|
+
// beforeExit flushes may run concurrently and must never reuse it.
|
|
1296
|
+
if (payload.runId === runId && safeNumberIsSafeInteger(payload.seq)) {
|
|
1297
|
+
sequence = safeMathMax(sequence, payload.seq + 1);
|
|
1298
|
+
}
|
|
1299
|
+
if (!dropped.payload) {
|
|
1300
|
+
const persisted = { chunks: dropped.chunks, bytes: dropped.bytes, payload };
|
|
1301
|
+
const temporaryPath = join(spoolDirectory, `.dropped-${process.pid}.tmp`);
|
|
1302
|
+
writeFileSync(temporaryPath, safeJsonStringify(persisted), { encoding: 'utf8', mode: 0o600 });
|
|
1303
|
+
renameSync(temporaryPath, receiptPath);
|
|
1304
|
+
}
|
|
1305
|
+
const response = await safeFetch(endpoint, {
|
|
1306
|
+
method: 'POST',
|
|
1307
|
+
headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
|
|
1308
|
+
body: safeJsonStringify(payload),
|
|
1309
|
+
signal: safeAbortTimeout(10_000),
|
|
1310
|
+
});
|
|
1311
|
+
if (!responseAccepted(response)) return;
|
|
1312
|
+
// The receiver accepted this exact omission count. A retry would add
|
|
1313
|
+
// the same loss again under a new process run, so acknowledge it now.
|
|
1314
|
+
unlinkSync(receiptPath);
|
|
1315
|
+
}
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
if (error?.code !== 'ENOENT') debug('crash spool omission replay failed', error);
|
|
1318
|
+
}
|
|
1319
|
+
for (let index = 0; index < files.length; index++) {
|
|
1320
|
+
const name = files[index];
|
|
1321
|
+
const filePath = join(spoolDirectory, name);
|
|
1322
|
+
try {
|
|
1323
|
+
const body = readFileSync(filePath, 'utf8');
|
|
1324
|
+
if (safeBufferByteLength(body, 'utf8') > MAX_PAYLOAD_BYTES) {
|
|
1325
|
+
unlinkSync(filePath);
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
const response = await safeFetch(endpoint, {
|
|
1329
|
+
method: 'POST',
|
|
1330
|
+
headers: {
|
|
1331
|
+
Authorization: `Bearer ${token}`,
|
|
1332
|
+
'Content-Type': 'application/json',
|
|
1333
|
+
},
|
|
1334
|
+
body,
|
|
1335
|
+
signal: safeAbortTimeout(10_000),
|
|
1336
|
+
});
|
|
1337
|
+
if (!responseAccepted(response)) break;
|
|
1338
|
+
unlinkSync(filePath);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
debug('spooled crash payload replay failed', error);
|
|
1341
|
+
break;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function flush(reason = 'manual') {
|
|
1347
|
+
if (flushInFlight) return flushInFlight;
|
|
1348
|
+
if (!processAction.suppressed && reason !== 'interval' && reason !== 'http-finish') {
|
|
1349
|
+
finalizeAction(processAction, processAction.outcome || (process.exitCode ? 'error' : 'success'));
|
|
1350
|
+
if (reason !== 'before-exit' && reason !== 'process-exit' &&
|
|
1351
|
+
reason !== 'uncaught-exception' && reason !== 'unhandled-rejection') {
|
|
1352
|
+
continueProcessAction();
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
flushInFlight = safePromiseFinally(doFlush(reason), () => {
|
|
1356
|
+
flushInFlight = null;
|
|
1357
|
+
});
|
|
1358
|
+
return flushInFlight;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const timer = setInterval(() => { void flush('interval'); }, flushIntervalMs);
|
|
1362
|
+
timer.unref();
|
|
1363
|
+
void replaySpooledPayloads();
|
|
1364
|
+
process.on('uncaughtExceptionMonitor', (error) => {
|
|
1365
|
+
const action = actionForProbe() || processAction;
|
|
1366
|
+
if (action && !action.suppressed && !action.finalized) {
|
|
1367
|
+
action.outcome = 'error';
|
|
1368
|
+
action.terminalError = `error:${errorClass(error)}`;
|
|
1369
|
+
finalizeAction(action, 'error');
|
|
1370
|
+
}
|
|
1371
|
+
spoolCurrent('uncaught-exception');
|
|
1372
|
+
});
|
|
1373
|
+
process.once('exit', (code) => {
|
|
1374
|
+
if (!processAction.suppressed && !processAction.finalized) {
|
|
1375
|
+
processAction.terminalError = code ? 'error:generic' : processAction.terminalError;
|
|
1376
|
+
finalizeAction(processAction, code ? 'error' : processAction.outcome || 'success');
|
|
1377
|
+
}
|
|
1378
|
+
if (pending || pendingQueue.length || safeMapSize(currentExecutions) ||
|
|
1379
|
+
safeMapSize(currentShape) || safeMapSize(currentBranch)) {
|
|
1380
|
+
spoolCurrent('process-exit');
|
|
1381
|
+
}
|
|
1382
|
+
});
|
|
1383
|
+
process.once('beforeExit', () => {
|
|
1384
|
+
if (beforeExitStarted) return;
|
|
1385
|
+
beforeExitStarted = true;
|
|
1386
|
+
if (!processAction.suppressed) {
|
|
1387
|
+
finalizeAction(processAction, processAction.outcome || (process.exitCode ? 'error' : 'success'));
|
|
1388
|
+
}
|
|
1389
|
+
void flush('before-exit');
|
|
1390
|
+
});
|
|
1391
|
+
|
|
1392
|
+
const control = Object.freeze({
|
|
1393
|
+
enabled: true,
|
|
1394
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
1395
|
+
buildId,
|
|
1396
|
+
runId,
|
|
1397
|
+
flush,
|
|
1398
|
+
selectProbe(name) {
|
|
1399
|
+
if (!enabled || processAction.suppressed) return passThrough;
|
|
1400
|
+
if (name === '__hstValue') return observe;
|
|
1401
|
+
if (name === '__hstBranch') return branch;
|
|
1402
|
+
if (name === '__hstThrow') return thrown;
|
|
1403
|
+
return null;
|
|
1404
|
+
},
|
|
1405
|
+
selectSiteProbe(name, site) {
|
|
1406
|
+
if (!enabled || processAction.suppressed) return null;
|
|
1407
|
+
const state = safeMapGet(probeStates, site);
|
|
1408
|
+
if (!state) return null;
|
|
1409
|
+
if (name === '__hstValue') {
|
|
1410
|
+
const fields = state.metadata.allowedFields;
|
|
1411
|
+
return (value) => {
|
|
1412
|
+
if (!enabled) return value;
|
|
1413
|
+
if (state.saturated) {
|
|
1414
|
+
const action = actionForProbe();
|
|
1415
|
+
if (action?.suppressed) return value;
|
|
1416
|
+
const executionValue = state.metadata.probeKind === 'parameter' ||
|
|
1417
|
+
state.metadata.probeKind === 'return';
|
|
1418
|
+
if (state.omitted < MAX_SAFE_COUNT) state.omitted++;
|
|
1419
|
+
if (saturatedProbeOmissions < MAX_SAFE_COUNT) saturatedProbeOmissions++;
|
|
1420
|
+
if (executionValue) {
|
|
1421
|
+
const skippedEvents = value !== null && typeof value === 'object' && !safeArrayIsArray(value)
|
|
1422
|
+
? 1 + safeMathMin(fields.length, 48)
|
|
1423
|
+
: 1;
|
|
1424
|
+
omitExecutionEvents(action, skippedEvents);
|
|
1425
|
+
}
|
|
1426
|
+
return value;
|
|
1427
|
+
}
|
|
1428
|
+
return observeState(state, value, fields);
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
if (name === '__hstBranch') return (value) => {
|
|
1432
|
+
if (!enabled) return value;
|
|
1433
|
+
if (state.saturated) {
|
|
1434
|
+
const action = actionForProbe();
|
|
1435
|
+
if (action?.suppressed) return value;
|
|
1436
|
+
if (state.omitted < MAX_SAFE_COUNT) state.omitted++;
|
|
1437
|
+
if (saturatedProbeOmissions < MAX_SAFE_COUNT) saturatedProbeOmissions++;
|
|
1438
|
+
omitExecutionEvents(action);
|
|
1439
|
+
return value;
|
|
1440
|
+
}
|
|
1441
|
+
return branchState(state, value);
|
|
1442
|
+
};
|
|
1443
|
+
if (name === '__hstThrow') return (value) => {
|
|
1444
|
+
if (!enabled) return value;
|
|
1445
|
+
if (state.saturated) {
|
|
1446
|
+
const action = actionForProbe();
|
|
1447
|
+
if (action?.suppressed) return value;
|
|
1448
|
+
if (state.omitted < MAX_SAFE_COUNT) state.omitted++;
|
|
1449
|
+
if (saturatedProbeOmissions < MAX_SAFE_COUNT) saturatedProbeOmissions++;
|
|
1450
|
+
omitExecutionEvents(action);
|
|
1451
|
+
return value;
|
|
1452
|
+
}
|
|
1453
|
+
return thrownState(state, value);
|
|
1454
|
+
};
|
|
1455
|
+
return null;
|
|
1456
|
+
},
|
|
1457
|
+
status() {
|
|
1458
|
+
return {
|
|
1459
|
+
enabled,
|
|
1460
|
+
sequence,
|
|
1461
|
+
pending: pending !== null || pendingQueue.length > 0,
|
|
1462
|
+
shapeKeys: safeMapSize(currentShape),
|
|
1463
|
+
branchKeys: safeMapSize(currentBranch),
|
|
1464
|
+
executionFamilies: safeMapSize(currentExecutions),
|
|
1465
|
+
};
|
|
1466
|
+
},
|
|
1467
|
+
});
|
|
1468
|
+
activeControl = control;
|
|
1469
|
+
Object.defineProperty(processHost, CONTROL_CHANNEL, {
|
|
1470
|
+
configurable: false,
|
|
1471
|
+
enumerable: false,
|
|
1472
|
+
writable: false,
|
|
1473
|
+
value: control,
|
|
1474
|
+
});
|
|
1475
|
+
if (!(CONTROL_GLOBAL in globalThis)) {
|
|
1476
|
+
Object.defineProperty(globalThis, CONTROL_GLOBAL, {
|
|
1477
|
+
configurable: false,
|
|
1478
|
+
enumerable: false,
|
|
1479
|
+
writable: false,
|
|
1480
|
+
value: control,
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
debug(`enabled for build ${buildId}`);
|
|
1484
|
+
return control;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function install(options = {}) {
|
|
1488
|
+
try {
|
|
1489
|
+
return installRuntime(options);
|
|
1490
|
+
} catch (error) {
|
|
1491
|
+
// Bootstrap is injected into the application entry point, so this final
|
|
1492
|
+
// boundary must catch every setup failure. Telemetry is never allowed to
|
|
1493
|
+
// prevent the host application from starting.
|
|
1494
|
+
debug('installation failed open', error);
|
|
1495
|
+
return { enabled: false, reason: 'installation-failed' };
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
const publicRuntime = Object.freeze({ install, selectProbe, safeSelectProbe, safeSelectSiteProbe });
|
|
1500
|
+
// Keep the exported capability immutable. The generated CommonJS entry locks
|
|
1501
|
+
// its cache slot after genuine evaluation; the ESM wrapper evaluates this
|
|
1502
|
+
// source outside CommonJS's public cache.
|
|
1503
|
+
Object.defineProperty(module, 'exports', {
|
|
1504
|
+
configurable: false,
|
|
1505
|
+
enumerable: true,
|
|
1506
|
+
writable: false,
|
|
1507
|
+
value: publicRuntime,
|
|
1508
|
+
});
|
|
1509
|
+
install();
|