@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,832 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import fg from 'fast-glob';
|
|
8
|
+
// @babel/core intentionally has no bundled declarations. Keep this small
|
|
9
|
+
// build-transform boundary dynamic instead of leaking `any` into CLI contracts.
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const babel = require('@babel/core');
|
|
12
|
+
const INSTRUMENTED_MARKER = '__haystack_telemetry_instrumented_v1__';
|
|
13
|
+
const INSTRUMENTED_MARKER_COMMENT = `/* ${INSTRUMENTED_MARKER} */`;
|
|
14
|
+
const BOOTSTRAP_MARKER = '__haystack_telemetry_bootstrap_v1__';
|
|
15
|
+
const BOOTSTRAP_MARKER_COMMENT = `/* ${BOOTSTRAP_MARKER} */`;
|
|
16
|
+
const RUNTIME_DIRECTORY = '.haystack-telemetry';
|
|
17
|
+
const RUNTIME_FILENAME = 'runtime.cjs';
|
|
18
|
+
const RUNTIME_ESM_FILENAME = 'runtime.mjs';
|
|
19
|
+
const MANIFEST_FILENAME = 'instrumentation.json';
|
|
20
|
+
const RUNTIME_CONTROL_CHANNEL_PLACEHOLDER = '__HAYSTACK_TELEMETRY_CONTROL_CHANNEL__';
|
|
21
|
+
const RUNTIME_INTEGRITY_PLACEHOLDER = '__HAYSTACK_TELEMETRY_RUNTIME_INTEGRITY__';
|
|
22
|
+
const MANIFEST_SCHEMA_VERSION = 'haystack-node-telemetry-instrumentation-v3';
|
|
23
|
+
const MAX_INSTRUMENTED_SITES = 50_000;
|
|
24
|
+
const SOURCE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.mts', '.mjs', '.cts', '.cjs'];
|
|
25
|
+
const SAFE_SOURCE_PATH = /^[^#|\r\n]+$/;
|
|
26
|
+
const SAFE_IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]{0,127}$/;
|
|
27
|
+
function generatedBody(code) {
|
|
28
|
+
const body = code.startsWith('#!')
|
|
29
|
+
? code.slice(code.indexOf('\n') + 1)
|
|
30
|
+
: code;
|
|
31
|
+
return body;
|
|
32
|
+
}
|
|
33
|
+
function hasGeneratedInstrumentation(code) {
|
|
34
|
+
const body = generatedBody(code);
|
|
35
|
+
return body.startsWith(INSTRUMENTED_MARKER_COMMENT) ||
|
|
36
|
+
body.startsWith(BOOTSTRAP_MARKER_COMMENT);
|
|
37
|
+
}
|
|
38
|
+
function hasGeneratedEntryBootstrap(code) {
|
|
39
|
+
// The bootstrap is inserted after directive prologues so a customer's
|
|
40
|
+
// `"use strict"` remains effective. It therefore need not be byte zero.
|
|
41
|
+
return generatedBody(code).includes(BOOTSTRAP_MARKER_COMMENT);
|
|
42
|
+
}
|
|
43
|
+
function normalizeLineEndings(value) {
|
|
44
|
+
return value.replace(/\r\n/g, '\n');
|
|
45
|
+
}
|
|
46
|
+
function normalizedPath(path) {
|
|
47
|
+
return path.split(sep).join('/');
|
|
48
|
+
}
|
|
49
|
+
function assertInside(root, candidate, label) {
|
|
50
|
+
const rel = relative(root, candidate);
|
|
51
|
+
if (rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
52
|
+
throw new Error(`${label} must be inside ${root}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function shortHash(parts) {
|
|
56
|
+
const hash = createHash('sha256');
|
|
57
|
+
for (const part of parts)
|
|
58
|
+
hash.update(part).update('\0');
|
|
59
|
+
return hash.digest('hex').slice(0, 24);
|
|
60
|
+
}
|
|
61
|
+
function functionName(path) {
|
|
62
|
+
const node = path.node;
|
|
63
|
+
if (SAFE_IDENTIFIER.test(node.id?.name || ''))
|
|
64
|
+
return node.id.name;
|
|
65
|
+
if (SAFE_IDENTIFIER.test(node.key?.name || ''))
|
|
66
|
+
return node.key.name;
|
|
67
|
+
const parent = path.parentPath;
|
|
68
|
+
if (parent?.isVariableDeclarator() && SAFE_IDENTIFIER.test(parent.node.id?.name || ''))
|
|
69
|
+
return parent.node.id.name;
|
|
70
|
+
if (parent?.isObjectProperty() && SAFE_IDENTIFIER.test(parent.node.key?.name || ''))
|
|
71
|
+
return parent.node.key.name;
|
|
72
|
+
if (parent?.isAssignmentExpression() && SAFE_IDENTIFIER.test(parent.node.left?.property?.name || '')) {
|
|
73
|
+
return parent.node.left.property.name;
|
|
74
|
+
}
|
|
75
|
+
return '(anon)';
|
|
76
|
+
}
|
|
77
|
+
function memberName(node) {
|
|
78
|
+
if (!node)
|
|
79
|
+
return null;
|
|
80
|
+
if (node.type === 'Identifier')
|
|
81
|
+
return SAFE_IDENTIFIER.test(node.name) ? node.name : null;
|
|
82
|
+
if (node.type === 'StringLiteral')
|
|
83
|
+
return SAFE_IDENTIFIER.test(node.value) ? node.value : null;
|
|
84
|
+
if (node.type === 'MemberExpression' && !node.computed) {
|
|
85
|
+
const object = memberName(node.object);
|
|
86
|
+
const property = memberName(node.property);
|
|
87
|
+
return object && property ? `${object}.${property}` : null;
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
function qualifiedFunctionName(path) {
|
|
92
|
+
const node = path.node;
|
|
93
|
+
const localName = functionName(path);
|
|
94
|
+
const classPath = path.findParent((candidate) => candidate.isClassDeclaration?.() || candidate.isClassExpression?.());
|
|
95
|
+
if (classPath && (path.isClassMethod?.() || path.isClassPrivateMethod?.())) {
|
|
96
|
+
const className = classPath.node.id?.name;
|
|
97
|
+
const method = memberName(node.key);
|
|
98
|
+
if (className && method)
|
|
99
|
+
return `${className}.${method}`;
|
|
100
|
+
}
|
|
101
|
+
const parent = path.parentPath;
|
|
102
|
+
if (path.isObjectMethod?.() || parent?.isObjectProperty?.()) {
|
|
103
|
+
const property = memberName(node.key ?? parent.node.key);
|
|
104
|
+
const objectExpression = path.isObjectMethod?.() ? parent : parent.parentPath;
|
|
105
|
+
const declarator = objectExpression?.parentPath;
|
|
106
|
+
const owner = declarator?.isVariableDeclarator?.()
|
|
107
|
+
? memberName(declarator.node.id)
|
|
108
|
+
: null;
|
|
109
|
+
if (owner && property)
|
|
110
|
+
return `${owner}.${property}`;
|
|
111
|
+
}
|
|
112
|
+
if (parent?.isVariableDeclarator?.()) {
|
|
113
|
+
const name = memberName(parent.node.id);
|
|
114
|
+
if (name)
|
|
115
|
+
return name;
|
|
116
|
+
}
|
|
117
|
+
if (parent?.isAssignmentExpression?.()) {
|
|
118
|
+
const name = memberName(parent.node.left);
|
|
119
|
+
if (name)
|
|
120
|
+
return name;
|
|
121
|
+
}
|
|
122
|
+
return localName;
|
|
123
|
+
}
|
|
124
|
+
function enclosingFunctionName(path) {
|
|
125
|
+
const fn = path.getFunctionParent();
|
|
126
|
+
return fn ? functionName(fn) : '(top)';
|
|
127
|
+
}
|
|
128
|
+
function sourceFieldsForParameter(fnPath, name) {
|
|
129
|
+
const binding = fnPath.scope.getBinding(name);
|
|
130
|
+
if (!binding)
|
|
131
|
+
return [];
|
|
132
|
+
const fields = new Set();
|
|
133
|
+
for (const reference of binding.referencePaths ?? []) {
|
|
134
|
+
const member = reference.parentPath;
|
|
135
|
+
const isMember = member?.isMemberExpression() || member?.isOptionalMemberExpression?.();
|
|
136
|
+
if (!isMember || member.node.object !== reference.node)
|
|
137
|
+
continue;
|
|
138
|
+
const property = member.node.property;
|
|
139
|
+
if (!member.node.computed && property?.name)
|
|
140
|
+
fields.add(property.name);
|
|
141
|
+
else if (member.node.computed && typeof property?.value === 'string')
|
|
142
|
+
fields.add(property.value);
|
|
143
|
+
}
|
|
144
|
+
return [...fields].filter(field => /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/.test(field)).sort().slice(0, 48);
|
|
145
|
+
}
|
|
146
|
+
function sourceFieldsForReturnedObject(node) {
|
|
147
|
+
if (node?.type !== 'ObjectExpression')
|
|
148
|
+
return [];
|
|
149
|
+
const fields = new Set();
|
|
150
|
+
for (const property of node.properties || []) {
|
|
151
|
+
if (property?.type !== 'ObjectProperty' && property?.type !== 'ObjectMethod')
|
|
152
|
+
continue;
|
|
153
|
+
const key = !property.computed && property.key?.name
|
|
154
|
+
? property.key.name
|
|
155
|
+
: typeof property.key?.value === 'string'
|
|
156
|
+
? property.key.value
|
|
157
|
+
: null;
|
|
158
|
+
if (key && /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/.test(key))
|
|
159
|
+
fields.add(key);
|
|
160
|
+
}
|
|
161
|
+
return [...fields].sort().slice(0, 48);
|
|
162
|
+
}
|
|
163
|
+
function parameterBindings(t, parameter) {
|
|
164
|
+
if (t.isIdentifier(parameter))
|
|
165
|
+
return [parameter];
|
|
166
|
+
if (t.isAssignmentPattern(parameter))
|
|
167
|
+
return parameterBindings(t, parameter.left);
|
|
168
|
+
if (t.isRestElement(parameter))
|
|
169
|
+
return parameterBindings(t, parameter.argument);
|
|
170
|
+
if (t.isObjectPattern(parameter)) {
|
|
171
|
+
return parameter.properties.flatMap((property) => t.isRestElement(property)
|
|
172
|
+
? parameterBindings(t, property.argument)
|
|
173
|
+
: t.isObjectProperty(property)
|
|
174
|
+
? parameterBindings(t, property.value)
|
|
175
|
+
: []);
|
|
176
|
+
}
|
|
177
|
+
if (t.isArrayPattern(parameter)) {
|
|
178
|
+
return parameter.elements.flatMap((element) => element ? parameterBindings(t, element) : []);
|
|
179
|
+
}
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
function optionalProbeCall(t, probeName, value) {
|
|
183
|
+
return t.callExpression(t.identifier(probeName), [value]);
|
|
184
|
+
}
|
|
185
|
+
function capturedSiteProbeDeclaration(t, capturedName, runtimeControlName, passthroughName, runtimeIntegrity, runtimeProbeName, site) {
|
|
186
|
+
return t.variableDeclaration('const', [t.variableDeclarator(t.identifier(capturedName), t.callExpression(t.functionExpression(null, [], t.blockStatement([
|
|
187
|
+
t.tryStatement(t.blockStatement([
|
|
188
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier('selected'), t.callExpression(t.memberExpression(t.identifier(runtimeControlName), t.identifier('safeSelectSiteProbe')), [
|
|
189
|
+
t.stringLiteral(runtimeIntegrity),
|
|
190
|
+
t.stringLiteral(runtimeProbeName),
|
|
191
|
+
t.stringLiteral(site),
|
|
192
|
+
t.identifier(passthroughName),
|
|
193
|
+
]))]),
|
|
194
|
+
t.returnStatement(t.conditionalExpression(t.binaryExpression('===', t.unaryExpression('typeof', t.identifier('selected')), t.stringLiteral('function')), t.identifier('selected'), t.identifier(passthroughName))),
|
|
195
|
+
]), t.catchClause(t.identifier('_error'), t.blockStatement([
|
|
196
|
+
t.returnStatement(t.identifier(passthroughName)),
|
|
197
|
+
]))),
|
|
198
|
+
])), []))]);
|
|
199
|
+
}
|
|
200
|
+
function runtimeControlDeclarations(t, controlName, helperName, specifier, moduleType, runtimeGuardChannel, runtimeBlockedChannel) {
|
|
201
|
+
const fallbackModule = (processHostName) => t.objectExpression([t.objectMethod('method', t.identifier('safeSelectProbe'), [], t.blockStatement([t.returnStatement(t.identifier(helperName))])), t.objectMethod('method', t.identifier('safeSelectSiteProbe'), [
|
|
202
|
+
t.identifier('_integrity'),
|
|
203
|
+
t.identifier('_name'),
|
|
204
|
+
t.identifier('_site'),
|
|
205
|
+
t.identifier('passthrough'),
|
|
206
|
+
], t.blockStatement(processHostName ? [
|
|
207
|
+
t.variableDeclaration('let', [t.variableDeclarator(t.identifier('selected'), t.nullLiteral())]),
|
|
208
|
+
t.returnStatement(t.functionExpression(null, [t.identifier('value')], t.blockStatement([
|
|
209
|
+
t.ifStatement(t.identifier('selected'), t.returnStatement(t.callExpression(t.identifier('selected'), [t.identifier('value')]))),
|
|
210
|
+
t.tryStatement(t.blockStatement([
|
|
211
|
+
t.ifStatement(t.binaryExpression('in', t.stringLiteral(runtimeBlockedChannel), t.identifier(processHostName)), t.blockStatement([
|
|
212
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('selected'), t.identifier('passthrough'))),
|
|
213
|
+
t.returnStatement(t.identifier('value')),
|
|
214
|
+
])),
|
|
215
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier('loaded'), t.memberExpression(t.identifier(processHostName), t.stringLiteral(runtimeGuardChannel), true))]),
|
|
216
|
+
t.ifStatement(t.logicalExpression('&&', t.identifier('loaded'), t.binaryExpression('===', t.unaryExpression('typeof', t.memberExpression(t.identifier('loaded'), t.identifier('safeSelectSiteProbe'))), t.stringLiteral('function'))), t.blockStatement([
|
|
217
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier('candidate'), t.callExpression(t.memberExpression(t.identifier('loaded'), t.identifier('safeSelectSiteProbe')), [
|
|
218
|
+
t.identifier('_integrity'),
|
|
219
|
+
t.identifier('_name'),
|
|
220
|
+
t.identifier('_site'),
|
|
221
|
+
t.identifier('passthrough'),
|
|
222
|
+
]))]),
|
|
223
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('selected'), t.conditionalExpression(t.binaryExpression('===', t.unaryExpression('typeof', t.identifier('candidate')), t.stringLiteral('function')), t.identifier('candidate'), t.identifier('passthrough')))),
|
|
224
|
+
t.returnStatement(t.callExpression(t.identifier('selected'), [t.identifier('value')])),
|
|
225
|
+
])),
|
|
226
|
+
]), t.catchClause(t.identifier('_error'), t.blockStatement([
|
|
227
|
+
t.expressionStatement(t.assignmentExpression('=', t.identifier('selected'), t.identifier('passthrough'))),
|
|
228
|
+
]))),
|
|
229
|
+
t.returnStatement(t.identifier('value')),
|
|
230
|
+
]))),
|
|
231
|
+
] : [t.returnStatement(t.identifier('passthrough'))]))]);
|
|
232
|
+
if (moduleType === 'module') {
|
|
233
|
+
return [
|
|
234
|
+
t.importDeclaration([t.importDefaultSpecifier(t.identifier(`${controlName}Imported`))], t.stringLiteral(specifier)),
|
|
235
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier(controlName), t.conditionalExpression(t.binaryExpression('===', t.unaryExpression('typeof', t.memberExpression(t.identifier(`${controlName}Imported`), t.identifier('safeSelectProbe'))), t.stringLiteral('function')), t.identifier(`${controlName}Imported`), fallbackModule()))]),
|
|
236
|
+
];
|
|
237
|
+
}
|
|
238
|
+
return [
|
|
239
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier(controlName), t.callExpression(t.functionExpression(null, [], t.blockStatement([
|
|
240
|
+
t.tryStatement(t.blockStatement([
|
|
241
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier('processHost'), t.callExpression(t.identifier('require'), [t.stringLiteral('node:process')]))]),
|
|
242
|
+
t.ifStatement(t.binaryExpression('in', t.stringLiteral(runtimeBlockedChannel), t.identifier('processHost')), t.returnStatement(fallbackModule('processHost'))),
|
|
243
|
+
t.variableDeclaration('const', [t.variableDeclarator(t.identifier('loaded'), t.memberExpression(t.identifier('processHost'), t.stringLiteral(runtimeGuardChannel), true))]),
|
|
244
|
+
t.returnStatement(t.conditionalExpression(t.logicalExpression('&&', t.identifier('loaded'), t.binaryExpression('===', t.unaryExpression('typeof', t.memberExpression(t.identifier('loaded'), t.identifier('safeSelectProbe'))), t.stringLiteral('function'))), t.identifier('loaded'), fallbackModule('processHost'))),
|
|
245
|
+
]), t.catchClause(t.identifier('_error'), t.blockStatement([
|
|
246
|
+
t.returnStatement(fallbackModule()),
|
|
247
|
+
]))),
|
|
248
|
+
])), []))]),
|
|
249
|
+
];
|
|
250
|
+
}
|
|
251
|
+
function passthroughHelper(t, helperName) {
|
|
252
|
+
return t.functionDeclaration(t.identifier(helperName), [t.identifier('_site'), t.identifier('value')], t.blockStatement([t.returnStatement(t.identifier('value'))]));
|
|
253
|
+
}
|
|
254
|
+
function sitePassthroughHelper(t, helperName) {
|
|
255
|
+
return t.functionDeclaration(t.identifier(helperName), [t.identifier('value')], t.blockStatement([t.returnStatement(t.identifier('value'))]));
|
|
256
|
+
}
|
|
257
|
+
function instrumentSource(code, sourcePath, runtimeSpecifier, moduleType, controlChannel, runtimeIntegrity, includedQualifiedNames) {
|
|
258
|
+
if (hasGeneratedInstrumentation(code))
|
|
259
|
+
return null;
|
|
260
|
+
const probes = { branches: 0, parameters: 0, bindings: 0, returns: 0, throws: 0 };
|
|
261
|
+
const ordinals = new WeakMap();
|
|
262
|
+
const functionIdentities = new WeakMap();
|
|
263
|
+
const functionNameCounts = new Map();
|
|
264
|
+
const selectedOwners = new Map();
|
|
265
|
+
const sites = new Map();
|
|
266
|
+
const siteProbeNames = new Map();
|
|
267
|
+
let helperName = '';
|
|
268
|
+
let siteHelperName = '';
|
|
269
|
+
let runtimeControlName = '';
|
|
270
|
+
let skipFileProbes = false;
|
|
271
|
+
const nextOrdinal = (path, kind) => {
|
|
272
|
+
const owner = path.getFunctionParent()?.node ?? path.findParent((candidate) => candidate.isProgram())?.node;
|
|
273
|
+
if (!owner)
|
|
274
|
+
return 0;
|
|
275
|
+
let byKind = ordinals.get(owner);
|
|
276
|
+
if (!byKind) {
|
|
277
|
+
byKind = new Map();
|
|
278
|
+
ordinals.set(owner, byKind);
|
|
279
|
+
}
|
|
280
|
+
const next = byKind.get(kind) ?? 0;
|
|
281
|
+
byKind.set(kind, next + 1);
|
|
282
|
+
return next;
|
|
283
|
+
};
|
|
284
|
+
const ownerIdentity = (path) => {
|
|
285
|
+
const fn = path.isFunction?.() ? path : path.getFunctionParent();
|
|
286
|
+
return fn ? functionIdentities.get(fn.node) ?? `${functionName(fn)}:unmapped` : '(top):0';
|
|
287
|
+
};
|
|
288
|
+
const site = (fn, qualifiedName, owner, kind, structuralKey, label, allowedFields = []) => {
|
|
289
|
+
const targetId = `site-${shortHash([sourcePath, owner, kind, structuralKey])}`;
|
|
290
|
+
const value = `${sourcePath}#${fn}:${targetId}${label ? `(${label})` : ''}`;
|
|
291
|
+
// Leave room under the ingestion endpoint's 512-character key cap for a
|
|
292
|
+
// static field label and its longest value-shape outcome.
|
|
293
|
+
if (value.length > 360)
|
|
294
|
+
throw new Error(`Telemetry site exceeds the ingestion key limit in ${sourcePath}`);
|
|
295
|
+
sites.set(value, {
|
|
296
|
+
probe_key: value,
|
|
297
|
+
site_id: targetId,
|
|
298
|
+
source_path: sourcePath,
|
|
299
|
+
qualified_name: qualifiedName,
|
|
300
|
+
probe_kind: kind,
|
|
301
|
+
owner_key: owner,
|
|
302
|
+
structural_key: structuralKey,
|
|
303
|
+
...(label ? { label } : {}),
|
|
304
|
+
allowed_fields: [...allowedFields],
|
|
305
|
+
});
|
|
306
|
+
return value;
|
|
307
|
+
};
|
|
308
|
+
const plugin = ({ types: t }) => {
|
|
309
|
+
const siteProbeName = (path, runtimeProbeName, siteKey) => {
|
|
310
|
+
const existing = siteProbeNames.get(siteKey);
|
|
311
|
+
if (existing)
|
|
312
|
+
return existing;
|
|
313
|
+
const name = path.scope.getProgramParent().generateUidIdentifier(`__haystackTelemetrySite_${siteProbeNames.size}`).name;
|
|
314
|
+
siteProbeNames.set(siteKey, name);
|
|
315
|
+
return name;
|
|
316
|
+
};
|
|
317
|
+
const insideAccessor = (path) => {
|
|
318
|
+
const fn = path.getFunctionParent();
|
|
319
|
+
return Boolean(fn && (fn.node.kind === 'get' || fn.node.kind === 'set'));
|
|
320
|
+
};
|
|
321
|
+
const selectedFunction = (path) => {
|
|
322
|
+
if (!includedQualifiedNames)
|
|
323
|
+
return true;
|
|
324
|
+
const functionPath = path.isFunction?.() ? path : path.getFunctionParent();
|
|
325
|
+
return Boolean(functionPath && includedQualifiedNames.has(qualifiedFunctionName(functionPath)));
|
|
326
|
+
};
|
|
327
|
+
const wrapTest = (path, syntax) => {
|
|
328
|
+
if (skipFileProbes || insideAccessor(path) || !selectedFunction(path))
|
|
329
|
+
return;
|
|
330
|
+
const testPath = path.get('test');
|
|
331
|
+
const test = testPath?.node;
|
|
332
|
+
if (!test || !t.isExpression(test))
|
|
333
|
+
return;
|
|
334
|
+
const fn = enclosingFunctionName(path);
|
|
335
|
+
const functionPath = path.getFunctionParent();
|
|
336
|
+
const qualifiedName = functionPath ? qualifiedFunctionName(functionPath) : '(top)';
|
|
337
|
+
const ordinal = nextOrdinal(path, `branch:${syntax}`);
|
|
338
|
+
const branchSite = site(fn, qualifiedName, ownerIdentity(path), 'condition', `${syntax}:${ordinal}`);
|
|
339
|
+
testPath.replaceWith(optionalProbeCall(t, siteProbeName(path, '__hstBranch', branchSite), test));
|
|
340
|
+
testPath.skip();
|
|
341
|
+
probes.branches++;
|
|
342
|
+
};
|
|
343
|
+
return {
|
|
344
|
+
visitor: {
|
|
345
|
+
Program: {
|
|
346
|
+
enter(path) {
|
|
347
|
+
helperName = path.scope.generateUidIdentifier('__haystackTelemetryPassthrough').name;
|
|
348
|
+
siteHelperName = path.scope.generateUidIdentifier('__haystackTelemetrySitePassthrough').name;
|
|
349
|
+
runtimeControlName = path.scope.generateUidIdentifier('__haystackTelemetryControl').name;
|
|
350
|
+
path.traverse({
|
|
351
|
+
CallExpression(candidate) {
|
|
352
|
+
if (candidate.get('callee').isIdentifier?.({ name: 'eval' })) {
|
|
353
|
+
skipFileProbes = true;
|
|
354
|
+
candidate.stop();
|
|
355
|
+
}
|
|
356
|
+
},
|
|
357
|
+
WithStatement(candidate) {
|
|
358
|
+
skipFileProbes = true;
|
|
359
|
+
candidate.stop();
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
},
|
|
363
|
+
exit(path) {
|
|
364
|
+
const optionalProbeCount = probes.branches + probes.bindings + probes.returns + probes.throws;
|
|
365
|
+
const totalProbeCount = optionalProbeCount + probes.parameters;
|
|
366
|
+
if (skipFileProbes || totalProbeCount === 0)
|
|
367
|
+
return;
|
|
368
|
+
const declarations = [
|
|
369
|
+
...runtimeControlDeclarations(t, runtimeControlName, helperName, runtimeSpecifier, moduleType, `${controlChannel}_runtimeGuard`, `${controlChannel}_runtimeBlocked`),
|
|
370
|
+
passthroughHelper(t, helperName),
|
|
371
|
+
sitePassthroughHelper(t, siteHelperName),
|
|
372
|
+
...[...siteProbeNames.entries()].map(([siteKey, name]) => {
|
|
373
|
+
const probeKind = sites.get(siteKey)?.probe_kind;
|
|
374
|
+
const runtimeProbeName = probeKind === 'condition'
|
|
375
|
+
? '__hstBranch'
|
|
376
|
+
: probeKind === 'throw'
|
|
377
|
+
? '__hstThrow'
|
|
378
|
+
: '__hstValue';
|
|
379
|
+
return capturedSiteProbeDeclaration(t, name, runtimeControlName, siteHelperName, runtimeIntegrity, runtimeProbeName, siteKey);
|
|
380
|
+
}),
|
|
381
|
+
];
|
|
382
|
+
const paths = path.unshiftContainer('body', declarations);
|
|
383
|
+
for (const inserted of paths)
|
|
384
|
+
inserted.skip();
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
Function: {
|
|
388
|
+
enter(path) {
|
|
389
|
+
const fn = functionName(path);
|
|
390
|
+
const occurrence = functionNameCounts.get(fn) ?? 0;
|
|
391
|
+
functionNameCounts.set(fn, occurrence + 1);
|
|
392
|
+
const owner = `${fn}:${occurrence}`;
|
|
393
|
+
functionIdentities.set(path.node, owner);
|
|
394
|
+
const qualifiedName = qualifiedFunctionName(path);
|
|
395
|
+
if (includedQualifiedNames?.has(qualifiedName)) {
|
|
396
|
+
const owners = selectedOwners.get(qualifiedName) ?? new Set();
|
|
397
|
+
owners.add(owner);
|
|
398
|
+
selectedOwners.set(qualifiedName, owners);
|
|
399
|
+
}
|
|
400
|
+
if (path.node.kind === 'get' || path.node.kind === 'set')
|
|
401
|
+
return;
|
|
402
|
+
if (t.isArrowFunctionExpression(path.node) && !t.isBlockStatement(path.node.body)) {
|
|
403
|
+
path.ensureBlock();
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
exit(path) {
|
|
407
|
+
if (skipFileProbes || path.node.kind === 'get' || path.node.kind === 'set' ||
|
|
408
|
+
!selectedFunction(path))
|
|
409
|
+
return;
|
|
410
|
+
const fn = functionName(path);
|
|
411
|
+
const statements = [];
|
|
412
|
+
for (const [parameterIndex, parameter] of path.node.params.entries()) {
|
|
413
|
+
for (const [bindingIndex, identifier] of parameterBindings(t, parameter).entries()) {
|
|
414
|
+
const fields = sourceFieldsForParameter(path, identifier.name);
|
|
415
|
+
const parameterSite = site(fn, qualifiedFunctionName(path), ownerIdentity(path), 'parameter', `${parameterIndex}:${bindingIndex}:${identifier.name}`, `parameter:${identifier.name}`, fields);
|
|
416
|
+
statements.push(t.expressionStatement(optionalProbeCall(t, siteProbeName(path, '__hstValue', parameterSite), t.identifier(identifier.name))));
|
|
417
|
+
probes.parameters++;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (statements.length > 0 && t.isBlockStatement(path.node.body)) {
|
|
421
|
+
const inserted = path.get('body').unshiftContainer('body', statements);
|
|
422
|
+
for (const statement of inserted)
|
|
423
|
+
statement.skip();
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
IfStatement: { exit: (path) => wrapTest(path, 'if') },
|
|
428
|
+
ConditionalExpression: { exit: (path) => wrapTest(path, 'conditional') },
|
|
429
|
+
WhileStatement: { exit: (path) => wrapTest(path, 'while') },
|
|
430
|
+
DoWhileStatement: { exit: (path) => wrapTest(path, 'do-while') },
|
|
431
|
+
ForStatement: {
|
|
432
|
+
exit(path) {
|
|
433
|
+
if (path.node.test)
|
|
434
|
+
wrapTest(path, 'for');
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
VariableDeclarator: {
|
|
438
|
+
exit(path) {
|
|
439
|
+
if (skipFileProbes || insideAccessor(path) || !selectedFunction(path))
|
|
440
|
+
return;
|
|
441
|
+
if (!t.isIdentifier(path.node.id) || !path.node.init || !t.isExpression(path.node.init))
|
|
442
|
+
return;
|
|
443
|
+
const fn = enclosingFunctionName(path);
|
|
444
|
+
const functionPath = path.getFunctionParent();
|
|
445
|
+
const qualifiedName = functionPath ? qualifiedFunctionName(functionPath) : '(top)';
|
|
446
|
+
const ordinal = nextOrdinal(path, `binding:${path.node.id.name}`);
|
|
447
|
+
const valueSite = site(fn, qualifiedName, ownerIdentity(path), 'binding', `${path.node.id.name}:${ordinal}`, `binding:${path.node.id.name}`);
|
|
448
|
+
path.get('init').replaceWith(optionalProbeCall(t, siteProbeName(path, '__hstValue', valueSite), path.node.init));
|
|
449
|
+
path.get('init').skip();
|
|
450
|
+
probes.bindings++;
|
|
451
|
+
},
|
|
452
|
+
},
|
|
453
|
+
ReturnStatement: {
|
|
454
|
+
exit(path) {
|
|
455
|
+
if (skipFileProbes || insideAccessor(path) || !selectedFunction(path))
|
|
456
|
+
return;
|
|
457
|
+
if (!path.node.argument || !t.isExpression(path.node.argument))
|
|
458
|
+
return;
|
|
459
|
+
const fn = enclosingFunctionName(path);
|
|
460
|
+
const functionPath = path.getFunctionParent();
|
|
461
|
+
const qualifiedName = functionPath ? qualifiedFunctionName(functionPath) : '(top)';
|
|
462
|
+
const ordinal = nextOrdinal(path, 'return');
|
|
463
|
+
const fields = sourceFieldsForReturnedObject(path.node.argument);
|
|
464
|
+
const returnSite = site(fn, qualifiedName, ownerIdentity(path), 'return', String(ordinal), 'return', fields);
|
|
465
|
+
path.get('argument').replaceWith(optionalProbeCall(t, siteProbeName(path, '__hstValue', returnSite), path.node.argument));
|
|
466
|
+
path.get('argument').skip();
|
|
467
|
+
probes.returns++;
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
ThrowStatement: {
|
|
471
|
+
exit(path) {
|
|
472
|
+
if (skipFileProbes || insideAccessor(path) || !selectedFunction(path))
|
|
473
|
+
return;
|
|
474
|
+
if (!path.node.argument || !t.isExpression(path.node.argument))
|
|
475
|
+
return;
|
|
476
|
+
const fn = enclosingFunctionName(path);
|
|
477
|
+
const functionPath = path.getFunctionParent();
|
|
478
|
+
const qualifiedName = functionPath ? qualifiedFunctionName(functionPath) : '(top)';
|
|
479
|
+
const ordinal = nextOrdinal(path, 'throw');
|
|
480
|
+
const throwSite = site(fn, qualifiedName, ownerIdentity(path), 'throw', String(ordinal), 'throw');
|
|
481
|
+
path.get('argument').replaceWith(optionalProbeCall(t, siteProbeName(path, '__hstThrow', throwSite), path.node.argument));
|
|
482
|
+
path.get('argument').skip();
|
|
483
|
+
probes.throws++;
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
};
|
|
489
|
+
const transformed = babel.transformSync(code, {
|
|
490
|
+
parserOpts: {
|
|
491
|
+
sourceType: 'unambiguous',
|
|
492
|
+
plugins: ['jsx'],
|
|
493
|
+
},
|
|
494
|
+
plugins: [plugin],
|
|
495
|
+
babelrc: false,
|
|
496
|
+
configFile: false,
|
|
497
|
+
ast: false,
|
|
498
|
+
code: true,
|
|
499
|
+
retainLines: true,
|
|
500
|
+
compact: false,
|
|
501
|
+
});
|
|
502
|
+
if (!transformed?.code)
|
|
503
|
+
throw new Error(`Instrumentation produced no code for ${sourcePath}`);
|
|
504
|
+
if (!helperName || !siteHelperName || !runtimeControlName) {
|
|
505
|
+
throw new Error(`Instrumentation produced no helper identity for ${sourcePath}`);
|
|
506
|
+
}
|
|
507
|
+
const ambiguousSelections = [...selectedOwners.entries()]
|
|
508
|
+
.filter(([, owners]) => owners.size > 1)
|
|
509
|
+
.map(([qualifiedName]) => `${sourcePath}#${qualifiedName}`);
|
|
510
|
+
if (ambiguousSelections.length > 0) {
|
|
511
|
+
throw new Error(`Telemetry exact symbol selection is ambiguous: ${ambiguousSelections.join(', ')}; select a symbol with a unique stable identity`);
|
|
512
|
+
}
|
|
513
|
+
return {
|
|
514
|
+
code: prefixAfterShebang(transformed.code, `${INSTRUMENTED_MARKER_COMMENT} `),
|
|
515
|
+
probes,
|
|
516
|
+
sites: [...sites.values()].sort((left, right) => left.site_id.localeCompare(right.site_id)),
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
function prefixAfterShebang(code, prefix) {
|
|
520
|
+
if (!code.startsWith('#!'))
|
|
521
|
+
return prefix + code;
|
|
522
|
+
const newline = code.indexOf('\n');
|
|
523
|
+
if (newline === -1)
|
|
524
|
+
return `${code}\n${prefix}`;
|
|
525
|
+
return `${code.slice(0, newline + 1)}${prefix}${code.slice(newline + 1)}`;
|
|
526
|
+
}
|
|
527
|
+
function findNearestPackageType(start, stop) {
|
|
528
|
+
let current = start;
|
|
529
|
+
for (;;) {
|
|
530
|
+
const manifest = join(current, 'package.json');
|
|
531
|
+
if (existsSync(manifest)) {
|
|
532
|
+
try {
|
|
533
|
+
return JSON.parse(readFileSync(manifest, 'utf8')).type === 'module' ? 'module' : 'commonjs';
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
throw new Error(`Could not parse ${manifest}: ${error instanceof Error ? error.message : String(error)}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (current === stop || dirname(current) === current)
|
|
540
|
+
return 'commonjs';
|
|
541
|
+
current = dirname(current);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
function moduleTypeForOutput(output, dist) {
|
|
545
|
+
const extension = extname(output);
|
|
546
|
+
if (extension === '.mjs')
|
|
547
|
+
return 'module';
|
|
548
|
+
if (extension === '.cjs')
|
|
549
|
+
return 'commonjs';
|
|
550
|
+
return findNearestPackageType(dirname(output), process.cwd());
|
|
551
|
+
}
|
|
552
|
+
function runtimeSpecifierForOutput(output, dist) {
|
|
553
|
+
const runtimeFilename = moduleTypeForOutput(output, dist) === 'module'
|
|
554
|
+
? RUNTIME_ESM_FILENAME
|
|
555
|
+
: RUNTIME_FILENAME;
|
|
556
|
+
const runtime = normalizedPath(relative(dirname(output), join(dist, RUNTIME_DIRECTORY, runtimeFilename)));
|
|
557
|
+
return runtime.startsWith('./') || runtime.startsWith('../') ? runtime : `./${runtime}`;
|
|
558
|
+
}
|
|
559
|
+
function bootstrapFor(entry, dist, buildId, runtimeHash, runtimeGuardChannel, runtimeBlockedChannel) {
|
|
560
|
+
const specifier = runtimeSpecifierForOutput(entry, dist);
|
|
561
|
+
const moduleType = moduleTypeForOutput(entry, dist);
|
|
562
|
+
if (moduleType === 'module') {
|
|
563
|
+
return `/* ${BOOTSTRAP_MARKER} */ import __haystackTelemetryRuntimeV1 from ${JSON.stringify(specifier)};\n__haystackTelemetryRuntimeV1.install({ buildId: ${JSON.stringify(buildId)} });\n`;
|
|
564
|
+
}
|
|
565
|
+
// Evaluate a hash-verified runtime outside CommonJS's public cache so a
|
|
566
|
+
// preload cannot substitute its exports. Generated modules read the captured
|
|
567
|
+
// capability from process instead of re-requiring a mutable cache entry. A
|
|
568
|
+
// normal configurable cache entry is still populated for application code,
|
|
569
|
+
// and applications remain free to delete it.
|
|
570
|
+
return `/* ${BOOTSTRAP_MARKER} */ (() => { const processHost = require('node:process'); const define = (name, descriptor) => ({}).constructor.defineProperty(processHost, name, descriptor); const block = () => { try { if (!(${JSON.stringify(runtimeBlockedChannel)} in processHost)) define(${JSON.stringify(runtimeBlockedChannel)}, { configurable: false, enumerable: false, writable: false, value: true }); } catch (_ignored) {} }; try { if (${JSON.stringify(runtimeGuardChannel)} in processHost || ${JSON.stringify(runtimeBlockedChannel)} in processHost) { block(); return; } const runtimePath = require.resolve(${JSON.stringify(specifier)}); const source = require('node:fs').readFileSync(runtimePath, 'utf8'); if (require('node:crypto').createHash('sha256').update(source).digest('hex') !== ${JSON.stringify(runtimeHash)}) { block(); return; } const Module = require('node:module'); const loaded = new Module(runtimePath); loaded.filename = runtimePath; loaded.paths = Module._nodeModulePaths(require('node:path').dirname(runtimePath)); loaded._compile(source, runtimePath); loaded.loaded = true; const runtime = loaded.exports; if (!runtime || typeof runtime.install !== 'function' || typeof runtime.safeSelectProbe !== 'function') { block(); return; } define(${JSON.stringify(runtimeGuardChannel)}, { configurable: false, enumerable: false, writable: false, value: runtime }); require.cache[runtimePath] = loaded; runtime.install({ buildId: ${JSON.stringify(buildId)} }); } catch (_error) { block(); } })();\n`;
|
|
571
|
+
}
|
|
572
|
+
function insertEntryBootstrap(code, bootstrap) {
|
|
573
|
+
const bootstrapAst = babel.parseSync(bootstrap, {
|
|
574
|
+
sourceType: 'unambiguous',
|
|
575
|
+
});
|
|
576
|
+
if (!bootstrapAst?.program?.body?.length) {
|
|
577
|
+
throw new Error('Telemetry bootstrap produced no statements');
|
|
578
|
+
}
|
|
579
|
+
const transformed = babel.transformSync(code, {
|
|
580
|
+
parserOpts: {
|
|
581
|
+
sourceType: 'unambiguous',
|
|
582
|
+
plugins: ['jsx'],
|
|
583
|
+
},
|
|
584
|
+
plugins: [() => ({
|
|
585
|
+
visitor: {
|
|
586
|
+
Program: {
|
|
587
|
+
exit(path) {
|
|
588
|
+
path.unshiftContainer('body', bootstrapAst.program.body);
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
})],
|
|
593
|
+
babelrc: false,
|
|
594
|
+
configFile: false,
|
|
595
|
+
ast: false,
|
|
596
|
+
code: true,
|
|
597
|
+
retainLines: true,
|
|
598
|
+
compact: false,
|
|
599
|
+
});
|
|
600
|
+
if (!transformed?.code)
|
|
601
|
+
throw new Error('Telemetry bootstrap produced no entry output');
|
|
602
|
+
return transformed.code;
|
|
603
|
+
}
|
|
604
|
+
function esmRuntimeWrapper(runtimeHash) {
|
|
605
|
+
return `import { createHash } from 'node:crypto';
|
|
606
|
+
import { readFileSync } from 'node:fs';
|
|
607
|
+
import Module from 'node:module';
|
|
608
|
+
import { dirname } from 'node:path';
|
|
609
|
+
import { fileURLToPath } from 'node:url';
|
|
610
|
+
|
|
611
|
+
const fallback = Object.freeze({
|
|
612
|
+
install() { return { enabled: false, reason: 'runtime-unavailable' }; },
|
|
613
|
+
safeSelectProbe(_integrity, _name, passthrough) { return passthrough; },
|
|
614
|
+
safeSelectSiteProbe(_integrity, _name, _site, passthrough) { return passthrough; },
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
const runtime = (() => {
|
|
618
|
+
try {
|
|
619
|
+
const filename = fileURLToPath(new URL('./${RUNTIME_FILENAME}', import.meta.url));
|
|
620
|
+
const source = readFileSync(filename, 'utf8');
|
|
621
|
+
if (createHash('sha256').update(source).digest('hex') !== ${JSON.stringify(runtimeHash)}) return fallback;
|
|
622
|
+
const loaded = new Module(filename);
|
|
623
|
+
loaded.filename = filename;
|
|
624
|
+
loaded.paths = Module._nodeModulePaths(dirname(filename));
|
|
625
|
+
loaded._compile(source, filename);
|
|
626
|
+
return loaded.exports && typeof loaded.exports.safeSelectProbe === 'function'
|
|
627
|
+
? loaded.exports
|
|
628
|
+
: fallback;
|
|
629
|
+
} catch (_error) {
|
|
630
|
+
return fallback;
|
|
631
|
+
}
|
|
632
|
+
})();
|
|
633
|
+
|
|
634
|
+
export default runtime;
|
|
635
|
+
`;
|
|
636
|
+
}
|
|
637
|
+
function sourceForOutput(output, dist, sourceRoot) {
|
|
638
|
+
const rel = relative(dist, output);
|
|
639
|
+
const stem = rel.slice(0, -extname(rel).length);
|
|
640
|
+
const matches = SOURCE_EXTENSIONS
|
|
641
|
+
.map(extension => join(sourceRoot, `${stem}${extension}`))
|
|
642
|
+
.filter(candidate => existsSync(candidate) && statSync(candidate).isFile());
|
|
643
|
+
if (matches.length > 1) {
|
|
644
|
+
throw new Error(`Output ${normalizedPath(rel)} maps to multiple source files: ${matches.map(normalizedPath).join(', ')}`);
|
|
645
|
+
}
|
|
646
|
+
return matches[0] ?? null;
|
|
647
|
+
}
|
|
648
|
+
function runtimeAssetPath() {
|
|
649
|
+
return join(dirname(fileURLToPath(import.meta.url)), '..', 'assets', 'telemetry', RUNTIME_FILENAME);
|
|
650
|
+
}
|
|
651
|
+
function readExistingManifest(path) {
|
|
652
|
+
if (!existsSync(path))
|
|
653
|
+
return null;
|
|
654
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
655
|
+
if (parsed.schema_version !== MANIFEST_SCHEMA_VERSION || !Array.isArray(parsed.sites)) {
|
|
656
|
+
throw new Error(`Telemetry instrumentation manifest at ${path} is outdated; rebuild the output directory`);
|
|
657
|
+
}
|
|
658
|
+
return { ...parsed, already_instrumented: true };
|
|
659
|
+
}
|
|
660
|
+
export function instrumentNodeTelemetryBuild(distDirectory, options) {
|
|
661
|
+
const requestedDist = resolve(distDirectory);
|
|
662
|
+
if (!existsSync(requestedDist) || !statSync(requestedDist).isDirectory()) {
|
|
663
|
+
throw new Error(`Compiled output directory not found: ${requestedDist}`);
|
|
664
|
+
}
|
|
665
|
+
const dist = realpathSync(requestedDist);
|
|
666
|
+
const entry = resolve(dist, options.entry);
|
|
667
|
+
assertInside(dist, entry, 'Entry');
|
|
668
|
+
if (!existsSync(entry) || !statSync(entry).isFile())
|
|
669
|
+
throw new Error(`Entry file not found: ${entry}`);
|
|
670
|
+
const requestedSourceRoot = resolve(options.sourceRoot ?? join(dirname(dist), 'src'));
|
|
671
|
+
if (!existsSync(requestedSourceRoot) || !statSync(requestedSourceRoot).isDirectory()) {
|
|
672
|
+
throw new Error(`Source directory not found: ${requestedSourceRoot}`);
|
|
673
|
+
}
|
|
674
|
+
const sourceRoot = realpathSync(requestedSourceRoot);
|
|
675
|
+
const selectedSymbolsBySource = new Map();
|
|
676
|
+
for (const selected of options.includeSymbols ?? []) {
|
|
677
|
+
if (!selected || typeof selected.sourcePath !== 'string' ||
|
|
678
|
+
typeof selected.qualifiedName !== 'string' ||
|
|
679
|
+
!SAFE_SOURCE_PATH.test(selected.sourcePath) ||
|
|
680
|
+
selected.sourcePath.startsWith('/') || selected.sourcePath.includes('\\') ||
|
|
681
|
+
selected.sourcePath.split('/').some(part => !part || part === '.' || part === '..') ||
|
|
682
|
+
!selected.qualifiedName || selected.qualifiedName.length > 256) {
|
|
683
|
+
throw new Error('Telemetry includeSymbols entries require safe exact sourcePath and qualifiedName values');
|
|
684
|
+
}
|
|
685
|
+
const names = selectedSymbolsBySource.get(selected.sourcePath) ?? new Set();
|
|
686
|
+
names.add(selected.qualifiedName);
|
|
687
|
+
selectedSymbolsBySource.set(selected.sourcePath, names);
|
|
688
|
+
}
|
|
689
|
+
const selectiveInstrumentation = options.includeSymbols !== undefined;
|
|
690
|
+
const runtimeDirectory = join(dist, RUNTIME_DIRECTORY);
|
|
691
|
+
const manifestPath = join(runtimeDirectory, MANIFEST_FILENAME);
|
|
692
|
+
const existing = readExistingManifest(manifestPath);
|
|
693
|
+
if (existing) {
|
|
694
|
+
const requestedEntry = normalizedPath(relative(dist, entry));
|
|
695
|
+
if (existing.entry !== requestedEntry) {
|
|
696
|
+
throw new Error(`Output is already instrumented for entry ${existing.entry}, not ${requestedEntry}; rebuild before changing entry`);
|
|
697
|
+
}
|
|
698
|
+
if (!existsSync(join(dist, existing.runtime))) {
|
|
699
|
+
throw new Error('Telemetry instrumentation manifest exists but its runtime is missing; rebuild the output directory');
|
|
700
|
+
}
|
|
701
|
+
for (const file of existing.instrumented_files) {
|
|
702
|
+
const output = join(dist, file.output_path);
|
|
703
|
+
const valid = existsSync(output) && (file.output_path === existing.entry
|
|
704
|
+
? hasGeneratedEntryBootstrap(readFileSync(output, 'utf8'))
|
|
705
|
+
: generatedBody(readFileSync(output, 'utf8')).startsWith(INSTRUMENTED_MARKER_COMMENT));
|
|
706
|
+
if (!valid) {
|
|
707
|
+
throw new Error(`Telemetry instrumentation manifest is stale for ${file.output_path}; rebuild the output directory`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
return existing;
|
|
711
|
+
}
|
|
712
|
+
const bundledRuntime = normalizeLineEndings(readFileSync(runtimeAssetPath(), 'utf8'));
|
|
713
|
+
const outputFiles = fg.sync('**/*.{js,cjs,mjs}', {
|
|
714
|
+
cwd: dist,
|
|
715
|
+
onlyFiles: true,
|
|
716
|
+
dot: true,
|
|
717
|
+
ignore: [`${RUNTIME_DIRECTORY}/**`, '**/node_modules/**'],
|
|
718
|
+
}).map(file => resolve(dist, file))
|
|
719
|
+
.filter(file => normalizeLineEndings(readFileSync(file, 'utf8')) !== bundledRuntime)
|
|
720
|
+
.sort();
|
|
721
|
+
if (outputFiles.length === 0)
|
|
722
|
+
throw new Error(`No JavaScript output files found under ${dist}`);
|
|
723
|
+
if (outputFiles.some(file => hasGeneratedInstrumentation(readFileSync(file, 'utf8')))) {
|
|
724
|
+
throw new Error('Found instrumented files without a matching manifest; rebuild the output directory before retrying');
|
|
725
|
+
}
|
|
726
|
+
const controlChannel = `__haystackTelemetryControl_${shortHash(outputFiles.flatMap(output => [
|
|
727
|
+
normalizedPath(relative(dist, output)),
|
|
728
|
+
normalizeLineEndings(readFileSync(output, 'utf8')),
|
|
729
|
+
]))}`;
|
|
730
|
+
const runtimeTemplate = readFileSync(runtimeAssetPath(), 'utf8');
|
|
731
|
+
if (!runtimeTemplate.includes(RUNTIME_CONTROL_CHANNEL_PLACEHOLDER) ||
|
|
732
|
+
!runtimeTemplate.includes(RUNTIME_INTEGRITY_PLACEHOLDER)) {
|
|
733
|
+
throw new Error('Telemetry runtime is missing its authentication placeholders');
|
|
734
|
+
}
|
|
735
|
+
const runtimeChannelSource = runtimeTemplate.replaceAll(RUNTIME_CONTROL_CHANNEL_PLACEHOLDER, controlChannel);
|
|
736
|
+
const runtimeIntegrity = createHash('sha256').update(runtimeChannelSource).digest('hex');
|
|
737
|
+
const authenticatedRuntime = runtimeChannelSource.replaceAll(RUNTIME_INTEGRITY_PLACEHOLDER, runtimeIntegrity);
|
|
738
|
+
const authenticatedRuntimeHash = createHash('sha256').update(authenticatedRuntime).digest('hex');
|
|
739
|
+
const pending = [];
|
|
740
|
+
const skipped = [];
|
|
741
|
+
for (const output of outputFiles) {
|
|
742
|
+
const source = sourceForOutput(output, dist, sourceRoot);
|
|
743
|
+
if (!source) {
|
|
744
|
+
skipped.push(normalizedPath(relative(dist, output)));
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const sourcePath = normalizedPath(relative(process.cwd(), source));
|
|
748
|
+
if (sourcePath === '..' || sourcePath.startsWith('../') || isAbsolute(sourcePath)) {
|
|
749
|
+
throw new Error(`Source file is outside the current project directory: ${sourcePath}`);
|
|
750
|
+
}
|
|
751
|
+
if (!SAFE_SOURCE_PATH.test(sourcePath)) {
|
|
752
|
+
throw new Error(`Source path contains a telemetry-reserved character (# or |): ${sourcePath}`);
|
|
753
|
+
}
|
|
754
|
+
const transformed = instrumentSource(readFileSync(output, 'utf8'), sourcePath, runtimeSpecifierForOutput(output, dist), moduleTypeForOutput(output, dist), controlChannel, runtimeIntegrity, selectiveInstrumentation ? selectedSymbolsBySource.get(sourcePath) ?? new Set() : null);
|
|
755
|
+
if (transformed)
|
|
756
|
+
pending.push({ output, source: sourcePath, ...transformed });
|
|
757
|
+
}
|
|
758
|
+
if (pending.length === 0) {
|
|
759
|
+
throw new Error('No compiled files had an exact source-file match; pass --source-root with the correct source directory');
|
|
760
|
+
}
|
|
761
|
+
if (!pending.some(file => file.output === entry)) {
|
|
762
|
+
throw new Error('The entry file did not map to an exact source file; pass --source-root with the correct source directory');
|
|
763
|
+
}
|
|
764
|
+
const instrumentedSites = pending.flatMap(file => file.sites)
|
|
765
|
+
.sort((left, right) => left.site_id.localeCompare(right.site_id));
|
|
766
|
+
if (selectiveInstrumentation) {
|
|
767
|
+
const matched = new Set(instrumentedSites.map(site => `${site.source_path}\0${site.qualified_name}`));
|
|
768
|
+
const unmatched = (options.includeSymbols ?? []).filter(selected => !matched.has(`${selected.sourcePath}\0${selected.qualifiedName}`));
|
|
769
|
+
if (unmatched.length > 0) {
|
|
770
|
+
throw new Error(`Telemetry exact symbol selection did not match: ${unmatched.map(selected => `${selected.sourcePath}#${selected.qualifiedName}`).join(', ')}`);
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
if (instrumentedSites.length > MAX_INSTRUMENTED_SITES) {
|
|
774
|
+
throw new Error(`Build contains ${instrumentedSites.length} telemetry sites; maximum is ${MAX_INSTRUMENTED_SITES}`);
|
|
775
|
+
}
|
|
776
|
+
const buildId = shortHash(pending.flatMap(file => [
|
|
777
|
+
normalizedPath(relative(dist, file.output)),
|
|
778
|
+
file.code,
|
|
779
|
+
]));
|
|
780
|
+
const runtimeGuardChannel = `${controlChannel}_runtimeGuard`;
|
|
781
|
+
const runtimeBlockedChannel = `${controlChannel}_runtimeBlocked`;
|
|
782
|
+
const bootstrap = bootstrapFor(entry, dist, buildId, authenticatedRuntimeHash, runtimeGuardChannel, runtimeBlockedChannel);
|
|
783
|
+
for (const file of pending) {
|
|
784
|
+
writeFileSync(file.output, file.output === entry ? insertEntryBootstrap(file.code, bootstrap) : file.code);
|
|
785
|
+
}
|
|
786
|
+
mkdirSync(runtimeDirectory, { recursive: true });
|
|
787
|
+
writeFileSync(join(runtimeDirectory, RUNTIME_FILENAME), authenticatedRuntime);
|
|
788
|
+
writeFileSync(join(runtimeDirectory, RUNTIME_ESM_FILENAME), esmRuntimeWrapper(authenticatedRuntimeHash));
|
|
789
|
+
const result = {
|
|
790
|
+
schema_version: MANIFEST_SCHEMA_VERSION,
|
|
791
|
+
build_id: buildId,
|
|
792
|
+
dist_directory: dist,
|
|
793
|
+
entry: normalizedPath(relative(dist, entry)),
|
|
794
|
+
runtime: `${RUNTIME_DIRECTORY}/${RUNTIME_FILENAME}`,
|
|
795
|
+
instrumented_files: pending.map(file => ({
|
|
796
|
+
output_path: normalizedPath(relative(dist, file.output)),
|
|
797
|
+
source_path: file.source,
|
|
798
|
+
probes: file.probes,
|
|
799
|
+
})),
|
|
800
|
+
sites: instrumentedSites,
|
|
801
|
+
skipped_unmapped_files: skipped,
|
|
802
|
+
already_instrumented: false,
|
|
803
|
+
};
|
|
804
|
+
writeFileSync(manifestPath, `${JSON.stringify(result, null, 2)}\n`);
|
|
805
|
+
return result;
|
|
806
|
+
}
|
|
807
|
+
export async function telemetryInstrumentCommand(distDirectory, options) {
|
|
808
|
+
let includeSymbols = options.includeSymbols;
|
|
809
|
+
if (options.includeSymbolsFile) {
|
|
810
|
+
const parsed = JSON.parse(readFileSync(resolve(options.includeSymbolsFile), 'utf8'));
|
|
811
|
+
const rows = Array.isArray(parsed) ? parsed : parsed?.symbols;
|
|
812
|
+
if (!Array.isArray(rows)) {
|
|
813
|
+
throw new Error('--include-symbols JSON must be an array or an object with a symbols array');
|
|
814
|
+
}
|
|
815
|
+
includeSymbols = rows;
|
|
816
|
+
}
|
|
817
|
+
const result = instrumentNodeTelemetryBuild(distDirectory, { ...options, includeSymbols });
|
|
818
|
+
if (options.json) {
|
|
819
|
+
console.log(JSON.stringify(result, null, 2));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
if (result.already_instrumented) {
|
|
823
|
+
console.log(chalk.dim(`Telemetry already instrumented (build ${result.build_id}).`));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const probes = result.instrumented_files.reduce((total, file) => total + file.probes.branches + file.probes.parameters + file.probes.bindings + file.probes.returns + file.probes.throws, 0);
|
|
827
|
+
console.log(chalk.green(`Instrumented ${result.instrumented_files.length} file(s) with ${probes} telemetry probes.`));
|
|
828
|
+
console.log(chalk.dim('No source imports were added. Set HAYSTACK_TELEMETRY=1, HAYSTACK_TELEMETRY_ENDPOINT, and HAYSTACK_TELEMETRY_TOKEN at runtime.'));
|
|
829
|
+
if (result.skipped_unmapped_files.length > 0) {
|
|
830
|
+
console.log(chalk.yellow(`Skipped ${result.skipped_unmapped_files.length} generated/unmapped file(s); see --json for exact paths.`));
|
|
831
|
+
}
|
|
832
|
+
}
|