This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.
The bug that only showed up in console apps
A Sentry .NET user filed issue #5352: running the Sentry.Samples.Console.Basic sample, every structured log went out with an empty sentry.sdk.name and an empty sentry.sdk.version. They also suspected trace metrics had the same problem. They were right.
Those two attributes are not decoration. They tell Sentry which SDK produced a log or a metric. Lose them and a whole class of apps ships telemetry that cannot be attributed back to the .NET SDK. The strange part: the exact same logging code inside an ASP.NET Core app was fine. Only console apps dropped the fields. That split is the whole story, so this post walks the trace that explains it.
Following the value
Logs and metrics both end up in one method, SetDefaultAttributes in src/Sentry/Protocol/SentryAttributes.cs. Before the fix it read the SDK fields straight off the object it was handed:
if (sdk.Name is { } name) { SetAttribute("sentry.sdk.name", name); }
if (sdk.Version is { } version) { SetAttribute("sentry.sdk.version", version); }
Nothing wrong on its face. If Name and Version are set they get written. If they are null the attributes are quietly skipped. So the real question is what sdk is on the console path.
Two call sites feed this method:
// src/Sentry/SentryLog.cs:153
sdk ??= scope?.Sdk ?? SdkVersion.Instance;
// src/Sentry/SentryMetric.Factory.cs:23
metric.Attributes.SetDefaultAttributes(options, scope?.Sdk ?? SdkVersion.Instance);
Both look defensive. Both say "use the scope's Sdk, otherwise fall back to SdkVersion.Instance". SdkVersion.Instance is the populated object, with Name = "sentry.dotnet" and the assembly version. So why does the fallback never help?
Here is the line that decides it, in src/Sentry/Scope.cs:277:
public SdkVersion Sdk { get; } = new(); // Name = null, Version = null
Scope.Sdk is auto-initialized to new SdkVersion(). That object exists, its Name and Version are just null. Because the property is never null, scope?.Sdk ?? SdkVersion.Instance always resolves to scope.Sdk and never reaches SdkVersion.Instance. The ?? only fires when scope itself is null. The fallback everyone wrote was dead code.
That empty object flows into SetDefaultAttributes, both guards see null, so both attributes drop out.
Why ASP.NET Core escaped
The framework integrations fill the scope's Sdk. ASP.NET Core does it in its middleware (src/Sentry.AspNetCore/SentryMiddleware.cs:257-258), setting scope.Sdk.Name and scope.Sdk.Version. Once those are non-null the guards pass and the attributes appear. The core Enricher populates the Sdk on events and transactions, never on the scope and never on the logs or metrics path. So a plain console app, with no integration touching scope.Sdk, leaves it empty. That is the difference between the two app types, all the way down.
The fix
One change, at the single point both logs and metrics share. Fall back per field to the populated instance when the incoming field is null:
// Fall back to the populated SDK instance when the scope's Sdk was not filled in by a framework
// integration, e.g. in console apps, so logs and metrics still carry the SDK name and version (see #5352).
if ((sdk.Name ?? SdkVersion.Instance.Name) is { } name)
{
SetAttribute("sentry.sdk.name", name);
}
if ((sdk.Version ?? SdkVersion.Instance.Version) is { } version)
{
SetAttribute("sentry.sdk.version", version);
}
A few things make this the right shape. It fixes logs and metrics in one edit because both funnel through here, so the two call sites cannot drift apart. It does not override anyone: when ASP.NET Core has set scope.Sdk.Name, that value is non-null so the ?? short circuits and the integration still wins. SdkVersion.Instance is the same object the envelope header already uses, so logs and metrics now agree with the envelope instead of contradicting it.
Test first, watch it fail, then pass
Two new tests reproduce the console path. The log one:
[Fact]
public void SetDefaultAttributes_EmptyScopeSdk_UsesSdkInstance()
{
var options = new SentryOptions();
var log = new SentryLog(Timestamp, TraceId, SentryLogLevel.Info, "message");
// A console app does not populate scope.Sdk, so its Name and Version stay null.
log.SetDefaultAttributes(options, new Scope(options));
SdkVersion.Instance.Version.Should().NotBeNullOrWhiteSpace();
log.Attributes.ShouldContain("sentry.sdk.name", "sentry.dotnet");
log.Attributes.ShouldContain("sentry.sdk.version", SdkVersion.Instance.Version);
}
The metric one mirrors it with new SdkVersion(), which is what the metric path receives in a console app. Two existing serialization tests were pinning the buggy payload (no SDK attributes), so they are corrected to include the name and version.
Revert only the source file and keep the tests: the suite reports Failed: 4, Passed: 2529. The two new tests fail on the missing sentry.sdk.name, plus the two corrected serialization tests. Put the one-line fix back and it is Failed: 0, Passed: 2533, Skipped: 5, Total: 2538 on net10.0. dotnet format --verify-no-changes is clean on the changed files. The pre-existing Protocol_Default_VerifyAttributes tests never caught the bug because they pre-populate the Sdk, so they never touch the empty case.
The change
This is a fix inside the Sentry .NET SDK itself, not an app that uses it. Issue #5352, pull request getsentry/sentry-dotnet#5483, branch fix/sdkversion-empty-for-console at commit 36a0d63. One line of source in SentryAttributes.cs plus the regression tests. A dead ?? fallback that everyone assumed was covering the empty case, sitting one property away from the fix.
Best Use of Sentry
This entry is in two Bug Smash categories. Clear the Lineup covers the fix itself. Best Use of Sentry fits because the fix lives inside Sentry's own SDK. sentry.sdk.name and sentry.sdk.version are the attribution layer of the telemetry pipeline. They are how the platform knows a log or a metric came from sentry.dotnet and which version produced it, which is what lets Sentry group data by SDK, spot version-specific regressions and route an issue to the right maintainers. When a console app drops them, its logs and metrics arrive unattributed. The ASP.NET Core path silently disagreeing with it made the gap easy to miss. Restoring the fields at the one method both paths share means every log and every metric now carries the same SDK identity the envelope header already sends, so the data a team relies on to debug production lines up with itself.
AI disclosure
AI assistance (Claude, Anthropic) was used to trace the root cause, write the fix and the tests, then run the test suite. I own the change, reviewed it and verified it locally before submitting. Verified: the full Sentry.Tests suite on net10.0 (2533 passing, 0 failing); the bug reproduced by reverting only the source file (4 failing); dotnet format --verify-no-changes clean on the changed files.
Top comments (0)