Skip to content

Commit 06d4ff4

Browse files
authored
feat(telemetry): add performance instrumentation and CLI Performance dashboard (#625)
## Summary - Add tracing spans and timing attributes for key performance-sensitive operations - Remove redundant distribution metrics that duplicate span attributes - Create a [CLI Performance dashboard](https://sentry.sentry.io/dashboard/2335804/) with 18 widgets ## Instrumentation Changes | Change | File | Details | |--------|------|---------| | Target resolution span | `resolve-target.ts` | Wraps `resolveOrgAndProject()` and `resolveAllTargets()` in `resolve` spans with `resolve.method` (`flags`/`env_vars`/`defaults`/`dsn`/`inference`/`none`) and `resolve.cache_hit` attributes | | Env file scanning span | `dsn/env-file.ts` | Wraps `detectFromEnvFiles()` and `detectFromAllEnvFiles()` in `dsn.detect.env` spans with `dsn.env_files_checked` and `dsn.env_dsn_found` attributes | | Command phase timing | `command.ts` | Adds `phase.pre_ms`, `phase.exec_ms`, `phase.render_ms` attributes to the root `cli.command` span via `performance.now()` markers | | Completion result count | `telemetry.ts` | Emits `completion.result_count` distribution metric from the deferred telemetry queue | | Remove redundant metrics | `dsn/code-scanner.ts` | Removes `dsn.files_collected`, `dsn.files_scanned`, `dsn.dsns_found` distribution metrics — span attributes on `dsn.detect.code` already capture identical data | ## Dashboard Layout (18 widgets, 10 rows) | Row | Left (w=3) | Right (w=3) | |-----|------------|-------------| | 1 | Command Duration (p50/p75/p90) | Command Volume by Runtime | | 2 | Duration by Command (full w=6) | | | 3 | Completion Latency (p50/p75/p90) | Completion by Command (p75) | | 4 | Target Resolution (p50/p75/p90) | Resolution by Method | | 5 | Command Phase Breakdown (full w=6) | | | 6 | DSN Detection Duration (p75) | File Scan Volume (avg) | | 7 | Project Root Detection | HTTP Cache Hit/Miss | | 8 | API Response Time (p50/p90) | API Calls per Command | | 9 | CPU & Event Loop Utilization | Memory Usage | | 10 | Seer AI Outcomes | Delta Upgrade Stats |
1 parent 7ce9bdf commit 06d4ff4

File tree

5 files changed

+260
-203
lines changed

5 files changed

+260
-203
lines changed

src/lib/command.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ import {
6060
parseLogLevel,
6161
setLogLevel,
6262
} from "./logger.js";
63-
import { setArgsContext, setFlagContext } from "./telemetry.js";
63+
import { setArgsContext, setFlagContext, withTracing } from "./telemetry.js";
6464

6565
/**
6666
* Parse a string input as a number.
@@ -586,20 +586,29 @@ export function buildCommand<
586586
throw new AuthError("not_authenticated");
587587
}
588588

589-
const generator = originalFunc.call(
590-
this,
591-
cleanFlags as FLAGS,
592-
...(args as unknown as ARGS)
589+
// Execution phase: core command logic, API calls, org/project resolution
590+
const returned = await withTracing(
591+
"exec",
592+
"cli.command.exec",
593+
async () => {
594+
const generator = originalFunc.call(
595+
this,
596+
cleanFlags as FLAGS,
597+
...(args as unknown as ARGS)
598+
);
599+
let result = await generator.next();
600+
while (!result.done) {
601+
handleYieldedValue(stdout, result.value, cleanFlags, renderer);
602+
result = await generator.next();
603+
}
604+
return result.value as CommandReturn | undefined;
605+
}
593606
);
594-
let result = await generator.next();
595-
while (!result.done) {
596-
handleYieldedValue(stdout, result.value, cleanFlags, renderer);
597-
result = await generator.next();
598-
}
599607

600-
// Generator completed successfully — finalize with hint.
601-
const returned = result.value as CommandReturn | undefined;
602-
writeFinalization(stdout, returned?.hint, cleanFlags.json, renderer);
608+
// Render phase: output finalization
609+
await withTracing("render", "cli.command.render", () => {
610+
writeFinalization(stdout, returned?.hint, cleanFlags.json, renderer);
611+
});
603612
} catch (err) {
604613
// Finalize before error handling to close streaming state
605614
// (e.g., table footer). No hint since the generator didn't

src/lib/dsn/code-scanner.ts

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@
1515
import type { Dirent } from "node:fs";
1616
import { readdir, stat } from "node:fs/promises";
1717
import path from "node:path";
18-
// biome-ignore lint/performance/noNamespaceImport: Sentry SDK recommends namespace import
19-
import * as Sentry from "@sentry/node-core/light";
2018
import ignore, { type Ignore } from "ignore";
2119
import pLimit from "p-limit";
2220
import { DEFAULT_SENTRY_HOST, getConfiguredSentryUrl } from "../constants.js";
@@ -689,9 +687,6 @@ function scanDirectory(
689687
const { files, dirMtimes } = collectResult;
690688

691689
span.setAttribute("dsn.files_collected", files.length);
692-
Sentry.metrics.distribution("dsn.files_collected", files.length, {
693-
attributes: { stop_on_first: stopOnFirst },
694-
});
695690

696691
if (files.length === 0) {
697692
return { dsns: [], sourceMtimes: {}, dirMtimes };
@@ -709,13 +704,6 @@ function scanDirectory(
709704
"dsn.dsns_found": results.size,
710705
});
711706

712-
Sentry.metrics.distribution("dsn.files_scanned", filesScanned, {
713-
attributes: { stop_on_first: stopOnFirst },
714-
});
715-
Sentry.metrics.distribution("dsn.dsns_found", results.size, {
716-
attributes: { stop_on_first: stopOnFirst },
717-
});
718-
719707
return { dsns: [...results.values()], sourceMtimes, dirMtimes };
720708
},
721709
{

src/lib/dsn/env-file.ts

Lines changed: 58 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import { opendir } from "node:fs/promises";
1212
import { join } from "node:path";
13+
import { withTracingSpan } from "../telemetry.js";
1314
import { createDetectedDsn } from "./parser.js";
1415
import { scanSpecificFiles } from "./scanner.js";
1516
import type { DetectedDsn } from "./types.js";
@@ -23,6 +24,8 @@ export type EnvFileScanResult = {
2324
dsns: DetectedDsn[];
2425
/** Map of source file paths to their mtimes (only files containing DSNs) */
2526
sourceMtimes: Record<string, number>;
27+
/** Number of package directories scanned for env files (monorepo scan only) */
28+
packagesScanned?: number;
2629
};
2730

2831
/**
@@ -99,17 +102,25 @@ export const extractDsnFromEnvFile = extractDsnFromEnvContent;
99102
export async function detectFromEnvFiles(
100103
cwd: string
101104
): Promise<DetectedDsn | null> {
102-
const { dsns } = await scanSpecificFiles(cwd, [...ENV_FILES], {
103-
stopOnFirst: true,
104-
processFile: (_relativePath, content) => {
105-
const dsn = extractDsnFromEnvContent(content);
106-
return dsn ? { dsn } : null;
107-
},
108-
createDsn: (raw, relativePath) =>
109-
createDetectedDsn(raw, "env_file", relativePath),
110-
});
105+
return await withTracingSpan(
106+
"detectFromEnvFiles",
107+
"dsn.detect.env",
108+
async (span) => {
109+
const { dsns } = await scanSpecificFiles(cwd, [...ENV_FILES], {
110+
stopOnFirst: true,
111+
processFile: (_relativePath, content) => {
112+
const dsn = extractDsnFromEnvContent(content);
113+
return dsn ? { dsn } : null;
114+
},
115+
createDsn: (raw, relativePath) =>
116+
createDetectedDsn(raw, "env_file", relativePath),
117+
});
111118

112-
return dsns[0] ?? null;
119+
span.setAttribute("dsn.env_files_checked", ENV_FILES.length);
120+
span.setAttribute("dsn.env_dsn_found", dsns.length > 0);
121+
return dsns[0] ?? null;
122+
}
123+
);
113124
}
114125

115126
/**
@@ -125,33 +136,43 @@ export async function detectFromEnvFiles(
125136
export async function detectFromAllEnvFiles(
126137
cwd: string
127138
): Promise<EnvFileScanResult> {
128-
const allDsns: DetectedDsn[] = [];
129-
const allMtimes: Record<string, number> = {};
139+
return await withTracingSpan(
140+
"detectFromAllEnvFiles",
141+
"dsn.detect.env",
142+
async (span) => {
143+
const allDsns: DetectedDsn[] = [];
144+
const allMtimes: Record<string, number> = {};
130145

131-
// 1. Check root .env files (all of them, not just first)
132-
const { dsns: rootDsns, sourceMtimes: rootMtimes } = await scanSpecificFiles(
133-
cwd,
134-
[...ENV_FILES],
135-
{
136-
stopOnFirst: false,
137-
processFile: (_relativePath, content) => {
138-
const dsn = extractDsnFromEnvContent(content);
139-
return dsn ? { dsn } : null;
140-
},
141-
createDsn: (raw, relativePath) =>
142-
createDetectedDsn(raw, "env_file", relativePath),
143-
}
144-
);
145-
allDsns.push(...rootDsns);
146-
Object.assign(allMtimes, rootMtimes);
146+
// 1. Check root .env files (all of them, not just first)
147+
const { dsns: rootDsns, sourceMtimes: rootMtimes } =
148+
await scanSpecificFiles(cwd, [...ENV_FILES], {
149+
stopOnFirst: false,
150+
processFile: (_relativePath, content) => {
151+
const dsn = extractDsnFromEnvContent(content);
152+
return dsn ? { dsn } : null;
153+
},
154+
createDsn: (raw, relativePath) =>
155+
createDetectedDsn(raw, "env_file", relativePath),
156+
});
157+
allDsns.push(...rootDsns);
158+
Object.assign(allMtimes, rootMtimes);
147159

148-
// 2. Check monorepo package directories
149-
const { dsns: monorepoDsns, sourceMtimes: monorepoMtimes } =
150-
await detectFromMonorepoEnvFiles(cwd);
151-
allDsns.push(...monorepoDsns);
152-
Object.assign(allMtimes, monorepoMtimes);
160+
// 2. Check monorepo package directories
161+
const {
162+
dsns: monorepoDsns,
163+
sourceMtimes: monorepoMtimes,
164+
packagesScanned = 0,
165+
} = await detectFromMonorepoEnvFiles(cwd);
166+
allDsns.push(...monorepoDsns);
167+
Object.assign(allMtimes, monorepoMtimes);
153168

154-
return { dsns: allDsns, sourceMtimes: allMtimes };
169+
// Root checks ENV_FILES.length names; each monorepo package also checks ENV_FILES.length
170+
const filesChecked = ENV_FILES.length * (1 + packagesScanned);
171+
span.setAttribute("dsn.env_files_checked", filesChecked);
172+
span.setAttribute("dsn.env_dsn_found", allDsns.length > 0);
173+
return { dsns: allDsns, sourceMtimes: allMtimes };
174+
}
175+
);
155176
}
156177

157178
/**
@@ -168,6 +189,7 @@ export async function detectFromMonorepoEnvFiles(
168189
): Promise<EnvFileScanResult> {
169190
const dsns: DetectedDsn[] = [];
170191
const sourceMtimes: Record<string, number> = {};
192+
let packagesScanned = 0;
171193

172194
for (const monorepoRoot of MONOREPO_ROOTS) {
173195
const rootDir = join(cwd, monorepoRoot);
@@ -188,6 +210,7 @@ export async function detectFromMonorepoEnvFiles(
188210
continue;
189211
}
190212

213+
packagesScanned += 1;
191214
const pkgDir = join(rootDir, entry.name);
192215
const packagePath = `${monorepoRoot}/${entry.name}`;
193216

@@ -202,7 +225,7 @@ export async function detectFromMonorepoEnvFiles(
202225
}
203226
}
204227

205-
return { dsns, sourceMtimes };
228+
return { dsns, sourceMtimes, packagesScanned };
206229
}
207230

208231
/** Result of detecting DSN in a single package */

0 commit comments

Comments
 (0)