Repository navigation
Fix MSBuild.exe telemetry delivery in headless CI - #15186
YuliiaKovalova wants to merge 18 commits into
Conversation
Standalone .NET Framework MSBuild.exe (VS/Build Tools) did not deliver telemetry from CI agents: * The VS payload did not ship Microsoft.VisualStudio.Telemetry and Microsoft.VisualStudio.RemoteControl, and amd64/arm64 had no codeBase for them, so session initialization failed silently. * TelemetrySession.Dispose() only persists events locally; ephemeral CI agents never ran another process to upload them. Changes: * Ship the Microsoft.VisualStudio.Telemetry runtime closure in MSBuild\Current\Bin (files.swr) and bind amd64/arm64 to it via codeBase. * Only the entry process (and the .NET server node) owns a telemetry session; worker, task host and RAR nodes no longer start one. * Track ownership so only MSBuild-owned sessions are disposed; host (VS) sessions are never disposed. * On CI, flush with DisposeToNetworkAsync under a bounded total budget (10s default, MSBUILD_TELEMETRY_SHUTDOWN_TIMEOUT_MS), off the lock and without affecting the exit code. Outside CI keep persist-and-exit. * Report Azure DevOps and GitHub Actions as BuildEngineHost, after VS and MSBUILD_HOST_NAME. * Add opt-in MSBUILD_TELEMETRY_DIAGNOSTICS status messages on stderr. * Consent is unchanged: MSBUILD_TELEMETRY_OPTOUT and VS opt-in. * Document delivery, prerequisites, limitations and new variables. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…n code Telemetry now uses the same automated-environment detection as the terminal logger instead of a separate CI detector. Simplify TelemetryManager shutdown and diagnostics, derive crash-session creation from the node mode, and remove non-essential comments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contain non-critical failures after the owned session is detached, guard diagnostics entirely, and correct the telemetry overview and worker-process wording. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Crash events and the VS FaultEvent no longer carry exception message text, which can contain customer data. Exception types, HRESULTs, the stack hash, and path-redacted stack traces are still sent. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the shutdown delegate, outcome enum and test seams with one bounded call: the owned session is saved (or uploaded in CI) on a background task that the exiting process abandons after MSBUILD_TELEMETRY_SHUTDOWN_TIMEOUT_MS. Reuse EnvironmentUtilities for parsing the timeout. Keep tests that guard privacy and the host/CI contract; remove tests that only exercised test seams or pre-existing behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Path.GetFileNameWithoutExtension only splits on the current OS separators, so on Unix a Windows path kept its directories in the diagnostic description. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Visual Studio loads its own copy, so the packaged copy only serves MSBuild.exe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Read the telemetry environment variables through Traits, print diagnostics like other opt-in MSBuild stderr diagnostics, and group the codeBase entries with the other assemblies MSBuild redistributes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The telemetry closure is already copied next to MSBuild.exe as dependencies of Microsoft.Build.Framework, the same way Utilities.Internal and Newtonsoft.Json ship. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Diagnostic output can alter the exit code, and critical ownership and shutdown behavior lacks automated regression coverage.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Enables reliable Visual Studio telemetry delivery from headless .NET Framework MSBuild processes while preserving consent and protecting crash data.
Changes:
- Adds required telemetry assemblies and 64-bit/ARM64 resolution configuration.
- Introduces process ownership, CI-aware upload, bounded shutdown, diagnostics, and host attribution.
- Removes exception messages from crash telemetry and documents the behavior.
| File | Description |
|---|---|
src/Shared/UnitTests/TestAssemblyInfo.cs |
Disables telemetry during tests. |
src/Package/MSBuild.VSSetup/files.swr |
Packages telemetry dependencies. |
src/MSBuild/XMake.cs |
Manages process-level telemetry ownership and shutdown. |
src/MSBuild/app.amd64.config |
Resolves telemetry assemblies for 64-bit processes. |
src/Framework/Traits.cs |
Adds telemetry diagnostics and timeout settings. |
src/Framework/Telemetry/TelemetryManager.cs |
Implements bounded save/upload behavior. |
src/Framework/Telemetry/CrashTelemetryRecorder.cs |
Flushes sessions and sanitizes fault exceptions. |
src/Framework/Telemetry/CrashTelemetry.cs |
Removes exception-message properties. |
src/Framework/DebugUtils.cs |
Exposes the current process node mode. |
src/Framework/BuildEnvironmentState.cs |
Centralizes CI detection and attribution. |
src/Framework.UnitTests/CrashTelemetry_Tests.cs |
Verifies exception messages are excluded. |
src/Framework.UnitTests/BuildEnvironmentState_Tests.cs |
Tests CI and host detection precedence. |
documentation/wiki/MSBuild-Environment-Variables.md |
Documents telemetry environment variables. |
documentation/wiki/CollectedTelemetry.md |
Clarifies host-specific telemetry behavior. |
documentation/VS-Telemetry-Data.md |
Documents collection, consent, delivery, and privacy. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
| # | Dimension | Verdict |
|---|---|---|
| 4 | Test Coverage & Completeness | 🟠 1 MAJOR |
| 10 | Design Before Implementation | 🟠 1 MAJOR |
| 6 | Logging & Diagnostics Rigor | 🟡 Existing thread |
✅ 21/24 dimensions clean.
- Test Coverage — add deterministic coverage for CI network disposal and its timeout.
- Design — preserve the declared process-ownership boundary on worker/task-host/RAR crash paths.
- Diagnostics — the existing review thread already covers protecting
Console.Error.WriteLine; not duplicated here.
Warning
Firewall blocked 5 domains
The following domains were blocked by the firewall during workflow execution:
api.github.comcafe.github.comgithub.compatchdiff.githubusercontent.comraw.githubusercontent.com
[!TIP]
api.github.com is blocked because GitHub API access uses the built-in GitHub tools by default. Instead of adding api.github.com to network.allowed, use tools.github.mode: gh-proxy for direct pre-authenticated GitHub CLI access without requiring network access to api.github.com:
tools:
github:
mode: gh-proxySee GitHub Tools for more information on gh-proxy mode.
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "api.github.com"
- "cafe.github.com"
- "github.com"
- "patchdiff.githubusercontent.com"
- "raw.githubusercontent.com"See Network Configuration for more information.
Generated by Expert Code Review (on open) for #15186 · copilot · gpt56 · 1.4K AIC · ⌖ 10.3 AIC · ⊞ 24.5K
baronfel
left a comment
There was a problem hiding this comment.
This seems reasonable, but @rainersigwald and @JanProvaznik probably have thoughts/feedback about the dll dependencies and if we can get that through VS perf gates.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
JanProvaznik
left a comment
There was a problem hiding this comment.
General points:
Ownership: the entry process (or the server node) owns the session. Worker and task-host nodes don't create one. Inside VS, MSBuild uses the VS session and never disposes it.
this goes against the design of the crash/hang telemetry no?
Those kinds of problems are supposed to start the session in the node that crashed because there is no way to at that point send it to the node that owns the singular session.
The server path basically does not work:
A resident MSBuild server saves or uploads its session only at idle expiry (15 min after the last build) or explicit shutdown, so events can be lost on agents recycled sooner. Consider a per-build flush in CI. Also, Environment.Exit(0) in the cancel path ( XMake.cs:1541 ) skips finally .
| internal static bool IsOptOut() => | ||
| #if NETFRAMEWORK | ||
| Traits.Instance.FrameworkTelemetryOptOut; | ||
| Traits.Instance.FrameworkTelemetryOptOut || Traits.Instance.SdkTelemetryOptOut; |
There was a problem hiding this comment.
DOTNET_CLI_TELEMETRY_OPTOUT now also silences MSBuild's events inside VS. IsOptOut() is shared by Initialize(isStandalone: false) and by BeginBuild 's collection gate ( BuildManager.cs:555 ), yet the docs say the VS path is unaffected ( VS-Telemetry-Data.md:200 ). Is that intended? If not, apply the SDK variable only when isStandalone .
There was a problem hiding this comment.
It comes from @baronfel 's comment here #15186 (comment), so yes, it's intended.
Session ownership: the process that owns the build creates the telemetry session (the MSBuild.exe in-proc build and the server node). The thin client creates one lazily, only when it falls back to an in-proc build. Worker, task host and RAR nodes create none; a crash in them is reported through a crash-only session created in MSBuild.exe. Shutdown: in CI the session uploads at exit only when events were started and the session is opted in. The upload gets 80% of the shutdown budget, is then canceled and the pending events are saved locally. A later Dispose waits within the remaining budget. The Ctrl+C handler disposes telemetry before Environment.Exit(0). Privacy: hash every logger type name and the BuildCheck custom-check failure message, and report only the file name of the active project in hang diagnostics. The node mode is parsed with a stricter pattern. Docs: describe how telemetry flows, which processes report, consent, delivery and crashes, and drop the point-in-time background section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28a8830c-459d-4c9e-98d6-904457bef00c
Privacy: a logger type in a Microsoft. namespace is reported as is, so that hang diagnostics show which Microsoft loggers were registered, and any other logger type is still hashed. The check is an ordinal prefix match, and the name of a constructed generic type is always hashed because it lists the assembly-qualified names of its type arguments. Project file names stay hashed. Docs: update the privacy section of VS-Telemetry-Data.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 28a8830c-459d-4c9e-98d6-904457bef00c

Context
MSBuild.exe(.NET Framework, Visual Studio / Build Tools) sent no telemetry from headless CI. Two confirmed causes:Microsoft.VisualStudio.Telemetry.dllorMicrosoft.VisualStudio.RemoteControl.dll, and the amd64/arm64MSBuild.exe.configcouldn't locate them. Session creation threw, the exception was swallowed, and nothing was reported.TelemetrySession.Dispose()blocks on the SDK's own internal send timeout, which is long and not CI-aware. An ephemeral agent can exit before it completes.Changes Made
Keeps
Microsoft.VisualStudio.Telemetryand the existing destination, consent, andVS/MSBuild/Buildevent. Adopts the .NET SDK's design ideas (the host owns the session, CI-aware upload, bounded shutdown) but not its OpenTelemetry stack, which would change the destination and add a new .NET Framework dependency closure.files.swrships the two missing DLLs.app.amd64.config(used by amd64 and arm64) addscodeBaseentries for them and forUtilities.Internal. x86 loads them from its own folder./nodemode:N,-nodemode:N,--nodemode:N, and thenmodealias). Inside VS, MSBuild uses the VS session and never disposes it.MSBuild.exeprocess reports its own crash. A process that has no session (a worker, task host or RAR node, or the thin client) creates one just for the crash, bounded by the shutdown budget. In a process that MSBuild is hosted in, such as VS, the host's session is used if there is one.DisposeToNetworkAsync, only if the process started events and the session is opted in; outside CI, events are saved as before. Both are capped at 10 s in total (MSBUILD_TELEMETRY_SHUTDOWN_TIMEOUT_MS). The upload gets 80% of the budget. If it fails, or doesn't finish in time and is canceled, the events are saved locally in the rest of the budget, so that a later process can upload them. A second shutdown request waits for the first one within the remaining budget, and the server node's Ctrl+C handler shuts telemetry down beforeEnvironment.Exit(0). Telemetry never changes the exit code.BuildEngineHostreportsAzure DevOps/GitHub Action. Precedence: VS >MSBUILD_HOST_NAME> CI > VS Code.MSBUILD_TELEMETRY_OPTOUTandDOTNET_CLI_TELEMETRY_OPTOUTboth opt out, inMSBuild.exeand inside Visual Studio, so setting the standard .NET variable is enough. This deliberately changes the behavior inside Visual Studio: before, onlyMSBUILD_TELEMETRY_OPTOUTapplied there. When opted out, no session is created and no MSBuild events are sent. The VS opt-in is still honored, and detecting CI never opts anyone in.MSBUILD_TELEMETRY_DIAGNOSTICS=1writes initialization, opt-out, failure, and shutdown status to stderr.FaultEventno longer carry exception messages. This deliberately changes the crash event contract. Hang diagnostics identify the project of an active node only by the hash of its file name, without its directory. They report a logger type by name when it is in aMicrosoft.namespace, and by the hash of its name otherwise. The BuildCheck custom-check loading failure message has its paths removed, is truncated, and is hashed.VS-Telemetry-Data.mdgains a flowchart of what each process sends and how, a table of which process owns a session, and sections on crashes, consent, and delivery.Testing
The build passes with no warnings. Focused telemetry tests pass on net48 and net11.0, including CI/non-CI shutdown selection, the bounded timeout, and both opt-out variables. A headless E2E harness ran x86/x64
MSBuild.exefrom the built VSIX layout (exactly what VS setup installs), simulating CI through a local proxy, with one approved run against the production collector: events arrived with the correctBuildEngineHost/BuildSuccess, no duplicates across multiprocess/server/node-reuse builds, shutdown capped at 10 s under a stalled network, and opt-out / missing-dependency cases correctly disabled telemetry without affecting the build. Not tested: ARM64 at runtime, a clean Build Tools image, real Azure DevOps/GitHub Actions agents.Review follow-up: new tests cover when the upload happens (CI, events started, opted in), the cancel-then-save fallback and the shared shutdown budget, the crash-only session in
MSBuild.exeversus hosted processes, node mode parsing, the hashing of custom logger type names, project file names, and the BuildCheck message, and the plain-text reporting of Microsoft logger types. The key tests were checked to fail when the code they guard is broken. The builtMSBuild.exewas also run with the opt-out set andMSBUILD_TELEMETRY_DIAGNOSTICS=1, so that nothing was sent: the entry process and the server node reach telemetry initialization, and the worker node and the thin client (while the server builds) don't. Not re-run after these changes: the proxy and production-collector E2E harness.Known limitations and follow-ups
DisposeToNetworkAsyncends the session. Its events reach the local store about every 30 s, are uploaded by the process that holds the upload lock, and are saved when the node exits, so an upload right after each build isn't guaranteed. Possible follow-ups: send the build event from the server node back to the thin client, which would upload it when it exits, or let a short-lived server exit after the build in CI, which changes the server's lifetime and needs agreement.BUILD_IDon a developer machine also triggers the upload on exit. The cost of that false positive is a bounded wait, followed by a local save./nodemode:Non the command line.Background: how delivery happened before 18.13
Before the upload-on-exit behavior described above,
MSBuild.exeonly saved events to the local store on exit; it never uploaded them itself. Whether those events still reached the collector depended on which of two mechanisms applied.In-process hosting (for example, Visual Studio's own build engine).
BuildManager.EndBuildTelemetrycallsTelemetryManager.Instance.Initialize(isStandalone: false), which resolves the session throughTelemetryService.DefaultSessionrather than creating one. When MSBuild's Framework assemblies are loaded in-process by a host that already created a default session (Visual Studio does this early at startup for its own telemetry), MSBuild's build event is posted onto that same live session object. MSBuild does not own or dispose it; the host's own long-running session delivers it through its normal flush cadence. Delivery on this path is not changed by this PR, and it was always reliable.Separate
MSBuild.exeprocesses (standalone, including CI and builds started from a VS Developer Command Prompt).XMake.csalways calledInitialize(isStandalone: true)first, so everyMSBuild.exeprocess owned its own session, created withTelemetryService.CreateAndGetDefaultSession, regardless of how it was launched. That session persists events to a storage folder keyed by the collector's instrumentation key (%LOCALAPPDATA%\Microsoft\VSApplicationInsights\vstel<hash>\*.trn), shared by every process on the machine that uses the same collector key. Before this change, delivery from that folder depended entirely on some process acting as sender:PersistenceTransmittertries to acquire a named, cross-process mutex over the folder. Only the process holding the mutex runs aSenderthat polls the folder and uploads files.FlushManager.FlushDelay), and the sender polls every 10 seconds when idle (Sender.sendingIntervalOnNoData). But events raised close to exit, including the build's own end event, can still be left in a final.trnfile written during session disposal, after the last auto-flush..trnfile sits in the shared folder until another process using the same collector key starts, acquires the now-free sender mutex, and uploads it. That process can be Visual Studio itself (devenv.exe, since many builds happen under VS or a VS Developer Command Prompt) or simply a laterMSBuild.exeinvocation on the same machine. On a reused dev or build machine, a single drain can pick up leftovers from several prior runs if nothing drained them in between.MSBuild.exeprocess uploaded a first process's leftover file, and a minimal harness using the sameTelemetryService.DefaultSessionAPI that Visual Studio uses delivered a childMSBuild.exeprocess's file about 7 seconds after the child exited.