autotel-cli 0.1.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/index.js +514 -8
- package/dist/index.js.map +1 -1
- package/package.json +23 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Jag Reehal 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -107,6 +107,47 @@ npx autotel add backend datadog --help
|
|
|
107
107
|
- `--dry-run` - Skip installation and print what would be done
|
|
108
108
|
- `--force` - Overwrite non-CLI-owned config (creates backup first)
|
|
109
109
|
|
|
110
|
+
### `autotel codemod trace <path>`
|
|
111
|
+
|
|
112
|
+
Wrap functions in `trace()` with a span name derived from the function/variable/method name. Use this to adopt autotel on existing code without changing function bodies.
|
|
113
|
+
|
|
114
|
+
**Supported file types:** `.ts`, `.tsx`, `.js`, `.jsx`
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Single file (TypeScript or JavaScript)
|
|
118
|
+
npx autotel codemod trace src/index.ts
|
|
119
|
+
npx autotel codemod trace src/utils.js
|
|
120
|
+
|
|
121
|
+
# Glob pattern - TypeScript only
|
|
122
|
+
npx autotel codemod trace "src/**/*.ts"
|
|
123
|
+
|
|
124
|
+
# Glob pattern - all supported files
|
|
125
|
+
npx autotel codemod trace "src/**/*.{ts,tsx,js,jsx}"
|
|
126
|
+
|
|
127
|
+
# Dry run - print what would change without writing
|
|
128
|
+
npx autotel codemod trace "src/**/*.ts" --dry-run
|
|
129
|
+
|
|
130
|
+
# Custom span name template: {name}, {file} (basename), {path} (relative)
|
|
131
|
+
npx autotel codemod trace "src/**/*.ts" --name-pattern "{file}.{name}"
|
|
132
|
+
|
|
133
|
+
# Skip functions whose name matches a regex (repeatable)
|
|
134
|
+
npx autotel codemod trace "src/**/*.ts" --skip "^_" --skip "test|mock"
|
|
135
|
+
|
|
136
|
+
# Print per-file summary (wrapped count, skipped)
|
|
137
|
+
npx autotel codemod trace "src/**/*.ts" --print-files
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**Options:**
|
|
141
|
+
|
|
142
|
+
- `--dry-run` - Print changes without writing files
|
|
143
|
+
- `--name-pattern <pattern>` - Span name template. Placeholders: `{name}`, `{file}`, `{path}`. Default: `{name}` only.
|
|
144
|
+
- `--skip <regex>...` - Skip functions whose name matches (repeatable; combined as OR).
|
|
145
|
+
- `--print-files` - Print per-file summary (e.g. `✔ path (N wrapped)`, `↷ path (skipped: reason)`).
|
|
146
|
+
|
|
147
|
+
**Supported patterns:** Function declarations, arrow/function expressions in `const`/`let`/`var`, class and static methods (body wrap), object method shorthand, named default export function. Works with both TypeScript and JavaScript files. Span name defaults to function/variable/method name (or `ClassName.methodName` for methods).
|
|
148
|
+
|
|
149
|
+
**Exclusions:** Generator functions (`function*`, `async function*`); getters/setters; constructors; CJS (`require('autotel')`); anonymous default export; class/object methods that use `super` in the body; `.d.ts` files; `node_modules/`. The codemod does not modify files when no eligible functions are found (no unused `trace` import added). The `{path}` placeholder uses the relative path from `--cwd` with forward slashes; moving files will change span names.
|
|
150
|
+
|
|
110
151
|
## Global Options
|
|
111
152
|
|
|
112
153
|
All commands support these options:
|
package/dist/index.js
CHANGED
|
@@ -362,7 +362,7 @@ function addPluginInit(file, init) {
|
|
|
362
362
|
function sortImports(imports) {
|
|
363
363
|
const sideEffect = [];
|
|
364
364
|
const external = [];
|
|
365
|
-
const
|
|
365
|
+
const relative5 = [];
|
|
366
366
|
for (const imp of imports) {
|
|
367
367
|
if (imp.sideEffect) {
|
|
368
368
|
if (imp.source === "autotel/register") {
|
|
@@ -371,15 +371,15 @@ function sortImports(imports) {
|
|
|
371
371
|
sideEffect.push(imp);
|
|
372
372
|
}
|
|
373
373
|
} else if (imp.source.startsWith(".") || imp.source.startsWith("/")) {
|
|
374
|
-
|
|
374
|
+
relative5.push(imp);
|
|
375
375
|
} else {
|
|
376
376
|
external.push(imp);
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
const sortBySource = (a, b) => a.source.localeCompare(b.source);
|
|
380
380
|
external.sort(sortBySource);
|
|
381
|
-
|
|
382
|
-
return [...sideEffect, ...external, ...
|
|
381
|
+
relative5.sort(sortBySource);
|
|
382
|
+
return [...sideEffect, ...external, ...relative5];
|
|
383
383
|
}
|
|
384
384
|
function renderImport(imp) {
|
|
385
385
|
if (imp.sideEffect) {
|
|
@@ -406,9 +406,7 @@ function renderImports(imports) {
|
|
|
406
406
|
return sorted.map(renderImport).join("\n");
|
|
407
407
|
}
|
|
408
408
|
function renderCodeFile(file) {
|
|
409
|
-
const lines = [];
|
|
410
|
-
lines.push(CLI_HEADER);
|
|
411
|
-
lines.push("");
|
|
409
|
+
const lines = [CLI_HEADER, ""];
|
|
412
410
|
const mainImports = sortImports(file.imports);
|
|
413
411
|
if (mainImports.length > 0) {
|
|
414
412
|
lines.push(renderImports(mainImports));
|
|
@@ -2188,6 +2186,97 @@ function checkEsmHook(project) {
|
|
|
2188
2186
|
};
|
|
2189
2187
|
}
|
|
2190
2188
|
|
|
2189
|
+
// src/lib/logger-checker.ts
|
|
2190
|
+
import * as path8 from "path";
|
|
2191
|
+
import * as fs2 from "fs";
|
|
2192
|
+
var LOGGER_INSTRUMENTATION = {
|
|
2193
|
+
winston: "@opentelemetry/instrumentation-winston",
|
|
2194
|
+
bunyan: "@opentelemetry/instrumentation-bunyan",
|
|
2195
|
+
pino: "@opentelemetry/instrumentation-pino"
|
|
2196
|
+
};
|
|
2197
|
+
function extractAutoInstrumentations(content) {
|
|
2198
|
+
const instrumentations = [];
|
|
2199
|
+
const arrayPattern = /autoInstrumentations\s*:\s*\[(.*?)\]/s;
|
|
2200
|
+
const arrayMatch = content.match(arrayPattern);
|
|
2201
|
+
if (arrayMatch && arrayMatch[1]) {
|
|
2202
|
+
const items = arrayMatch[1].split(",").map((item) => item.trim().replaceAll(/['"]/g, "")).filter((item) => item.length > 0);
|
|
2203
|
+
instrumentations.push(...items);
|
|
2204
|
+
}
|
|
2205
|
+
const objectPattern = /autoInstrumentations\s*:\s*\{([^}]+)\}/s;
|
|
2206
|
+
const objectMatch = content.match(objectPattern);
|
|
2207
|
+
if (objectMatch && objectMatch[1]) {
|
|
2208
|
+
const props = objectMatch[1];
|
|
2209
|
+
const enabledPattern = /(\w+)\s*:\s*\{[^}]*enabled\s*:\s*true[^}]*\}/g;
|
|
2210
|
+
let enabledMatch;
|
|
2211
|
+
while ((enabledMatch = enabledPattern.exec(props)) !== null) {
|
|
2212
|
+
if (enabledMatch[1]) {
|
|
2213
|
+
instrumentations.push(enabledMatch[1]);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
return [...new Set(instrumentations)];
|
|
2218
|
+
}
|
|
2219
|
+
function findSourceFiles(packageRoot) {
|
|
2220
|
+
const sourceFiles = [];
|
|
2221
|
+
const srcDir = path8.join(packageRoot, "src");
|
|
2222
|
+
const dirsToCheck = [packageRoot, srcDir].filter(
|
|
2223
|
+
(dir) => fs2.existsSync(dir) && fs2.statSync(dir).isDirectory()
|
|
2224
|
+
);
|
|
2225
|
+
for (const dir of dirsToCheck) {
|
|
2226
|
+
const files = fs2.readdirSync(dir, { recursive: true });
|
|
2227
|
+
for (const file of files) {
|
|
2228
|
+
if (typeof file !== "string") continue;
|
|
2229
|
+
const filePath = path8.join(dir, file);
|
|
2230
|
+
try {
|
|
2231
|
+
if (fs2.statSync(filePath).isFile() && /\.(ts|js|mts|mjs|tsx|jsx)$/.test(file)) {
|
|
2232
|
+
sourceFiles.push(filePath);
|
|
2233
|
+
}
|
|
2234
|
+
} catch {
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return sourceFiles;
|
|
2239
|
+
}
|
|
2240
|
+
function checkLoggerInstrumentation(packageRoot, deps) {
|
|
2241
|
+
const hasWinston = !!deps["winston"];
|
|
2242
|
+
const hasBunyan = !!deps["bunyan"];
|
|
2243
|
+
const hasPino = !!deps["pino"];
|
|
2244
|
+
let logger = null;
|
|
2245
|
+
if (hasWinston) logger = "winston";
|
|
2246
|
+
else if (hasBunyan) logger = "bunyan";
|
|
2247
|
+
else if (hasPino) logger = "pino";
|
|
2248
|
+
if (!logger) {
|
|
2249
|
+
return {
|
|
2250
|
+
logger: null,
|
|
2251
|
+
hasLogger: false,
|
|
2252
|
+
hasInstrumentation: false,
|
|
2253
|
+
configuredInCode: false,
|
|
2254
|
+
instrumentationPackage: null
|
|
2255
|
+
};
|
|
2256
|
+
}
|
|
2257
|
+
const instrumentationPackage = LOGGER_INSTRUMENTATION[logger];
|
|
2258
|
+
const hasInstrumentation = !!deps[instrumentationPackage];
|
|
2259
|
+
const sourceFiles = findSourceFiles(packageRoot);
|
|
2260
|
+
let configuredInCode = false;
|
|
2261
|
+
for (const filePath of sourceFiles) {
|
|
2262
|
+
const content = readFileSafe(filePath);
|
|
2263
|
+
if (!content) continue;
|
|
2264
|
+
if (!content.includes("init(")) continue;
|
|
2265
|
+
const instrumentations = extractAutoInstrumentations(content);
|
|
2266
|
+
if (instrumentations.includes(logger)) {
|
|
2267
|
+
configuredInCode = true;
|
|
2268
|
+
break;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
return {
|
|
2272
|
+
logger,
|
|
2273
|
+
hasLogger: true,
|
|
2274
|
+
hasInstrumentation,
|
|
2275
|
+
configuredInCode,
|
|
2276
|
+
instrumentationPackage
|
|
2277
|
+
};
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2191
2280
|
// src/commands/doctor.ts
|
|
2192
2281
|
var CHECK_DEFINITIONS = [
|
|
2193
2282
|
{ id: "autotel-installed", title: "Autotel installed", description: "Check if autotel is installed" },
|
|
@@ -2195,7 +2284,8 @@ var CHECK_DEFINITIONS = [
|
|
|
2195
2284
|
{ id: "esm-hook", title: "ESM hook setup", description: "Check if autotel/register is imported correctly" },
|
|
2196
2285
|
{ id: "env-vars", title: "Environment variables", description: "Check if required env vars are present" },
|
|
2197
2286
|
{ id: "version-compat", title: "Version compatibility", description: "Check autotel package versions match" },
|
|
2198
|
-
{ id: "config-found", title: "Configuration found", description: "Check if instrumentation config exists" }
|
|
2287
|
+
{ id: "config-found", title: "Configuration found", description: "Check if instrumentation config exists" },
|
|
2288
|
+
{ id: "logger-instrumentation", title: "Logger instrumentation", description: "Check if logger instrumentation packages are installed" }
|
|
2199
2289
|
];
|
|
2200
2290
|
async function inferBackend(packageRoot, deps) {
|
|
2201
2291
|
if (deps["autotel-backends"]) {
|
|
@@ -2423,6 +2513,61 @@ async function runChecks(options, projectRoot) {
|
|
|
2423
2513
|
message: esmCheck.message,
|
|
2424
2514
|
details: esmCheck.details
|
|
2425
2515
|
});
|
|
2516
|
+
const loggerCheck = checkLoggerInstrumentation(project.packageRoot, deps);
|
|
2517
|
+
if (loggerCheck.hasLogger) {
|
|
2518
|
+
if (loggerCheck.configuredInCode && !loggerCheck.hasInstrumentation) {
|
|
2519
|
+
checks.push({
|
|
2520
|
+
id: "logger-instrumentation",
|
|
2521
|
+
title: "Logger instrumentation",
|
|
2522
|
+
level: "warning",
|
|
2523
|
+
status: "warn",
|
|
2524
|
+
message: `${loggerCheck.logger} is configured but instrumentation package is missing`,
|
|
2525
|
+
details: [
|
|
2526
|
+
`${loggerCheck.logger} is used in autoInstrumentations but ${loggerCheck.instrumentationPackage} is not installed`,
|
|
2527
|
+
`Install it: ${getInstallCommand(project.packageManager, [loggerCheck.instrumentationPackage])}`
|
|
2528
|
+
],
|
|
2529
|
+
fix: {
|
|
2530
|
+
cmd: getInstallCommand(project.packageManager, [loggerCheck.instrumentationPackage]),
|
|
2531
|
+
description: `Install ${loggerCheck.instrumentationPackage}`
|
|
2532
|
+
}
|
|
2533
|
+
});
|
|
2534
|
+
} else if (loggerCheck.hasInstrumentation && loggerCheck.configuredInCode) {
|
|
2535
|
+
checks.push({
|
|
2536
|
+
id: "logger-instrumentation",
|
|
2537
|
+
title: "Logger instrumentation",
|
|
2538
|
+
level: "info",
|
|
2539
|
+
status: "ok",
|
|
2540
|
+
message: `${loggerCheck.logger} instrumentation is properly configured`
|
|
2541
|
+
});
|
|
2542
|
+
} else if (loggerCheck.hasInstrumentation && !loggerCheck.configuredInCode) {
|
|
2543
|
+
checks.push({
|
|
2544
|
+
id: "logger-instrumentation",
|
|
2545
|
+
title: "Logger instrumentation",
|
|
2546
|
+
level: "info",
|
|
2547
|
+
status: "ok",
|
|
2548
|
+
message: `${loggerCheck.logger} instrumentation package is installed`,
|
|
2549
|
+
details: [
|
|
2550
|
+
`Add '${loggerCheck.logger}' to autoInstrumentations in your init() call to enable trace context injection`
|
|
2551
|
+
]
|
|
2552
|
+
});
|
|
2553
|
+
} else {
|
|
2554
|
+
checks.push({
|
|
2555
|
+
id: "logger-instrumentation",
|
|
2556
|
+
title: "Logger instrumentation",
|
|
2557
|
+
level: "info",
|
|
2558
|
+
status: "skip",
|
|
2559
|
+
message: `${loggerCheck.logger} is installed but not configured in code`
|
|
2560
|
+
});
|
|
2561
|
+
}
|
|
2562
|
+
} else {
|
|
2563
|
+
checks.push({
|
|
2564
|
+
id: "logger-instrumentation",
|
|
2565
|
+
title: "Logger instrumentation",
|
|
2566
|
+
level: "info",
|
|
2567
|
+
status: "skip",
|
|
2568
|
+
message: "No logger packages detected (winston, bunyan, pino)"
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2426
2571
|
return checks;
|
|
2427
2572
|
}
|
|
2428
2573
|
function calculateSummary(checks) {
|
|
@@ -2806,6 +2951,346 @@ ${presetType} presets:
|
|
|
2806
2951
|
}
|
|
2807
2952
|
}
|
|
2808
2953
|
|
|
2954
|
+
// src/commands/codemod-trace.ts
|
|
2955
|
+
import * as path10 from "path";
|
|
2956
|
+
import { glob as glob2 } from "glob";
|
|
2957
|
+
|
|
2958
|
+
// src/lib/codemod-trace.ts
|
|
2959
|
+
import * as path9 from "path";
|
|
2960
|
+
import { Project, SyntaxKind } from "ts-morph";
|
|
2961
|
+
var TRACE_IMPORT_MODULE = "autotel";
|
|
2962
|
+
function hasTraceImport(sourceFile) {
|
|
2963
|
+
for (const imp of sourceFile.getImportDeclarations()) {
|
|
2964
|
+
if (imp.getModuleSpecifierValue() !== TRACE_IMPORT_MODULE) continue;
|
|
2965
|
+
for (const spec of imp.getNamedImports()) {
|
|
2966
|
+
const name = spec.getName();
|
|
2967
|
+
if (name === "trace") return true;
|
|
2968
|
+
const alias = spec.getAliasNode()?.getText();
|
|
2969
|
+
if (alias === "trace" || name === "trace") return true;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
return false;
|
|
2973
|
+
}
|
|
2974
|
+
function addTraceImport(sourceFile) {
|
|
2975
|
+
if (hasTraceImport(sourceFile)) return;
|
|
2976
|
+
sourceFile.insertImportDeclaration(0, {
|
|
2977
|
+
moduleSpecifier: TRACE_IMPORT_MODULE,
|
|
2978
|
+
namedImports: ["trace"]
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
function expandNamePattern(pattern, name, filePath, cwd) {
|
|
2982
|
+
const file = path9.basename(filePath, path9.extname(filePath));
|
|
2983
|
+
const relPath = path9.relative(cwd, filePath).replaceAll("\\", "/");
|
|
2984
|
+
return pattern.replaceAll("{name}", name).replaceAll("{file}", file).replaceAll("{path}", relPath);
|
|
2985
|
+
}
|
|
2986
|
+
function getSpanName(name, filePath, options) {
|
|
2987
|
+
if (options.namePattern) {
|
|
2988
|
+
return expandNamePattern(options.namePattern, name, filePath, process.cwd());
|
|
2989
|
+
}
|
|
2990
|
+
return name;
|
|
2991
|
+
}
|
|
2992
|
+
function shouldSkipName(name, options) {
|
|
2993
|
+
if (!options.skip?.length) return false;
|
|
2994
|
+
return options.skip.some((re) => re.test(name));
|
|
2995
|
+
}
|
|
2996
|
+
function isAlreadyWrapped(node) {
|
|
2997
|
+
const text = node.getText();
|
|
2998
|
+
const trimmed = text.trimStart();
|
|
2999
|
+
return trimmed.startsWith("trace(");
|
|
3000
|
+
}
|
|
3001
|
+
function bodyContainsSuper(body) {
|
|
3002
|
+
let found = false;
|
|
3003
|
+
body.forEachDescendant((desc) => {
|
|
3004
|
+
if (desc.getKind() === SyntaxKind.SuperKeyword) found = true;
|
|
3005
|
+
});
|
|
3006
|
+
return found;
|
|
3007
|
+
}
|
|
3008
|
+
function isGenerator(method) {
|
|
3009
|
+
return method.isGenerator?.() ?? false;
|
|
3010
|
+
}
|
|
3011
|
+
function isInsideTraceCall(node) {
|
|
3012
|
+
let current = node.getParent();
|
|
3013
|
+
while (current) {
|
|
3014
|
+
if (current.getKind() === SyntaxKind.CallExpression) {
|
|
3015
|
+
const expr = current.getExpression?.();
|
|
3016
|
+
if (expr?.getText() === "trace") return true;
|
|
3017
|
+
}
|
|
3018
|
+
current = current.getParent();
|
|
3019
|
+
}
|
|
3020
|
+
return false;
|
|
3021
|
+
}
|
|
3022
|
+
function transformFile(content, filePath, options) {
|
|
3023
|
+
const skipped = [];
|
|
3024
|
+
let wrappedCount = 0;
|
|
3025
|
+
const project = new Project({ useInMemoryFileSystem: true });
|
|
3026
|
+
const sourceFile = project.createSourceFile(filePath, content);
|
|
3027
|
+
const edits = [];
|
|
3028
|
+
function skip(name, reason) {
|
|
3029
|
+
skipped.push({ name, reason });
|
|
3030
|
+
return true;
|
|
3031
|
+
}
|
|
3032
|
+
const defaultDecls = sourceFile.getExportedDeclarations().get("default");
|
|
3033
|
+
const defaultFn = defaultDecls?.[0] && (defaultDecls[0].getKind() === SyntaxKind.FunctionDeclaration || defaultDecls[0].getKind() === SyntaxKind.FunctionExpression) ? defaultDecls[0] : void 0;
|
|
3034
|
+
const allFns = sourceFile.getFunctions().filter((f) => !f.isOverload());
|
|
3035
|
+
const onlyDefaultExportFn = defaultFn && allFns.length === 1 && (allFns[0] === defaultFn || allFns[0].getStart() === defaultFn.getStart());
|
|
3036
|
+
for (const fn of allFns) {
|
|
3037
|
+
if (onlyDefaultExportFn) continue;
|
|
3038
|
+
if (isInsideTraceCall(fn)) continue;
|
|
3039
|
+
const name = fn.getName();
|
|
3040
|
+
if (!name) continue;
|
|
3041
|
+
const spanName = getSpanName(name, filePath, options);
|
|
3042
|
+
if (shouldSkipName(spanName, options)) {
|
|
3043
|
+
skip(spanName, "name match");
|
|
3044
|
+
continue;
|
|
3045
|
+
}
|
|
3046
|
+
if (isAlreadyWrapped(fn)) {
|
|
3047
|
+
skip(spanName, "already wrapped");
|
|
3048
|
+
continue;
|
|
3049
|
+
}
|
|
3050
|
+
const mod = fn.getModifiers().map((m) => m.getText()).join(" ");
|
|
3051
|
+
const modPrefix = mod ? mod + " " : "";
|
|
3052
|
+
const fnText = fn.getText();
|
|
3053
|
+
const rest = fnText.replace(/^function\s*\w*\s*/, "");
|
|
3054
|
+
const newText = `${modPrefix}const ${name} = trace('${spanName}', function ${name}${rest};`;
|
|
3055
|
+
edits.push({ node: fn, newText });
|
|
3056
|
+
wrappedCount += 1;
|
|
3057
|
+
}
|
|
3058
|
+
if (defaultFn) {
|
|
3059
|
+
const name = defaultFn.getName?.();
|
|
3060
|
+
if (!name) {
|
|
3061
|
+
skip("(default export)", "anonymous default export");
|
|
3062
|
+
} else {
|
|
3063
|
+
const spanName = getSpanName(name, filePath, options);
|
|
3064
|
+
if (shouldSkipName(spanName, options)) {
|
|
3065
|
+
skip(spanName, "name match");
|
|
3066
|
+
} else {
|
|
3067
|
+
const fn = defaultFn;
|
|
3068
|
+
const params = fn.getParameters?.() ?? [];
|
|
3069
|
+
const paramsText = params.map((p) => p.getText()).join(", ");
|
|
3070
|
+
const body = fn.getBody?.();
|
|
3071
|
+
const bodyText = body?.getText() ?? "{}";
|
|
3072
|
+
const decl = `const ${name} = trace('${spanName}', function ${name}(${paramsText}) ${bodyText};`;
|
|
3073
|
+
const full = decl + "\nexport default " + name + ";";
|
|
3074
|
+
edits.push({ node: defaultFn, newText: full });
|
|
3075
|
+
wrappedCount += 1;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
3080
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
3081
|
+
const init = decl.getInitializer();
|
|
3082
|
+
if (!init) continue;
|
|
3083
|
+
const kind = init.getKind();
|
|
3084
|
+
const isArrow = kind === SyntaxKind.ArrowFunction;
|
|
3085
|
+
const isFnExpr = kind === SyntaxKind.FunctionExpression;
|
|
3086
|
+
const isCall = kind === SyntaxKind.CallExpression;
|
|
3087
|
+
if (isCall) {
|
|
3088
|
+
const callExpr = init;
|
|
3089
|
+
const exprText = callExpr.getExpression().getText();
|
|
3090
|
+
if (exprText === "trace") {
|
|
3091
|
+
const name2 = decl.getName();
|
|
3092
|
+
if (typeof name2 === "string") {
|
|
3093
|
+
const spanName2 = getSpanName(name2, filePath, options);
|
|
3094
|
+
skipped.push({ name: spanName2, reason: "already wrapped" });
|
|
3095
|
+
}
|
|
3096
|
+
continue;
|
|
3097
|
+
}
|
|
3098
|
+
}
|
|
3099
|
+
if (!isArrow && !isFnExpr) continue;
|
|
3100
|
+
const name = decl.getName();
|
|
3101
|
+
if (typeof name !== "string") continue;
|
|
3102
|
+
const spanName = getSpanName(name, filePath, options);
|
|
3103
|
+
if (shouldSkipName(spanName, options)) {
|
|
3104
|
+
skip(spanName, "name match");
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (isAlreadyWrapped(init)) {
|
|
3108
|
+
skipped.push({ name: spanName, reason: "already wrapped" });
|
|
3109
|
+
continue;
|
|
3110
|
+
}
|
|
3111
|
+
const initText = init.getText();
|
|
3112
|
+
const newInit = `trace('${spanName}', ${initText})`;
|
|
3113
|
+
edits.push({ node: init, newText: newInit });
|
|
3114
|
+
wrappedCount += 1;
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
for (const clazz of sourceFile.getClasses()) {
|
|
3118
|
+
const className = clazz.getName();
|
|
3119
|
+
if (!className) continue;
|
|
3120
|
+
for (const _ctor of clazz.getConstructors()) {
|
|
3121
|
+
skipped.push({ name: `${className}.constructor`, reason: "constructor" });
|
|
3122
|
+
}
|
|
3123
|
+
for (const method of clazz.getMethods()) {
|
|
3124
|
+
const getter = method.getFirstChildByKind(SyntaxKind.GetKeyword);
|
|
3125
|
+
const setter = method.getFirstChildByKind(SyntaxKind.SetKeyword);
|
|
3126
|
+
if (getter || setter) {
|
|
3127
|
+
skip(method.getName() ?? "(getter/setter)", "getter/setter");
|
|
3128
|
+
continue;
|
|
3129
|
+
}
|
|
3130
|
+
if (isGenerator(method)) {
|
|
3131
|
+
skip(`${className}.${method.getName()}`, "generator");
|
|
3132
|
+
continue;
|
|
3133
|
+
}
|
|
3134
|
+
const methodName = method.getName();
|
|
3135
|
+
const spanName = getSpanName(`${className}.${methodName}`, filePath, options);
|
|
3136
|
+
if (shouldSkipName(spanName, options)) {
|
|
3137
|
+
skip(spanName, "name match");
|
|
3138
|
+
continue;
|
|
3139
|
+
}
|
|
3140
|
+
const body = method.getBody();
|
|
3141
|
+
if (!body) continue;
|
|
3142
|
+
if (bodyContainsSuper(body)) {
|
|
3143
|
+
skip(spanName, "super");
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
const bodyText = body.getText();
|
|
3147
|
+
const innerBody = bodyText.slice(1, -1).trim();
|
|
3148
|
+
const isAsync = method.isAsync();
|
|
3149
|
+
const prefix = isAsync ? "async " : "";
|
|
3150
|
+
const newBody = `{
|
|
3151
|
+
return trace('${spanName}', ${prefix}() => {
|
|
3152
|
+
${innerBody}
|
|
3153
|
+
})();
|
|
3154
|
+
}`;
|
|
3155
|
+
edits.push({ node: body, newText: newBody });
|
|
3156
|
+
wrappedCount += 1;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
3160
|
+
for (const decl of stmt.getDeclarationList().getDeclarations()) {
|
|
3161
|
+
const init = decl.getInitializer();
|
|
3162
|
+
if (!init || init.getKind() !== SyntaxKind.ObjectLiteralExpression) continue;
|
|
3163
|
+
const obj = init;
|
|
3164
|
+
for (const prop of obj.getProperties()) {
|
|
3165
|
+
if (prop.getKind() !== SyntaxKind.MethodDeclaration) continue;
|
|
3166
|
+
const method = prop;
|
|
3167
|
+
const methodName = method.getName();
|
|
3168
|
+
const spanName = getSpanName(methodName, filePath, options);
|
|
3169
|
+
if (shouldSkipName(spanName, options)) {
|
|
3170
|
+
skip(spanName, "name match");
|
|
3171
|
+
continue;
|
|
3172
|
+
}
|
|
3173
|
+
if (method.isGenerator?.()) {
|
|
3174
|
+
skip(spanName, "generator");
|
|
3175
|
+
continue;
|
|
3176
|
+
}
|
|
3177
|
+
const body = method.getBody();
|
|
3178
|
+
if (!body) continue;
|
|
3179
|
+
if (bodyContainsSuper(body)) {
|
|
3180
|
+
skip(spanName, "super");
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
const bodyText = body.getText();
|
|
3184
|
+
const innerBody = bodyText.slice(1, -1).trim();
|
|
3185
|
+
const newBody = `{
|
|
3186
|
+
return trace('${spanName}', () => {
|
|
3187
|
+
${innerBody}
|
|
3188
|
+
})();
|
|
3189
|
+
}`;
|
|
3190
|
+
edits.push({ node: body, newText: newBody });
|
|
3191
|
+
wrappedCount += 1;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
edits.sort((a, b) => b.node.getStart() - a.node.getStart());
|
|
3196
|
+
for (const { node, newText } of edits) {
|
|
3197
|
+
node.replaceWithText(newText);
|
|
3198
|
+
}
|
|
3199
|
+
if (wrappedCount > 0 && !hasTraceImport(sourceFile)) {
|
|
3200
|
+
addTraceImport(sourceFile);
|
|
3201
|
+
}
|
|
3202
|
+
const modified = sourceFile.getFullText();
|
|
3203
|
+
const changed = wrappedCount > 0;
|
|
3204
|
+
return {
|
|
3205
|
+
modified: changed ? modified : content,
|
|
3206
|
+
changed,
|
|
3207
|
+
wrappedCount,
|
|
3208
|
+
skipped
|
|
3209
|
+
};
|
|
3210
|
+
}
|
|
3211
|
+
|
|
3212
|
+
// src/commands/codemod-trace.ts
|
|
3213
|
+
var CODEMOD_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
3214
|
+
var GLOB_META = /[*?[\]]/;
|
|
3215
|
+
async function resolveCodemodFiles(pathArg, cwd) {
|
|
3216
|
+
const isGlob = GLOB_META.test(pathArg);
|
|
3217
|
+
if (!isGlob) {
|
|
3218
|
+
const absolute = path10.isAbsolute(pathArg) ? pathArg : path10.resolve(cwd, pathArg);
|
|
3219
|
+
if (fileExists(absolute)) {
|
|
3220
|
+
const ext = path10.extname(absolute);
|
|
3221
|
+
if (CODEMOD_EXTENSIONS.has(ext) && !absolute.endsWith(".d.ts")) {
|
|
3222
|
+
return [absolute];
|
|
3223
|
+
}
|
|
3224
|
+
return [];
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
const pattern = path10.isAbsolute(pathArg) ? pathArg : path10.join(cwd, pathArg);
|
|
3228
|
+
const matches = await glob2(pattern, {
|
|
3229
|
+
cwd,
|
|
3230
|
+
absolute: true,
|
|
3231
|
+
ignore: ["**/node_modules/**", "**/*.d.ts"]
|
|
3232
|
+
});
|
|
3233
|
+
return matches.filter((f) => {
|
|
3234
|
+
const ext = path10.extname(f);
|
|
3235
|
+
return CODEMOD_EXTENSIONS.has(ext) && !f.endsWith(".d.ts");
|
|
3236
|
+
});
|
|
3237
|
+
}
|
|
3238
|
+
async function runCodemodTrace(options) {
|
|
3239
|
+
const { path: pathArg, cwd, dryRun, namePattern, skip, printFiles, verbose: verbose2, quiet } = options;
|
|
3240
|
+
const files = await resolveCodemodFiles(pathArg, cwd);
|
|
3241
|
+
if (files.length === 0) {
|
|
3242
|
+
if (!quiet) {
|
|
3243
|
+
error(`No matching files found for: ${pathArg}`);
|
|
3244
|
+
}
|
|
3245
|
+
process.exitCode = 1;
|
|
3246
|
+
return;
|
|
3247
|
+
}
|
|
3248
|
+
const skipRegExps = skip?.map((s) => new RegExp(s)) ?? [];
|
|
3249
|
+
const transformOptions = { namePattern, skip: skipRegExps.length > 0 ? skipRegExps : void 0 };
|
|
3250
|
+
let totalWrapped = 0;
|
|
3251
|
+
let totalChanged = 0;
|
|
3252
|
+
for (const filePath of files) {
|
|
3253
|
+
const content = readFileSafe(filePath);
|
|
3254
|
+
if (content === null) {
|
|
3255
|
+
if (verbose2) dim(`Skip ${filePath} (unreadable)`);
|
|
3256
|
+
continue;
|
|
3257
|
+
}
|
|
3258
|
+
let result;
|
|
3259
|
+
try {
|
|
3260
|
+
result = transformFile(content, filePath, transformOptions);
|
|
3261
|
+
} catch (error2) {
|
|
3262
|
+
if (!quiet) {
|
|
3263
|
+
error(`Failed to transform ${filePath}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
3264
|
+
}
|
|
3265
|
+
if (verbose2 && error2 instanceof Error && error2.stack) dim(error2.stack);
|
|
3266
|
+
process.exitCode = 1;
|
|
3267
|
+
continue;
|
|
3268
|
+
}
|
|
3269
|
+
const relativePath = path10.relative(cwd, filePath);
|
|
3270
|
+
if (result.changed) {
|
|
3271
|
+
totalWrapped += result.wrappedCount;
|
|
3272
|
+
totalChanged += 1;
|
|
3273
|
+
if (!dryRun) {
|
|
3274
|
+
const fs3 = await import("fs");
|
|
3275
|
+
fs3.writeFileSync(filePath, result.modified, "utf8");
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const showSummary = printFiles || dryRun || result.changed;
|
|
3279
|
+
if (showSummary && !quiet) {
|
|
3280
|
+
if (result.changed) {
|
|
3281
|
+
console.log(`\u2714 ${relativePath} (${result.wrappedCount} wrapped)`);
|
|
3282
|
+
} else if (result.skipped.length > 0) {
|
|
3283
|
+
const reasons = [...new Set(result.skipped.map((s) => s.reason))].join("; ");
|
|
3284
|
+
console.log(`\u21B7 ${relativePath} (skipped: ${reasons})`);
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
if (dryRun && totalChanged > 0 && !quiet) {
|
|
3289
|
+
console.log("");
|
|
3290
|
+
dim(`Dry run: ${totalChanged} file(s) would be updated, ${totalWrapped} function(s) wrapped.`);
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
|
|
2809
3294
|
// src/cli.ts
|
|
2810
3295
|
function createProgram() {
|
|
2811
3296
|
const program = new Command();
|
|
@@ -2874,6 +3359,27 @@ function createProgram() {
|
|
|
2874
3359
|
});
|
|
2875
3360
|
addGlobalOptions(addCmd);
|
|
2876
3361
|
program.addCommand(addCmd);
|
|
3362
|
+
const codemodCmd = new Command("codemod").description("Codemod commands for adopting autotel");
|
|
3363
|
+
const traceCmd = new Command("trace").description("Wrap functions in trace() with span name from function/variable/method name").argument("<path>", "File path or glob (e.g. src/index.ts, src/**/*.ts)").option("--dry-run", "Print changes without writing files").option("--name-pattern <pattern>", "Span name template: {name}, {file}, {path}").option("--skip <regex>...", "Skip functions whose name matches (repeatable)").option("--print-files", "Print per-file summary (wrapped count, skipped)").action(async (pathArg, opts) => {
|
|
3364
|
+
const options = {
|
|
3365
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
3366
|
+
dryRun: opts.dryRun ?? false,
|
|
3367
|
+
noInstall: false,
|
|
3368
|
+
printInstallCmd: false,
|
|
3369
|
+
verbose: opts.verbose ?? false,
|
|
3370
|
+
quiet: opts.quiet ?? false,
|
|
3371
|
+
workspaceRoot: false,
|
|
3372
|
+
path: pathArg,
|
|
3373
|
+
namePattern: opts.namePattern,
|
|
3374
|
+
skip: Array.isArray(opts.skip) && opts.skip.length > 0 ? opts.skip : void 0,
|
|
3375
|
+
printFiles: opts.printFiles ?? false
|
|
3376
|
+
};
|
|
3377
|
+
await runCodemodTrace(options);
|
|
3378
|
+
});
|
|
3379
|
+
addGlobalOptions(traceCmd);
|
|
3380
|
+
codemodCmd.addCommand(traceCmd);
|
|
3381
|
+
addGlobalOptions(codemodCmd);
|
|
3382
|
+
program.addCommand(codemodCmd);
|
|
2877
3383
|
return program;
|
|
2878
3384
|
}
|
|
2879
3385
|
async function run() {
|