Skip to content

Commit f735717

Browse files
feat: enrich telemetry with pythonVersion and dbtCoreVersion customAttributes
Production telemetry currently carries `common.os` / `common.nodeArch` / `common.extversion` etc. on every event but **not** the user's Python interpreter version or installed dbt-core distribution version. That's a real blindspot — App Insights can't split error clusters by Python version (e.g. is `pythonBridgeInitPythonError "mashumaro UnserializableField"` a Python 3.13 + mashumaro 3.14 incompat or something else?) and we can't tell whether `catalogPythonError "missing used_schemas"` correlates with a specific dbt-core minor. Adds two customAttributes that get merged into every event going forward via `TelemetryService.setTelemetryCustomAttribute`: - `pythonVersion` — sourced from `PythonEnvironment.pythonVersion` (already populated via the VS Code Python extension's API on interpreter activation, no additional probe needed). - `dbtCoreVersion` — sourced from a small `child_process.spawn` probe in `src/telemetry/versionProbes.ts` that runs `python -c "from importlib.metadata import version; print(version('dbt-core'))"`. Reads dist-info directly so it survives even when dbt's own import chain is broken (the failure mode PR #96 targeted) — the bridge can be dead and we still get the version. Both are best-effort: probe failure → no customAttribute set rather than blocking activation. Wired into `DBTPowerUserExtension.activate` after `initializeDBTProjects` (when the Python interpreter is known) plus a refresh on `pythonEnvironment.onPythonEnvironmentChanged` so the dimensions track interpreter changes. `probeDbtCoreVersion` accepts an injectable `SpawnFn` parameter defaulted to `child_process.spawn`. `import * as childProcess` produces ESM-style bindings that resist `jest.spyOn` / `jest.mock` redefinition under ts-jest, so the tests pass their own spawner directly. Adds 6 unit tests covering: success, empty stdout (PackageNotFoundError), non-zero exit, error event, sync throw, and timeout. After this ships and rolls out, App Insights queries can split by `customDimensions.pythonVersion == "3.13.0"` or `customDimensions.dbtCoreVersion startswith "1.10"` on every event, not just the wrapper-failure events. Dashboards become diagnostic by construction without per-event injection. Tests: 33 suites / 486 passed (32 prior + 1 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 68ef665 commit f735717

4 files changed

Lines changed: 363 additions & 1 deletion

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1342,7 +1342,7 @@
13421342
"altimateai.vscode-altimate-mcp-server"
13431343
],
13441344
"dependencies": {
1345-
"@altimateai/dbt-integration": "^0.3.0",
1345+
"@altimateai/dbt-integration": "^0.3.1",
13461346
"@jupyterlab/coreutils": "^6.2.4",
13471347
"@jupyterlab/nbformat": "^4.2.4",
13481348
"@jupyterlab/services": "^7.0.0",

src/dbtPowerUserExtension.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { probeDbtCoreVersion } from "@altimateai/dbt-integration";
12
import { NotebookProviders } from "@lib";
23
import { commands, Disposable, ExtensionContext, workspace } from "vscode";
34
import { AutocompletionProviders } from "./autocompletion_provider";
@@ -41,6 +42,13 @@ export class DBTPowerUserExtension implements Disposable {
4142
];
4243

4344
private disposables: Disposable[] = [];
45+
/**
46+
* Monotonic sequence counter for `refreshVersionTelemetryAttributes` so
47+
* a slow earlier invocation can't overwrite attributes written by a
48+
* faster later one. Each refresh captures the seq at start and only
49+
* applies its results if the seq still matches.
50+
*/
51+
private versionRefreshSeq = 0;
4452

4553
constructor(
4654
private dbtProjectContainer: DBTProjectContainer,
@@ -97,6 +105,34 @@ export class DBTPowerUserExtension implements Disposable {
97105
this.dbtProjectContainer.setContext(context);
98106
this.dbtProjectContainer.initializeWalkthrough();
99107
await this.dbtProjectContainer.detectDBT();
108+
109+
// Enrich every telemetry event with the active Python interpreter version
110+
// and dbt-core dist version. Without these dimensions, App Insights can't
111+
// split error clusters by Python or dbt-core version — e.g. we can't
112+
// tell whether a `pythonBridgeInitPythonError "mashumaro
113+
// UnserializableField"` is a Python 3.13 + mashumaro 3.14 incompat or
114+
// something else. Both probes are best-effort: failure → attribute is
115+
// cleared rather than blocking activation.
116+
//
117+
// Primed BEFORE `initializeDBTProjects()` because that call is the
118+
// python-bridge init step where `pythonBridgeInitPythonError` itself
119+
// originates. The `void`-fired refresh runs synchronously up to its
120+
// first await, so `pythonVersion` is guaranteed in `customAttributes`
121+
// by the time `initializeDBTProjects()` starts — meaning a thrown
122+
// error caught below is always dimensioned with at least Python
123+
// version. (`dbtCoreVersion` arrives best-effort once the spawn
124+
// resolves.) Re-runs on interpreter change so the dimensions track
125+
// the user's selection. `detectDBT()` is the earliest point where
126+
// `pythonEnvironment.executionDetails` is set and `pythonVersion`
127+
// is populated.
128+
void this.refreshVersionTelemetryAttributes();
129+
const pythonEnv = this.dbtProjectContainer.getPythonEnvironment();
130+
this.disposables.push(
131+
pythonEnv.onPythonEnvironmentChanged(() => {
132+
void this.refreshVersionTelemetryAttributes();
133+
}),
134+
);
135+
100136
await this.dbtProjectContainer.initializeDBTProjects();
101137
await this.statusBars.initialize();
102138
// Ask to reload the window if the dbt integration changes
@@ -121,4 +157,56 @@ export class DBTPowerUserExtension implements Disposable {
121157
this.telemetry.sendTelemetryError("extensionActivationError", error);
122158
}
123159
}
160+
161+
/**
162+
* Populate `pythonVersion` and `dbtCoreVersion` customAttributes on the
163+
* telemetry service so every event from this point forward carries them.
164+
* Best-effort: if either probe fails (interpreter missing, dbt-core not
165+
* installed in this venv, probe timeout), the corresponding attribute is
166+
* **cleared** rather than left at a stale value from the previous
167+
* interpreter. Idempotent — wired both at activation and from the
168+
* `onPythonEnvironmentChanged` listener.
169+
*
170+
* Sequence-guarded: rapid interpreter switches can fire two refreshes
171+
* concurrently. A slower earlier probe finishing last would otherwise
172+
* overwrite a faster later probe's results. We capture the
173+
* `versionRefreshSeq` at entry and bail before any write if a newer
174+
* refresh has bumped the counter.
175+
*/
176+
private async refreshVersionTelemetryAttributes(): Promise<void> {
177+
const seq = ++this.versionRefreshSeq;
178+
try {
179+
const pythonEnv = this.dbtProjectContainer.getPythonEnvironment();
180+
const pythonVersion = pythonEnv.pythonVersion;
181+
if (seq !== this.versionRefreshSeq) {
182+
return;
183+
}
184+
if (pythonVersion) {
185+
this.telemetry.setTelemetryCustomAttribute(
186+
"pythonVersion",
187+
pythonVersion,
188+
);
189+
} else {
190+
this.telemetry.clearTelemetryCustomAttribute("pythonVersion");
191+
}
192+
const pythonPath = pythonEnv.pythonPath;
193+
const dbtCoreVersion = pythonPath
194+
? await probeDbtCoreVersion(pythonPath)
195+
: undefined;
196+
if (seq !== this.versionRefreshSeq) {
197+
return;
198+
}
199+
if (dbtCoreVersion) {
200+
this.telemetry.setTelemetryCustomAttribute(
201+
"dbtCoreVersion",
202+
dbtCoreVersion,
203+
);
204+
} else {
205+
this.telemetry.clearTelemetryCustomAttribute("dbtCoreVersion");
206+
}
207+
} catch {
208+
// Telemetry-enrichment failures must never block activation or
209+
// surface as an error — by design these dimensions are best-effort.
210+
}
211+
}
124212
}

src/telemetry/index.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,18 @@ export class TelemetryService implements vscode.Disposable {
1616
this.customAttributes[key] = value;
1717
}
1818

19+
/**
20+
* Remove a previously-set custom attribute so it stops appearing on
21+
* subsequent telemetry events. Use when the dimension no longer applies
22+
* — e.g. the user switched Python interpreters and we couldn't probe a
23+
* fresh value, so the previous interpreter's `pythonVersion` /
24+
* `dbtCoreVersion` would otherwise carry over and corrupt dimensioning
25+
* for events from the new interpreter.
26+
*/
27+
clearTelemetryCustomAttribute(key: string) {
28+
delete this.customAttributes[key];
29+
}
30+
1931
startTelemetryEvent(
2032
eventName: string,
2133
properties?: { [key: string]: string },

0 commit comments

Comments
 (0)