文章 代码库 城市生活记忆 Claude Skill AI分享 问龙虾
返回 Claude Skill

Sentry .NET SDK

Sentry 错误监控 .NET SDK 集成,异常追踪和性能监控

DevOps 社区公开 by Community

Sentry .NET SDK

Opinionated wizard that scans your .NET project and guides you through complete Sentry setup: error monitoring, distributed tracing, profiling, structured logging, and cron monitoring across all major .NET frameworks.

Invoke This Skill When

  • User asks to “add Sentry to .NET”, “set up Sentry in C#”, or “install Sentry for ASP.NET Core”
  • User wants error monitoring, tracing, profiling, logging, or crons for a .NET app
  • User mentions SentrySdk.Init, UseSentry, Sentry.AspNetCore, or Sentry.Maui
  • User wants to capture unhandled exceptions in WPF, WinForms, MAUI, or Azure Functions
  • User asks about SentryOptions, BeforeSend, TracesSampleRate, or symbol upload

Note: SDK version and APIs below reflect Sentry NuGet packages ≥6.1.0. Always verify against docs.sentry.io/platforms/dotnet/ before implementing.


Phase 1: Detect

Run these commands to understand the project before making any recommendations:

# Detect framework type — find all .csproj files
find . -name "*.csproj" | head -20

# Detect framework targets
grep -r "TargetFramework\|Project Sdk" --include="*.csproj" .

# Check for existing Sentry packages
grep -r "Sentry" --include="*.csproj" . | grep "PackageReference"

# Check startup files
ls Program.cs src/Program.cs App.xaml.cs MauiProgram.cs 2>/dev/null

# Check for appsettings
ls appsettings.json src/appsettings.json 2>/dev/null

# Check for logging libraries
grep -r "Serilog\|NLog\|log4net" --include="*.csproj" .

# Check for companion frontend
ls ../frontend ../client ../web 2>/dev/null
cat ../package.json 2>/dev/null | grep -E '"next"|"react"|"vue"' | head -3

What to determine:

QuestionImpact
Framework type?Determines correct package and init pattern
.NET version?.NET 8+ recommended; .NET Framework 4.6.2+ supported
Sentry already installed?Skip install, go to feature config
Logging library (Serilog, NLog)?Recommend matching Sentry sink/target
Async/hosted app (ASP.NET Core)?UseSentry() on WebHost; no IsGlobalModeEnabled needed
Desktop app (WPF, WinForms, WinUI)?Must set IsGlobalModeEnabled = true
Serverless (Azure Functions, Lambda)?Must set FlushOnCompletedRequest = true
Frontend directory found?Trigger Phase 4 cross-link

Framework → Package mapping:

DetectedPackage to install
Sdk="Microsoft.NET.Sdk.Web" (ASP.NET Core)Sentry.AspNetCore
App.xaml.cs with Application baseSentry (WPF)
[STAThread] in Program.csSentry (WinForms)
MauiProgram.csSentry.Maui
WebAssemblyHostBuilderSentry.AspNetCore.Blazor.WebAssembly
FunctionsStartupSentry.Extensions.Logging + Sentry.OpenTelemetry
HttpApplication / Global.asaxSentry.AspNet
Generic host / Worker ServiceSentry.Extensions.Logging

Phase 2: Recommend

Present a concrete recommendation based on what you found. Lead with a proposal — don’t ask open-ended questions.

Recommended (core coverage):

  • Error Monitoring — always; captures unhandled exceptions, structured captures, scope enrichment
  • Tracing — always for ASP.NET Core and hosted apps; auto-instruments HTTP requests and EF Core queries
  • Logging — recommended for all apps; routes ILogger / Serilog / NLog entries to Sentry as breadcrumbs and events

Optional (enhanced observability):

  • Profiling — CPU profiling; recommend for performance-critical services running on .NET 6+
  • Crons — detect missed/failed scheduled jobs; recommend when Hangfire, Quartz.NET, or scheduled endpoints detected

Recommendation logic:

FeatureRecommend when…
Error MonitoringAlways — non-negotiable baseline
TracingAlways for ASP.NET Core — request traces, EF Core spans, HttpClient spans are high-value
LoggingApp uses ILogger<T>, Serilog, NLog, or log4net
ProfilingPerformance-critical service on .NET 6+
CronsApp uses Hangfire, Quartz.NET, or scheduled Azure Functions

Propose: “I recommend setting up Error Monitoring + Tracing + Logging. Want me to also add Profiling or Crons?”


Phase 3: Guide

npx @sentry/wizard@latest -i dotnet

The wizard logs you into Sentry, selects your org and project, configures your DSN, and sets up MSBuild symbol upload for readable stack traces in production.

Skip to Verification after running the wizard.


Option 2: Manual Setup

Install the right package

# ASP.NET Core
dotnet add package Sentry.AspNetCore -v 6.1.0

# WPF or WinForms or Console
dotnet add package Sentry -v 6.1.0

# .NET MAUI
dotnet add package Sentry.Maui -v 6.1.0

# Blazor WebAssembly
dotnet add package Sentry.AspNetCore.Blazor.WebAssembly -v 6.1.0

# Azure Functions (Isolated Worker)
dotnet add package Sentry.Extensions.Logging -v 6.1.0
dotnet add package Sentry.OpenTelemetry -v 6.1.0

# Classic ASP.NET (System.Web / .NET Framework)
dotnet add package Sentry.AspNet -v 6.1.0

ASP.NET Core — Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseSentry(options =>
{
    options.Dsn = Environment.GetEnvironmentVariable("SENTRY_DSN")
                  ?? "___YOUR_DSN___";
    options.Debug = true;                         // disable in production
    options.SendDefaultPii = true;                // captures user IP, name, email
    options.MaxRequestBodySize = RequestSize.Always;
    options.MinimumBreadcrumbLevel = LogLevel.Debug;
    options.MinimumEventLevel = LogLevel.Warning;
    options.TracesSampleRate = 1.0;               // tune to 0.1–0.2 in production
    options.SetBeforeSend((@event, hint) =>
    {
        @event.ServerName = null;                 // scrub hostname from events
        return @event;
    });
});

var app = builder.Build();
app.Run();

appsettings.json (alternative configuration):

{
  "Sentry": {
    "Dsn": "___YOUR_DSN___",
    "SendDefaultPii": true,
    "MaxRequestBodySize": "Always",
    "MinimumBreadcrumbLevel": "Debug",
    "MinimumEventLevel": "Warning",
    "AttachStacktrace": true,
    "Debug": true,
    "TracesSampleRate": 1.0,
    "Environment": "production",
    "Release": "[email protected]"
  }
}

Environment variables (double underscore as separator):

export Sentry__Dsn="https://[email protected]/0"
export Sentry__TracesSampleRate="0.1"
export Sentry__Environment="staging"

WPF — App.xaml.cs

⚠️ Critical: Initialize in the constructor, NOT in OnStartup(). The constructor fires earlier, catching more failure modes.

using System.Windows;
using Sentry;

public partial class App : Application
{
    public App()
    {
        SentrySdk.Init(options =>
        {
            options.Dsn = "___YOUR_DSN___";
            options.Debug = true;
            options.SendDefaultPii = true;
            options.TracesSampleRate = 1.0;
            options.IsGlobalModeEnabled = true;   // required for all desktop apps
        });

        // Capture WPF UI-thread exceptions before WPF's crash dialog appears
        DispatcherUnhandledException += App_DispatcherUnhandledException;
    }

    private void App_DispatcherUnhandledException(
        object sender,
        System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e)
    {
        SentrySdk.CaptureException(e.Exception);
        // Set e.Handled = true to prevent crash dialog and keep app running
    }
}

WinForms — Program.cs

using System;
using System.Windows.Forms;
using Sentry;

static class Program
{
    [STAThread]
    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);

        // Required: allows Sentry to see unhandled WinForms exceptions
        Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);

        using (SentrySdk.Init(new SentryOptions
        {
            Dsn = "___YOUR_DSN___",
            Debug = true,
            TracesSampleRate = 1.0,
            IsGlobalModeEnabled = true,           // required for desktop apps
        }))
        {
            Application.Run(new MainForm());
        } // Disposing flushes all pending events
    }
}

.NET MAUI — MauiProgram.cs

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<App>()
            .UseSentry(options =>
            {
                options.Dsn = "___YOUR_DSN___";
                options.Debug = true;
                options.SendDefaultPii = true;
                options.TracesSampleRate = 1.0;
                // MAUI-specific: opt-in breadcrumbs (off by default — PII risk)
                options.IncludeTextInBreadcrumbs = false;
                options.IncludeTitleInBreadcrumbs = false;
                options.IncludeBackgroundingStateInBreadcrumbs = false;
            });

        return builder.Build();
    }
}

Blazor WebAssembly — Program.cs

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.UseSentry(options =>
{
    options.Dsn = "___YOUR_DSN___";
    options.Debug = true;
    options.SendDefaultPii = true;
    options.TracesSampleRate = 0.1;
});

// Hook logging pipeline without re-initializing the SDK
builder.Logging.AddSentry(o => o.InitializeSdk = false);

await builder.Build().RunAsync();

Azure Functions (Isolated Worker) — Program.cs

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Trace;
using Sentry.OpenTelemetry;

var host = new HostBuilder()
    .ConfigureFunctionsWorkerDefaults()
    .ConfigureServices(services =>
    {
        services.AddOpenTelemetry().WithTracing(builder =>
        {
            builder
                .AddSentry()                        // route OTel spans to Sentry
                .AddHttpClientInstrumentation();    // capture outgoing HTTP
        });
    })
    .ConfigureLogging(logging =>
    {
        logging.AddSentry(options =>
        {
            options.Dsn = "___YOUR_DSN___";
            options.Debug = true;
            options.TracesSampleRate = 1.0;
            options.UseOpenTelemetry();                     // let OTel drive tracing
            options.DisableSentryHttpMessageHandler = true; // prevent duplicate HTTP spans
        });
    })
    .Build();

await host.RunAsync();

AWS Lambda — LambdaEntryPoint.cs

public class LambdaEntryPoint : APIGatewayProxyFunction
{
    protected override void Init(IWebHostBuilder builder)
    {
        builder
            .UseSentry(options =>
            {
                options.Dsn = "___YOUR_DSN___";
                options.TracesSampleRate = 1.0;
                options.FlushOnCompletedRequest = true; // REQUIRED for Lambda
            })
            .UseStartup<Startup>();
    }
}

Classic ASP.NET — Global.asax.cs

public class MvcApplication : HttpApplication
{
    private IDisposable _sentry;

    protected void Application_Start()
    {
        _sentry = SentrySdk.Init(options =>
        {
            options.Dsn = "___YOUR_DSN___";
            options.TracesSampleRate = 1.0;
            options.AddEntityFramework(); // EF6 query breadcrumbs
            options.AddAspNet();          // Classic ASP.NET integration
        });
    }

    protected void Application_Error() => Server.CaptureLastError();

    protected void Application_BeginRequest() => Context.StartSentryTransaction();
    protected void Application_EndRequest() => Context.FinishSentryTransaction();

    protected void Application_End() => _sentry?.Dispose();
}

Symbol Upload (Readable Stack Traces)

Without debug symbols, stack traces show only method names — no file names or line numbers. Upload PDB files to unlock full source context.

Step 1: Create a Sentry auth token

Go to sentry.io/settings/auth-tokens/ and create a token with project:releases and org:read scopes.

Step 2: Add MSBuild properties to .csproj or Directory.Build.props:

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
  <SentryOrg>___ORG_SLUG___</SentryOrg>
  <SentryProject>___PROJECT_SLUG___</SentryProject>
  <SentryUploadSymbols>true</SentryUploadSymbols>
  <SentryUploadSources>true</SentryUploadSources>
  <SentryCreateRelease>true</SentryCreateRelease>
  <SentrySetCommits>true</SentrySetCommits>
</PropertyGroup>

Step 3: Set SENTRY_AUTH_TOKEN in CI:

# GitHub Actions
- name: Build & upload symbols
  run: dotnet build -c Release
  env:
    SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

For Each Agreed Feature

Load the corresponding reference file and follow its steps:

FeatureReference fileLoad when…
Error Monitoringreferences/error-monitoring.mdAlways — CaptureException, scopes, enrichment, filtering
Tracingreferences/tracing.mdServer apps, distributed tracing, EF Core spans, custom instrumentation
Profilingreferences/profiling.mdPerformance-critical apps on .NET 6+
Loggingreferences/logging.mdILogger<T>, Serilog, NLog, log4net integration
Cronsreferences/crons.mdHangfire, Quartz.NET, or scheduled function monitoring

For each feature: read the reference file, follow its steps exactly, and verify before moving on.


Verification

After wizard or manual setup, add a test throw and remove it after verifying:

// ASP.NET Core: add a temporary endpoint
app.MapGet("/sentry-test", () =>
{
    throw new Exception("Sentry test error — delete me");
});

// Or capture explicitly anywhere
SentrySdk.CaptureException(new Exception("Sentry test error — delete me"));

Then check your Sentry Issues dashboard — the error should appear within ~30 seconds.

Verification checklist:

CheckHow
Exceptions capturedThrow a test exception, verify in Sentry Issues
Stack traces readableCheck that file names and line numbers appear
Tracing activeCheck Performance tab for transactions
Logging wiredLog an error via ILogger, check it appears as Sentry breadcrumb
Symbol upload workingStack trace shows Controllers/HomeController.cs:42 not <unknown>

Config Reference

Core SentryOptions

OptionTypeDefaultEnv VarNotes
DsnstringSENTRY_DSNRequired. SDK disabled if unset.
DebugboolfalseSDK diagnostic output. Disable in production.
DiagnosticLevelSentryLevelDebugDebug, Info, Warning, Error, Fatal
ReleasestringautoSENTRY_RELEASEAuto-detected from assembly version + git SHA
Environmentstring"production"SENTRY_ENVIRONMENT"debug" when debugger attached
DiststringBuild variant. Max 64 chars.
SampleRatefloat1.0Error event sampling rate 0.0–1.0
TracesSampleRatedouble0.0Transaction sampling. Must be > 0 to enable.
TracesSamplerFunc<SamplingContext, double>Per-transaction dynamic sampler; overrides TracesSampleRate
ProfilesSampleRatedouble0.0Fraction of traced transactions to profile. Requires Sentry.Profiling.
SendDefaultPiiboolfalseInclude user IP, name, email
AttachStacktracebooltrueAttach stack trace to all messages
MaxBreadcrumbsint100Max breadcrumbs stored per event
IsGlobalModeEnabledboolfalse**Auto-true for MAUI, Blazor WASM. Must be true for WPF, WinForms, Console.
AutoSessionTrackingboolfalse**Auto-true for MAUI. Enable for Release Health.
CaptureFailedRequestsbooltrueAuto-capture HTTP client errors
CacheDirectoryPathstringOffline event caching directory
ShutdownTimeoutTimeSpanMax wait for event flush on shutdown
HttpProxystringProxy URL for Sentry requests
EnableBackpressureHandlingbooltrueAuto-reduce sample rates on delivery failures

ASP.NET Core Extended Options (SentryAspNetCoreOptions)

OptionTypeDefaultNotes
MaxRequestBodySizeRequestSizeNoneNone, Small (~4 KB), Medium (~10 KB), Always
MinimumBreadcrumbLevelLogLevelInformationMin log level for breadcrumbs
MinimumEventLevelLogLevelErrorMin log level to send as Sentry event
CaptureBlockingCallsboolfalseDetect .Wait() / .Result threadpool starvation
FlushOnCompletedRequestboolfalseRequired for Lambda / serverless
IncludeActivityDataboolfalseCapture System.Diagnostics.Activity values

MAUI Extended Options (SentryMauiOptions)

OptionTypeDefaultNotes
IncludeTextInBreadcrumbsboolfalseText from Button, Label, Entry elements. ⚠️ PII risk.
IncludeTitleInBreadcrumbsboolfalseTitles from Window, Page elements. ⚠️ PII risk.
IncludeBackgroundingStateInBreadcrumbsboolfalseWindow.Backgrounding event state. ⚠️ PII risk.

Environment Variables

VariablePurpose
SENTRY_DSNProject DSN
SENTRY_RELEASEApp version (e.g. [email protected])
SENTRY_ENVIRONMENTDeployment environment name
SENTRY_AUTH_TOKENMSBuild / sentry-cli symbol upload auth token

ASP.NET Core: use double underscore __ as hierarchy separator:

export Sentry__Dsn="https://..."
export Sentry__TracesSampleRate="0.1"

MSBuild Symbol Upload Properties

PropertyTypeDefaultDescription
SentryOrgstringSentry organization slug
SentryProjectstringSentry project slug
SentryUploadSymbolsboolfalseUpload PDB files for line numbers in stack traces
SentryUploadSourcesboolfalseUpload source files for source context
SentryCreateReleaseboolfalseAuto-create a Sentry release during build
SentrySetCommitsboolfalseAssociate git commits with the release
SentryUrlstringSelf-hosted Sentry URL

After completing .NET setup, check for companion frontend projects:

# Check for frontend in adjacent directories
ls ../frontend ../client ../web ../app 2>/dev/null

# Check for JavaScript framework indicators
cat ../package.json 2>/dev/null | grep -E '"next"|"react"|"vue"|"nuxt"' | head -3

If a frontend is found, suggest the matching SDK skill:

Frontend detectedSuggest skill
Next.js ("next" in package.json)sentry-nextjs-sdk
React SPA ("react" without "next")@sentry/react — see docs.sentry.io/platforms/javascript/guides/react/
Vue.js@sentry/vue — see docs.sentry.io/platforms/javascript/guides/vue/
Nuxt@sentry/nuxt — see docs.sentry.io/platforms/javascript/guides/nuxt/

Connecting frontend and backend with the same Sentry project enables distributed tracing — a single trace view spanning browser, .NET server, and any downstream APIs.


Troubleshooting

IssueCauseSolution
Events not appearingDSN misconfiguredSet Debug = true and check console output for SDK diagnostic messages
Stack traces show no file/linePDB files not uploadedAdd SentryUploadSymbols=true to .csproj; set SENTRY_AUTH_TOKEN in CI
WPF/WinForms exceptions missingIsGlobalModeEnabled not setSet options.IsGlobalModeEnabled = true in SentrySdk.Init()
Lambda/serverless events lostContainer freezes before flushSet options.FlushOnCompletedRequest = true
WPF UI-thread exceptions missingDispatcherUnhandledException not wiredRegister App.DispatcherUnhandledException in constructor (not OnStartup)
Duplicate HTTP spans in Azure FunctionsBoth Sentry and OTel instrument HTTPSet options.DisableSentryHttpMessageHandler = true
TracesSampleRate has no effectRate is 0.0 (default)Set TracesSampleRate > 0 to enable tracing
appsettings.json values ignoredConfig key format wrongUse flat key "Sentry:Dsn" or env var Sentry__Dsn (double underscore)
BeforeSend drops all eventsHook returns null unconditionallyVerify your filter logic; return null only for events you want to drop
MAUI native crashes not capturedWrong packageConfirm Sentry.Maui is installed (not just Sentry)

Reference: Crons

Crons — Sentry .NET SDK

Minimum SDK: Sentry ≥ 4.2.0


Overview

Sentry Cron Monitoring detects:

  • Missed check-ins — job didn’t run at the expected time
  • Runtime failures — job ran but encountered an error
  • Timeouts — job exceeded MaxRuntime without completing

CaptureCheckIn() API

// Signature
SentryId CaptureCheckIn(
    string monitorSlug,
    CheckInStatus status,
    SentryId? checkInId = null,
    TimeSpan? duration = null,
    Action<SentryMonitorOptions>? configureMonitorOptions = null
)

Check-In Status Values

StatusWhen to use
CheckInStatus.InProgressJob has started, work is underway
CheckInStatus.OkJob completed successfully
CheckInStatus.ErrorJob failed — an error occurred

Sends two signals: InProgress at start and Ok/Error at end.
Enables detection of both missed jobs and timeout violations.

// Mark job as started — save the checkInId for correlation
var checkInId = SentrySdk.CaptureCheckIn("my-monitor-slug", CheckInStatus.InProgress);

try
{
    DoWork();

    // Mark as successful
    SentrySdk.CaptureCheckIn("my-monitor-slug", CheckInStatus.Ok, checkInId);
}
catch (Exception ex)
{
    // Mark as failed
    SentrySdk.CaptureCheckIn("my-monitor-slug", CheckInStatus.Error, checkInId);
    throw;
}

Pattern B: Heartbeat Check-In (Simpler)

Sends a single check-in after execution. Detects missed jobs only — cannot detect timeouts.

try
{
    DoWork();
    SentrySdk.CaptureCheckIn("my-monitor-slug", CheckInStatus.Ok);
}
catch
{
    SentrySdk.CaptureCheckIn("my-monitor-slug", CheckInStatus.Error);
    throw;
}

Optionally report the actual runtime duration:

var sw = Stopwatch.StartNew();
DoWork();
sw.Stop();

SentrySdk.CaptureCheckIn(
    "my-monitor-slug",
    CheckInStatus.Ok,
    duration: sw.Elapsed
);

Programmatic Monitor Configuration (Upsert)

Create or update a monitor directly from code via configureMonitorOptions. This is sent with the first check-in and is idempotent — safe to call on every run.

Crontab Schedule

var checkInId = SentrySdk.CaptureCheckIn(
    "my-scheduled-job",
    CheckInStatus.InProgress,
    configureMonitorOptions: options =>
    {
        options.Schedule = "0 2 * * *";       // 2 AM daily (crontab expression)
        options.CheckInMargin = 5;             // 5 min grace period before "missed"
        options.MaxRuntime = 30;               // alert if running longer than 30 min
        options.TimeZone = "America/New_York"; // IANA timezone
        options.FailureIssueThreshold = 2;     // create issue after 2 consecutive failures
        options.RecoveryThreshold = 1;         // resolve issue after 1 consecutive success
    }
);

Interval-Based Schedule

var checkInId = SentrySdk.CaptureCheckIn(
    "my-interval-job",
    CheckInStatus.InProgress,
    configureMonitorOptions: options =>
    {
        options.Interval(6, SentryMonitorInterval.Hour); // every 6 hours
        options.CheckInMargin = 30;
        options.MaxRuntime = 120;
        options.TimeZone = "UTC";
        options.FailureIssueThreshold = 1;
        options.RecoveryThreshold = 3;
    }
);

SentryMonitorInterval Values

ValueDescription
SentryMonitorInterval.MinutePer-minute interval
SentryMonitorInterval.HourPer-hour interval
SentryMonitorInterval.DayPer-day interval
SentryMonitorInterval.WeekPer-week interval
SentryMonitorInterval.MonthPer-month interval
SentryMonitorInterval.YearPer-year interval

Monitor Configuration Reference

OptionTypeDescription
SchedulestringStandard crontab expression (e.g., "*/15 * * * *")
Interval(n, unit)methodInterval-based schedule; alternative to Schedule
CheckInMarginintMinutes of grace period before a missing check-in is flagged
MaxRuntimeintMaximum allowed runtime in minutes before a timeout alert
TimeZonestringIANA timezone name (e.g., "UTC", "America/Chicago")
FailureIssueThresholdintConsecutive failures before a Sentry issue is opened
RecoveryThresholdintConsecutive successes before a Sentry issue is closed

ASP.NET Core — BackgroundService / IHostedService

The most common .NET pattern for scheduled jobs is a BackgroundService. Pair it with CaptureCheckIn for full monitoring:

using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Sentry;

public class NightlyReportJob : BackgroundService
{
    private readonly ILogger<NightlyReportJob> _logger;
    private const string MonitorSlug = "nightly-report";

    public NightlyReportJob(ILogger<NightlyReportJob> logger)
        => _logger = logger;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Wait until next scheduled time (e.g., 2 AM)
            await WaitUntilNextRunAsync(stoppingToken);

            var checkInId = SentrySdk.CaptureCheckIn(
                MonitorSlug,
                CheckInStatus.InProgress,
                configureMonitorOptions: o =>
                {
                    o.Schedule = "0 2 * * *"; // 2 AM daily
                    o.CheckInMargin = 15;
                    o.MaxRuntime = 60;
                    o.TimeZone = "UTC";
                    o.FailureIssueThreshold = 1;
                    o.RecoveryThreshold = 1;
                }
            );

            try
            {
                _logger.LogInformation("Starting nightly report generation");
                await GenerateReportAsync(stoppingToken);
                _logger.LogInformation("Nightly report completed successfully");

                SentrySdk.CaptureCheckIn(MonitorSlug, CheckInStatus.Ok, checkInId);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Nightly report failed");
                SentrySdk.CaptureCheckIn(MonitorSlug, CheckInStatus.Error, checkInId);
            }
        }
    }

    private async Task WaitUntilNextRunAsync(CancellationToken ct)
    {
        var now = DateTime.UtcNow;
        var nextRun = now.Date.AddDays(now.Hour >= 2 ? 1 : 0).AddHours(2);
        var delay = nextRun - now;
        if (delay > TimeSpan.Zero)
            await Task.Delay(delay, ct);
    }

    private Task GenerateReportAsync(CancellationToken ct) => Task.CompletedTask; // replace with real logic
}

Register the hosted service in Program.cs:

builder.Services.AddHostedService<NightlyReportJob>();

Minimal IHostedService Implementation

For simpler one-shot or timer-based jobs:

public class SyncJob : IHostedService, IDisposable
{
    private Timer? _timer;
    private const string MonitorSlug = "data-sync";

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(RunJob, null, TimeSpan.Zero, TimeSpan.FromHours(1));
        return Task.CompletedTask;
    }

    private void RunJob(object? state)
    {
        var checkInId = SentrySdk.CaptureCheckIn(
            MonitorSlug,
            CheckInStatus.InProgress,
            configureMonitorOptions: o =>
            {
                o.Interval(1, SentryMonitorInterval.Hour);
                o.CheckInMargin = 5;
                o.MaxRuntime = 30;
                o.TimeZone = "UTC";
            }
        );

        try
        {
            SyncData();
            SentrySdk.CaptureCheckIn(MonitorSlug, CheckInStatus.Ok, checkInId);
        }
        catch
        {
            SentrySdk.CaptureCheckIn(MonitorSlug, CheckInStatus.Error, checkInId);
            throw;
        }
    }

    private void SyncData() { /* real logic here */ }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose() => _timer?.Dispose();
}

Hangfire Integration

A dedicated Sentry.Hangfire package wraps check-ins automatically around Hangfire job execution:

dotnet add package Sentry.Hangfire

Register the integration when configuring Hangfire:

// Program.cs
builder.Services.AddHangfire(config =>
{
    config.UseSqlServerStorage(connectionString);
    config.UseSentry(); // ← enables automatic check-in wrapping
});
builder.Services.AddHangfireServer();

With Hangfire, check-ins are sent automatically for every recurring job — no manual CaptureCheckIn calls needed. Set the monitor slug using the job’s RecurringJobId.

See the Hangfire integration guide for full details.


Quartz.NET Integration

No official Quartz.NET package exists. Use CaptureCheckIn manually inside IJob.Execute():

using Quartz;
using Sentry;

[DisallowConcurrentExecution]
public class MyQuartzJob : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        var slug = $"quartz-{context.JobDetail.Key.Name}";

        var checkInId = SentrySdk.CaptureCheckIn(slug, CheckInStatus.InProgress,
            configureMonitorOptions: o =>
            {
                o.Schedule = "0 */6 * * *"; // every 6 hours
                o.CheckInMargin = 10;
                o.MaxRuntime = 60;
                o.TimeZone = "UTC";
            }
        );

        try
        {
            await DoWorkAsync(context.CancellationToken);
            SentrySdk.CaptureCheckIn(slug, CheckInStatus.Ok, checkInId);
        }
        catch (Exception ex)
        {
            SentrySdk.CaptureCheckIn(slug, CheckInStatus.Error, checkInId);
            throw new JobExecutionException(ex);
        }
    }
}

Long-Running Job Pattern (Heartbeat Loop)

For processes that run continuously and should check in periodically:

public class LongRunningProcessor : BackgroundService
{
    private const string MonitorSlug = "queue-processor";

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var checkInId = SentrySdk.CaptureCheckIn(
                MonitorSlug,
                CheckInStatus.InProgress,
                configureMonitorOptions: o =>
                {
                    o.Interval(5, SentryMonitorInterval.Minute);
                    o.CheckInMargin = 2;
                    o.MaxRuntime = 10;
                    o.TimeZone = "UTC";
                }
            );

            try
            {
                await ProcessBatchAsync(stoppingToken);
                SentrySdk.CaptureCheckIn(MonitorSlug, CheckInStatus.Ok, checkInId);
            }
            catch (Exception ex) when (!stoppingToken.IsCancellationRequested)
            {
                SentrySdk.CaptureCheckIn(MonitorSlug, CheckInStatus.Error, checkInId);
                // optionally capture the exception too
                SentrySdk.CaptureException(ex);
            }

            await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken);
        }
    }
}

Alerting

Create issue alerts in Sentry:
Alerts → Create Alert → Issues → filter by tag monitor.slug equals my-monitor-slug


Rate Limits

Cron check-ins are rate-limited to 6 check-ins per minute per monitor per environment. Each environment (production, staging, etc.) tracks independently. Exceeding this limit silently drops events — visible in Usage Stats.


SDK Version Matrix

FeatureMin SDK Version
SentrySdk.CaptureCheckIn()4.2.0
Heartbeat pattern4.2.0
Programmatic monitor upsert (configureMonitorOptions)4.2.0
Crontab schedule4.2.0
Interval schedule (SentryMonitorInterval)4.2.0
Sentry.Hangfire auto-integration4.x
Quartz.NET❌ Manual API only

Troubleshooting

IssueSolution
Check-ins not appearing in SentryVerify monitorSlug matches the slug configured in Sentry; check DSN is correct and SDK is initialized
Monitor shows “missed” despite job runningIncrease CheckInMargin to allow more grace time; check server clock sync (NTP)
Monitor shows “timeout”Increase MaxRuntime; investigate why the job exceeds the expected duration
Monitor not auto-createdPass configureMonitorOptions on the first CaptureCheckIn call — the upsert creates the monitor
CheckInStatus.Error but no Sentry issueConfigure FailureIssueThreshold = 1 on the monitor options to create issues on first failure
Hangfire jobs not sending check-insEnsure config.UseSentry() is called inside AddHangfire; verify Sentry SDK is initialized before Hangfire starts
Quartz jobs not monitoredNo official integration — add CaptureCheckIn manually inside IJob.Execute()
Duplicate check-ins from multiple instancesUse a distributed lock (e.g., IDistributedLock) around the check-in calls, or configure Quartz/Hangfire with single-instance scheduling

Reference: Error Monitoring

Error Monitoring — Sentry .NET SDK

Minimum SDK: Sentry ≥ 4.0.0 (NuGet)
ASP.NET Core integration: Sentry.AspNetCore ≥ 4.0.0
MAUI integration: Sentry.Maui ≥ 4.0.0
User feedback API: Sentry ≥ 4.0.0 (CaptureFeedback)


Automatic vs Manual Error Capture

What Is Captured Automatically

Error TypeCaptured?Mechanism
Unhandled exceptions (all platforms)✅ YesAppDomain.CurrentDomain.UnhandledException
Unobserved Task exceptions✅ YesTaskScheduler.UnobservedTaskException
ASP.NET Core request errors✅ YesSentry middleware
WPF Dispatcher unhandled exceptions✅ YesApplication.DispatcherUnhandledException (with hook)
MAUI unhandled exceptions✅ YesPlatform-specific native integrations
WinForms exceptions✅ YesRequires SetUnhandledExceptionMode(ThrowException)
Caught + swallowed try/catch❌ NoMust call SentrySdk.CaptureException() manually
Graceful error returns❌ NoMust call SentrySdk.CaptureException() manually

The Core Rule

“If you catch an exception and don’t re-throw it, Sentry never sees it.”

// ✅ Automatically captured — unhandled, bubbles up
throw new Exception("Unhandled");

// ✅ Automatically captured — re-thrown
try
{
    await DoSomethingAsync();
}
catch (Exception ex)
{
    throw; // re-throw preserves stack trace
}

// ❌ NOT captured — swallowed by graceful return
try
{
    await DoSomethingAsync();
}
catch (Exception ex)
{
    return Result.Failure("Operation failed"); // ← Sentry never sees this
}

// ✅ Manually captured
try
{
    await DoSomethingAsync();
}
catch (Exception ex)
{
    SentrySdk.CaptureException(ex);
    return Result.Failure("Operation failed");
}

Core Capture API

SentrySdk.CaptureException

// Basic — capture a caught exception
SentryId id = SentrySdk.CaptureException(exception);

// With inline scope enrichment — changes are isolated to this ONE event
SentryId id = SentrySdk.CaptureException(exception, scope =>
{
    scope.SetTag("order.id", orderId.ToString());
    scope.Level = SentryLevel.Fatal;
    scope.User = new SentryUser { Id = userId };
});

Key behavior: The SDK clones the current scope before invoking the callback. Changes inside the callback apply only to that one event and do not affect subsequent events.

SentrySdk.CaptureMessage

// Default level is Info
SentrySdk.CaptureMessage("Something notable happened");

// With explicit severity
SentrySdk.CaptureMessage("Disk space critically low", SentryLevel.Warning);

// With scope enrichment
SentrySdk.CaptureMessage("Payment gateway timeout", scope =>
{
    scope.SetTag("gateway", "stripe");
    scope.Level = SentryLevel.Error;
}, SentryLevel.Error);

SentryLevel values:

SentryLevel.Debug
SentryLevel.Info      // default for CaptureMessage
SentryLevel.Warning
SentryLevel.Error
SentryLevel.Fatal

SentrySdk.CaptureEvent

For full manual control over every field on the event:

var evt = new SentryEvent
{
    Message = new SentryMessage { Message = "Custom structured event" },
    Level = SentryLevel.Error
};
evt.SetTag("custom-tag", "value");
evt.Fingerprint = new[] { "custom-fingerprint" };
SentrySdk.CaptureEvent(evt);

// Construct from a caught exception
try { ... }
catch (Exception ex)
{
    var evt = new SentryEvent(ex)
    {
        Level = SentryLevel.Fatal
    };
    SentrySdk.CaptureEvent(evt);
}

All capture signatures

// CaptureException
SentryId SentrySdk.CaptureException(Exception exception)
SentryId SentrySdk.CaptureException(Exception exception, Action<Scope> configureScope)

// CaptureMessage
SentryId SentrySdk.CaptureMessage(string message, SentryLevel level = SentryLevel.Info)
SentryId SentrySdk.CaptureMessage(string message, Action<Scope> configureScope,
    SentryLevel level = SentryLevel.Info)

// CaptureEvent
SentryId SentrySdk.CaptureEvent(SentryEvent evt, Scope? scope = null, SentryHint? hint = null)
SentryId SentrySdk.CaptureEvent(SentryEvent evt, Action<Scope> configureScope)
SentryId SentrySdk.CaptureEvent(SentryEvent evt, SentryHint? hint, Action<Scope> configureScope)

// Flush
void  SentrySdk.Flush()
void  SentrySdk.Flush(TimeSpan timeout)
Task  SentrySdk.FlushAsync(TimeSpan timeout)

// Utility
bool      SentrySdk.IsEnabled
SentryId  SentrySdk.LastEventId

ASP.NET Core — Automatic & Manual Error Capture

Installation

dotnet add package Sentry.AspNetCore

Initialization in Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseSentry(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.SendDefaultPii = true;              // Include user IP, headers, auth
    options.MaxRequestBodySize = RequestSize.Always;
    options.TracesSampleRate = 1.0;
    options.Debug = true;
});

var app = builder.Build();
app.Run();

Via appsettings.json (no code required)

{
  "Sentry": {
    "Dsn": "https://[email protected]/...",
    "SendDefaultPii": true,
    "MaxRequestBodySize": "Always",
    "MinimumBreadcrumbLevel": "Debug",
    "MinimumEventLevel": "Warning",
    "AttachStacktrace": true,
    "Debug": true,
    "TracesSampleRate": 1.0
  }
}

Via environment variables (double-underscore convention)

Sentry__Dsn=https://[email protected]/...
Sentry__Debug=true
Sentry__TracesSampleRate=0.5
Sentry__SendDefaultPii=true

What ASP.NET Core captures automatically

  • All unhandled exceptions thrown from controllers and middleware → captured as Sentry events
  • HTTP request data (URL, method, headers, body if configured)
  • User info from IHttpContext when SendDefaultPii = true
  • Breadcrumbs from Microsoft.Extensions.Logging
  • Performance transactions for each HTTP request (when TracesSampleRate > 0)

Manual capture in a controller

[ApiController]
[Route("[controller]")]
public class OrderController : ControllerBase
{
    [HttpPost]
    public IActionResult CreateOrder(OrderRequest request)
    {
        try
        {
            _orderService.Create(request);
            return Ok();
        }
        catch (PaymentDeclinedException ex)
        {
            SentrySdk.CaptureException(ex, scope =>
            {
                scope.SetTag("payment.gateway", request.PaymentGateway);
                scope.SetExtra("order_amount", request.Amount);
            });
            return StatusCode(402, "Payment declined");
        }
    }
}

Custom user factory (DI)

public class MyUserFactory : ISentryUserFactory
{
    private readonly IHttpContextAccessor _accessor;

    public MyUserFactory(IHttpContextAccessor accessor)
        => _accessor = accessor;

    public SentryUser? Create()
    {
        var user = _accessor.HttpContext?.User;
        if (user?.Identity?.IsAuthenticated != true) return null;

        return new SentryUser
        {
            Id       = user.FindFirst(ClaimTypes.NameIdentifier)?.Value,
            Email    = user.FindFirst(ClaimTypes.Email)?.Value,
            Username = user.Identity.Name
        };
    }
}

// Register in DI
services.AddSingleton<ISentryUserFactory, MyUserFactory>();

ASP.NET Core-specific options

OptionTypeDescription
SendDefaultPiiboolInclude request URL, headers, user IP, auth info
MaxRequestBodySizeRequestSizeNone, Small (<4 KB), Medium (<10 KB), Always
MinimumBreadcrumbLevelLogLevelMin log level for breadcrumb capture from ILogger
MinimumEventLevelLogLevelMin log level to generate a Sentry error event from ILogger
CaptureBlockingCallsboolDetect Task.Wait() / .Result threadpool starvation

Scope Management

How Scopes Work in .NET

The Hub holds a stack of scopes. When an event is captured the hub merges the topmost scope’s data into the event. Scope storage mode is controlled by IsGlobalModeEnabled:

IsGlobalModeEnabledStorageUse For
false (default)AsyncLocal<T>Server apps — per-request isolation
trueSingletonDesktop apps — shared scope across threads

ConfigureScope — Persistent Changes

Modifies the current ambient scope permanently (until changed or scope is popped). Use for session-level data:

SentrySdk.ConfigureScope(scope =>
{
    scope.SetTag("tenant.id", tenantId);
    scope.User = new SentryUser
    {
        Id    = user.Id.ToString(),
        Email = user.Email
    };
    scope.Level = SentryLevel.Warning;
    scope.TransactionName = "UserCheckout";
});

// Async variant
await SentrySdk.ConfigureScopeAsync(async scope =>
{
    var user = await _context.Users.FindAsync(userId);
    scope.User = new SentryUser { Id = user.Id.ToString(), Email = user.Email };
});

// Allocation-free overload (avoids closure)
SentrySdk.ConfigureScope(
    static (scope, tenantId) => scope.SetTag("tenant.id", tenantId),
    currentTenantId);

PushScope — Temporary Isolated Scope

Inherits parent scope data. All changes inside the using block are discarded when disposed:

using (SentrySdk.PushScope())
{
    SentrySdk.ConfigureScope(scope =>
    {
        scope.SetTag("operation", "bulk-import");
        scope.User = new SentryUser { Id = userId };
    });

    SentrySdk.CaptureException(new Exception("Scoped error"));
} // scope is popped here — tags/user cleared

Inline scope callback (preferred for single events)

The configureScope callback on capture methods is the preferred pattern for one-off enrichment without needing a using block:

// Only this event carries the tag
SentrySdk.CaptureException(ex, scope =>
{
    scope.SetTag("action", "checkout");
    scope.Level = SentryLevel.Fatal;
});

// The next event is NOT affected
SentrySdk.CaptureException(otherEx);

Clearing scope data

SentrySdk.ConfigureScope(scope =>
{
    scope.User = new SentryUser();   // Clear user (e.g., on logout)
    scope.Clear();                   // Clear everything
    scope.ClearBreadcrumbs();
    scope.ClearAttachments();
});

Scope decision guide

GoalAPI
Data on ALL events (app version, build ID)options.DefaultTags["key"] = "value"
Session/request-level dataSentrySdk.ConfigureScope(...)
One specific event onlyInline configureScope callback on capture
Temporary sub-context (batch job, etc.)SentrySdk.PushScope() + using

Context Enrichment

Tags (Indexed, Searchable)

Tags are indexed — use them for filtering, grouping, and alerting rules.

SentrySdk.ConfigureScope(scope =>
{
    scope.SetTag("page.locale", "de-at");
    scope.SetTag("user.plan", "enterprise");

    // Set multiple at once
    scope.SetTags(new Dictionary<string, string>
    {
        ["environment"] = "staging",
        ["region"]      = "us-east-1"
    });

    // Unset a tag
    scope.UnsetTag("page.locale");
});

// Default tags for ALL events (set in options)
SentrySdk.Init(options =>
{
    options.DefaultTags["app.version"]     = "2.0.1";
    options.DefaultTags["deployment.region"] = "us-east-1";
});

Tag constraints: Keys ≤ 32 chars (a-zA-Z, 0-9, _, ., :, -); values ≤ 200 chars, no newlines.

User

SentrySdk.ConfigureScope(scope =>
{
    scope.User = new SentryUser
    {
        Id        = "42",
        Username  = "john.doe",
        Email     = "[email protected]",
        IpAddress = "{{auto}}"        // let Sentry infer from the connection
    };

    // Custom fields
    scope.User.Other["account_type"] = "premium";
    scope.User.Other["tenant_id"]    = "acme-corp";
});

// Clear user on logout
SentrySdk.ConfigureScope(scope => scope.User = new SentryUser());

SentryUser fields:

FieldTypeNotes
Idstring?Internal identifier
Usernamestring?Display label
Emailstring?Enables Gravatars and Sentry messaging
IpAddressstring?"{{auto}}" to infer from connection; auto-set when SendDefaultPii = true
OtherIDictionary<string, string>Arbitrary additional user data

Manual:

SentrySdk.AddBreadcrumb(
    message:  "User authenticated",
    category: "auth",
    level:    BreadcrumbLevel.Info);

// With structured data
SentrySdk.AddBreadcrumb(
    message:  "User navigated to checkout",
    category: "navigation",
    type:     "navigation",
    data:     new Dictionary<string, string>
    {
        ["from"] = "/cart",
        ["to"]   = "/checkout"
    },
    level: BreadcrumbLevel.Info);

// Using Breadcrumb object
var crumb = new Breadcrumb(
    message:  "Button clicked",
    type:     "user",
    data:     new Dictionary<string, string> { ["button_id"] = "submit" },
    category: "ui.click",
    level:    BreadcrumbLevel.Info);
SentrySdk.AddBreadcrumb(crumb);

BreadcrumbLevel values: Debug, Info (default), Warning, Error, Critical

Automatically captured breadcrumbs:

SourceRequires
HTTP requestsSentryHttpMessageHandler with HttpClient
Logs (Info+)Microsoft.Extensions.Logging, Serilog, NLog, log4net
Database queriesEF6 or EF Core via DiagnosticSource
MAUI app eventsNavigation, lifecycle, user interactions

Max breadcrumbs: 100 (default). Override with options.MaxBreadcrumbs = 50.

Custom Contexts (Structured, Non-Searchable)

SentrySdk.ConfigureScope(scope =>
{
    scope.Contexts["character"] = new
    {
        Name       = "Mighty Fighter",
        Age        = 19,
        AttackType = "melee"
    };

    scope.Contexts["build"] = new
    {
        Version  = "2.0.1",
        Commit   = "abc123",
        Pipeline = "main-ci"
    };
});

The key "type" is reserved — do not use it. Contexts are not searchable; use Tags for searchable data.

Tags vs Contexts vs Extra

FeatureSearchable?Indexed?Best For
Tags✅ Yes✅ YesFiltering, grouping, alerting
Contexts❌ No❌ NoStructured debug info (nested objects)
Extra (deprecated)❌ No❌ NoPrefer Contexts instead
User✅ Partially✅ YesUser attribution and filtering

BeforeSend and Filtering Hooks

BeforeSend — Modify or Drop Error Events

Called immediately before transmission — last in the processing pipeline. Return null to drop the event.

SentrySdk.Init(options =>
{
    // Simple variant
    options.SetBeforeSend(@event =>
    {
        // Drop noisy exceptions
        if (@event.Exception?.Message.Contains("Noisy Exception") == true)
            return null;

        // Scrub server name for privacy
        @event.ServerName = null;

        return @event;
    });

    // Full variant with SentryHint
    options.SetBeforeSend((@event, hint) =>
    {
        if (@event.Exception is SqlException sqlEx && sqlEx.Number == 1205)
        {
            // Deadlock — enrich rather than drop
            @event.SetTag("sql.error_number", sqlEx.Number.ToString());
        }

        return @event;
    });
});

BeforeSendTransaction — Modify or Drop Performance Events

options.SetBeforeSendTransaction((transaction, hint) =>
{
    if (transaction.Name == "GET /health")
        return null; // Drop health-check transactions
    return transaction;
});

BeforeBreadcrumb — Filter or Modify Breadcrumbs

options.SetBeforeBreadcrumb(breadcrumb =>
    breadcrumb.Category == "Spammy.Logger"
        ? null          // null DROPS the breadcrumb
        : breadcrumb);  // returning it KEEPS it (optionally modified)

// Full variant with hint
options.SetBeforeBreadcrumb((breadcrumb, hint) =>
{
    if (breadcrumb.Level == BreadcrumbLevel.Debug)
        return null;
    return breadcrumb;
});

BeforeSendLog

options.SetBeforeSendLog(log =>
{
    if (log.Level < SentryLevel.Warning)
        return null;
    return log;
});

Fingerprinting and Custom Grouping

All events have a fingerprint. Events with the same fingerprint group into the same issue. The default fingerprint is computed from the stack trace. Override it in BeforeSend or directly on a scope/event.

Group more aggressively (collapse all matching into one issue)

options.SetBeforeSend(@event =>
{
    if (@event.Exception is SqlConnectionException)
    {
        // All SqlConnectionExceptions → one issue
        @event.SetFingerprint(new[] { "database-connection-error" });
    }
    return @event;
});

Group with greater granularity (split issues using {{ default }})

options.SetBeforeSend(@event =>
{
    if (@event.Exception is MyRpcException ex)
    {
        @event.SetFingerprint(new[]
        {
            "{{ default }}",        // keep Sentry's default hash
            ex.Function,            // split by RPC function
            ex.Code.ToString()      // split by status code
        });
    }
    return @event;
});

Set fingerprint directly on scope or event

// On scope — applies to all subsequent events in this scope
SentrySdk.ConfigureScope(scope =>
{
    scope.Fingerprint = new[] { "my-custom-fingerprint" };
});

// On a specific event
var evt = new SentryEvent(exception);
evt.Fingerprint = new[] { "{{ default }}", "additional-key" };
SentrySdk.CaptureEvent(evt);

Fingerprint template variables

VariableDescription
{{ default }}Sentry’s normally computed hash (extend rather than replace)
{{ transaction }}Current transaction name
{{ function }}Top function in stack trace
{{ type }}Exception type name

Exception Filters

Filter by exception type

SentrySdk.Init(options =>
{
    // Also suppresses TaskCanceledException (derives from OperationCanceledException)
    options.AddExceptionFilterForType<OperationCanceledException>();
    options.AddExceptionFilterForType<MyBusinessException>();
});

Custom IExceptionFilter

public class MyExceptionFilter : IExceptionFilter
{
    public bool Filter(Exception ex)
    {
        // Return true to DROP the exception (not sent to Sentry)
        return ex is MyCustomException mce && mce.IsExpected;
    }
}

SentrySdk.Init(options =>
{
    options.AddExceptionFilter(new MyExceptionFilter());
});

Deduplication

SentrySdk.Init(options =>
{
    // Default: All ^ InnerException
    options.DeduplicateMode =
        DeduplicateMode.SameEvent |
        DeduplicateMode.SameExceptionInstance;

    // Disable entirely
    options.DisableDuplicateEventDetection();
});

DeduplicateMode flags: SameEvent, SameExceptionInstance, InnerException, AggregateException, All


Unhandled Exception Capture

WPF

// App.xaml.cs — must be in constructor, NOT OnStartup()
public partial class App : Application
{
    public App()
    {
        SentrySdk.Init(options =>
        {
            options.Dsn = "https://[email protected]/...";
            options.IsGlobalModeEnabled = true; // Required for desktop apps
            options.TracesSampleRate = 1.0;
        });

        // Hook WPF dispatcher-level unhandled exceptions
        DispatcherUnhandledException += App_DispatcherUnhandledException;
    }

    void App_DispatcherUnhandledException(
        object sender, DispatcherUnhandledExceptionEventArgs e)
    {
        SentrySdk.CaptureException(e.Exception);
        e.Handled = true; // Prevent the WPF default crash dialog
    }
}

IsGlobalModeEnabled = true is required for WPF — ensures background thread exceptions share the same scope as the UI thread.

Critical: Initialize in the App() constructor, not OnStartup(). The constructor runs before any dispatcher frames, ensuring the unhandled exception hook is registered first.

MAUI

// MauiProgram.cs
public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseSentry(options =>
        {
            options.Dsn = "https://[email protected]/...";
            options.TracesSampleRate = 1.0;

            // Optional — all false by default (PII risk)
            options.IncludeTextInBreadcrumbs               = false;
            options.IncludeTitleInBreadcrumbs              = false;
            options.IncludeBackgroundingStateInBreadcrumbs = false;
        });
    return builder.Build();
}

MAUI platform coverage:

PlatformIntegration
AndroidAppDomainUnhandledExceptionIntegration + native Android SDK
iOS / Mac CatalystRuntimeMarshalManagedExceptionIntegration + native Cocoa SDK
Windows (WinUI)AppDomainUnhandledExceptionIntegration + WinUIUnhandledExceptionIntegration

Windows Forms

// Program.cs
[STAThread]
static void Main()
{
    Application.EnableVisualStyles();
    Application.SetCompatibleTextRenderingDefault(false);

    // REQUIRED: makes WinForms re-throw instead of swallowing exceptions
    Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException);

    using (SentrySdk.Init(options =>
    {
        options.Dsn = "https://[email protected]/...";
        options.IsGlobalModeEnabled = true;
        options.TracesSampleRate = 1.0;
    }))
    {
        Application.Run(new MainForm());
    }
}

Console App

// Program.cs
SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.TracesSampleRate = 1.0;
});

// SDK 3.31.0+ handles flush on exit automatically
// For older SDKs, wrap in: using var _ = SentrySdk.Init(...);

Disabling Built-in Integrations

SentrySdk.Init(options =>
{
    options.DisableAppDomainUnhandledExceptionCapture();
    options.DisableUnobservedTaskExceptionCapture();
    options.DisableAppDomainProcessExitFlush();
    options.DisableRuntimeMarshalManagedExceptionCapture(); // iOS/MacCatalyst
});

Event Processors

Unlike BeforeSend (only one allowed), multiple event processors can be registered at different scopes:

// Global — runs for all events
public class MyEventProcessor : ISentryEventProcessor
{
    public SentryEvent? Process(SentryEvent @event)
    {
        if (@event.Exception is BackgroundJobException)
            return null; // Drop — null discards the event

        @event.SetTag("app.layer", "background-worker");
        @event.ServerName = null; // Scrub hostname

        return @event;
    }
}

// Register globally via options
SentrySdk.Init(options =>
{
    options.AddEventProcessor(new MyEventProcessor());
});

// Register on current + following scopes
SentrySdk.ConfigureScope(scope =>
{
    scope.AddEventProcessor(new MyEventProcessor());
});

// Register for a single event only
SentrySdk.CaptureException(ex, scope =>
{
    scope.AddEventProcessor(new MyEventProcessor());
});

Exception processor (runs before the main chain)

public class MyExceptionProcessor : ISentryEventExceptionProcessor
{
    public void Process(Exception exception, SentryEvent sentryEvent)
    {
        if (exception is HttpRequestException httpEx)
        {
            sentryEvent.SetTag("http.status", httpEx.StatusCode?.ToString() ?? "unknown");
        }
    }
}

SentrySdk.Init(options =>
{
    options.AddExceptionProcessor(new MyExceptionProcessor());
});

Processor execution order:

  1. ISentryEventExceptionProcessor — exception-specific processors
  2. ISentryEventProcessor — general event processors
  3. SetBeforeSend / SetBeforeSendTransactionalways last

User Feedback

Programmatic API

// Capture an event first to get an ID
var eventId = SentrySdk.CaptureMessage("An event that will receive user feedback.");

// Submit user feedback linked to that event
SentrySdk.CaptureFeedback(
    message:           "It broke when I clicked submit.",
    contactEmail:      "[email protected]",
    name:              "Jane Doe",
    associatedEventId: eventId);

Full signature:

SentryId SentrySdk.CaptureFeedback(
    string message,
    string? contactEmail      = null,
    string? name              = null,
    string? replayId          = null,
    string? url               = null,
    SentryId? associatedEventId = null,
    Scope? scope              = null,
    SentryHint? hint          = null)

Using SentryFeedback object:

var feedback = new SentryFeedback(
    message:           "The checkout button is broken.",
    contactEmail:      "[email protected]",
    name:              "John Smith",
    associatedEventId: SentrySdk.LastEventId);

SentrySdk.CaptureFeedback(feedback);

Validation: Sentry rejects feedback with invalid email addresses. Pre-validate email format before calling the API.

Crash-Report Modal (JavaScript widget on error pages)

For ASP.NET Core web apps, show the browser-based report dialog on error response pages:

<!-- Include Sentry JS SDK -->
<script
  src="https://browser.sentry-cdn.com/10.40.0/bundle.min.js"
  crossorigin="anonymous">
</script>

<!-- Show dialog with the server-side event ID -->
<script>
  Sentry.init({ dsn: "https://[email protected]/..." });
  Sentry.showReportDialog({ eventId: "@ViewBag.SentryEventId" });
</script>
// In your error controller or exception handler middleware
ViewBag.SentryEventId = SentrySdk.LastEventId;

Error Capture Quick Reference

Scenario Coverage Table

ScenarioAuto Captured?Solution
Unhandled exception (all frameworks)✅ YesAppDomain.UnhandledException integration
Unobserved Task exception✅ YesTaskScheduler.UnobservedTaskException integration
ASP.NET Core request error✅ YesSentry middleware
WPF DispatcherUnhandledException✅ YesHook in App() constructor
MAUI unhandled exception✅ YesPlatform-specific native integrations
WinForms unhandled exception✅ YesRequires SetUnhandledExceptionMode(ThrowException)
try/catch with graceful return❌ NoSentrySdk.CaptureException(ex) before return
try/catch with re-throw✅ YesBubbles to unhandled exception handler
Background thread exception✅ YesIsGlobalModeEnabled = true for desktop apps

API Quick Reference

// ── Capture ───────────────────────────────────────────────────────────────
SentrySdk.CaptureException(ex)
SentrySdk.CaptureException(ex, scope => { scope.SetTag("key", "val"); })
SentrySdk.CaptureMessage("text")
SentrySdk.CaptureMessage("text", SentryLevel.Warning)

// ── User ──────────────────────────────────────────────────────────────────
SentrySdk.ConfigureScope(scope => scope.User = new SentryUser { Id = "42", Email = "..." });
SentrySdk.ConfigureScope(scope => scope.User = new SentryUser()); // clear on logout

// ── Tags (searchable) ─────────────────────────────────────────────────────
SentrySdk.ConfigureScope(scope => scope.SetTag("key", "value"));
SentrySdk.ConfigureScope(scope => scope.UnsetTag("key"));

// ── Contexts (structured, non-searchable) ─────────────────────────────────
SentrySdk.ConfigureScope(scope => scope.Contexts["name"] = new { Key = "value" });

// ── Breadcrumbs ───────────────────────────────────────────────────────────
SentrySdk.AddBreadcrumb(message: "...", category: "auth", level: BreadcrumbLevel.Info);

// ── Scope isolation ───────────────────────────────────────────────────────
using (SentrySdk.PushScope())
{
    SentrySdk.ConfigureScope(scope => scope.SetTag("key", "value"));
    SentrySdk.CaptureException(ex);
} // tag is cleared after this block

// ── Fingerprinting ────────────────────────────────────────────────────────
SentrySdk.ConfigureScope(scope => scope.Fingerprint = new[] { "group-key" });
// In BeforeSend: @event.SetFingerprint(new[] { "{{ default }}", "extra-dim" });

// ── Hooks (in SentrySdk.Init) ─────────────────────────────────────────────
options.SetBeforeSend((@event, hint) => @event)         // return null to drop
options.SetBeforeSendTransaction((txn, hint) => txn)
options.SetBeforeBreadcrumb((crumb, hint) => crumb)     // return null to drop
options.AddExceptionFilterForType<OperationCanceledException>()

// ── Flush ─────────────────────────────────────────────────────────────────
SentrySdk.Flush(TimeSpan.FromSeconds(5));
await SentrySdk.FlushAsync(TimeSpan.FromSeconds(5));

Configuration Options Reference

OptionTypeDefaultDescription
DsnstringDSN from Sentry project settings; also reads SENTRY_DSN env var
Releasestring?App release version; also reads SENTRY_RELEASE
Environmentstring?Deployment environment; also reads SENTRY_ENVIRONMENT
SampleRatefloat1.0Error event sampling rate (0–1)
TracesSampleRatedouble0Transaction sampling rate (0–1)
AttachStacktracebooltrueAttach stack traces to message events too
SendDefaultPiiboolfalseInclude IP, username, headers
MaxBreadcrumbsint100Max breadcrumbs per event
IsGlobalModeEnabledboolfalseSingleton scope for desktop apps
DebugboolfalseLog SDK diagnostics to console
DiagnosticLevelSentryLevelDebugMin level for SDK diagnostic logs
DeduplicateModeDeduplicateModeAll ^ InnerExceptionDuplicate event detection strategy
MaxAttachmentSizelong20 MiBMax attachment size in bytes
DefaultTagsIDictionary<string, string>{}Tags added to every event
CacheDirectoryPathstring?nullPath for offline envelope caching
ShutdownTimeoutTimeSpan2sFlush timeout on SDK shutdown
CaptureFailedRequestsbooltrueCapture HTTP client error responses
EnableLogsboolfalseEnable Sentry structured logging
StackTraceModeStackTraceModeEnhancedEnhanced or Original

Troubleshooting

IssueSolution
Caught exceptions not appearing in SentryAny try/catch that doesn’t re-throw must call SentrySdk.CaptureException(ex) before returning
WPF exceptions from background threads missingSet options.IsGlobalModeEnabled = true; initialize in App() constructor, not OnStartup()
WinForms exceptions not capturedCall Application.SetUnhandledExceptionMode(UnhandledExceptionMode.ThrowException) before SentrySdk.Init
Events dropped after process exit (console/CLI)SDK 3.31.0+ handles this automatically; on older versions wrap Init result in using var _ = SentrySdk.Init(...)
Stack traces show minified/optimized framesEnable symbol upload via MSBuild properties (SentryOrg, SentryProject, SentryAuthToken)
Duplicate events in SentryCheck DeduplicateMode; AggregateException wrapping can cause same exception to appear multiple times
Missing user data on eventsFor ASP.NET Core, enable SendDefaultPii = true or register a custom ISentryUserFactory; for desktop apps ensure IsGlobalModeEnabled = true
OperationCanceledException flooding SentryFilter with options.AddExceptionFilterForType<OperationCanceledException>()
Events not sent before Lambda/Azure Functions cold start endsCall await SentrySdk.FlushAsync(TimeSpan.FromSeconds(5)) at the end of your handler
SDK reports IsEnabled = falseDSN not set or set to empty string; check SENTRY_DSN env var or options initialization order

Reference: Logging

Logging — Sentry .NET SDK

Minimum SDK: Sentry ≥ 5.14.0 for native SentrySdk.Logger + EnableLogs
Integration packages (Sentry.Extensions.Logging, Sentry.Serilog, Sentry.NLog, Sentry.Log4Net) available since SDK ≥ 4.x
Native structured logs forwarded through integration packages: requires SDK ≥ 6.1.0


Enabling Native Structured Logs

EnableLogs must be set to true — logging is disabled by default:

SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/0";
    options.EnableLogs = true; // Required — logs are silently no-ops without this
});

Without EnableLogs = true, all SentrySdk.Logger.* calls are silently discarded.


Native Logger API — Six Levels

The native logger type is SentryStructuredLogger, accessed via SentrySdk.Logger:

SentrySdk.Logger.LogTrace("Entering method Foo");
SentrySdk.Logger.LogDebug("Loaded {0} items", itemCount);
SentrySdk.Logger.LogInfo("Order created successfully");
SentrySdk.Logger.LogWarning("Cache miss for key {0}", cacheKey);
SentrySdk.Logger.LogError("A {0} error occurred", "critical");
SentrySdk.Logger.LogFatal("Unrecoverable error — shutting down");
LevelMethodTypical Use
TraceLogTrace()Ultra-granular method entry/exit; high-volume — filter in production
DebugLogDebug()Development diagnostics, cache hits/misses
InfoLogInfo()Normal business milestones, confirmations
WarningLogWarning()Degraded state, approaching limits, recoverable issues
ErrorLogError()Failures requiring attention
FatalLogFatal()Critical failures, system unavailable

Attaching Custom Attributes

Use the lambda overload to attach typed key-value attributes to a log entry:

SentrySdk.Logger.LogWarning(static log =>
{
    log.SetAttribute("request.id", 12345);
    log.SetAttribute("user.tier", "premium");
    log.SetAttribute("is.retried", true);
}, "Payment declined for order {0}", orderId);

Supported Attribute Value Types

CategoryTypes
Textualstring, char
Logicalbool
Integralsbyte, byte, short, ushort, int, uint, long, nint
Floating-pointfloat, double
OtherAny type via ToString() fallback

Log Filtering — SetBeforeSendLog

Use SetBeforeSendLog to modify or drop logs before transmission. Return null to discard:

SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.EnableLogs = true;

    options.SetBeforeSendLog(static log =>
    {
        // Drop all Info and Trace logs in production
        if (log.Level is SentryLogLevel.Info or SentryLogLevel.Trace)
            return null;

        // Drop noisy health-check messages
        if (log.Message?.Contains("/health") == true)
            return null;

        // Enrich surviving logs
        log.SetAttribute("app.version", "2.1.0");

        return log;
    });
});

The SentryLog Object

MemberTypeDescription
TimestampDateTimeOffsetWhen the log was created
TraceIdSentryIdActive trace ID — links log to a trace
SpanIdSpanId?Active span ID — links log to a span
LevelSentryLogLevelTrace, Debug, Info, Warning, Error, Fatal
MessagestringFormatted log message
Templatestring?Original message template (if structured)
ParametersImmutableArrayTemplate parameters
TryGetAttribute()methodRead an attribute
SetAttribute()methodWrite/modify an attribute

Automatically Attached Attributes

These are added by the SDK to every log without any configuration:

Attribute KeySource
environmentSDK config
releaseSDK config
sdk.name, sdk.versionSDK internals
message.templateMessage template
message.parameter.0, .1, …Template parameters
server.addressHost info
user.id, user.name, user.emailActive scope user (requires SendDefaultPii = true)
originIntegration that created the log
sentry.trace.parent_span_idWhen inside an active span (enables log ↔ trace correlation)

Integration: Microsoft.Extensions.Logging (ILogger)

Install

dotnet add package Sentry.Extensions.Logging

What it does

The MEL integration provides three capabilities simultaneously:

  1. Stores log messages as breadcrumbs (attached to the next error event as context)
  2. Sends logs at or above the event threshold as Sentry error events
  3. Forwards logs as native Sentry structured logs (SDK ≥ 6.1.0)
// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.Logging.AddSentry(o =>
{
    o.Dsn = "https://[email protected]/...";
    o.MinimumBreadcrumbLevel = LogLevel.Debug;   // default: Information
    o.MinimumEventLevel = LogLevel.Error;         // default: Error
    o.InitializeSdk = true;                       // set false if using SentrySdk.Init elsewhere
});

Or configure via appsettings.json:

{
  "Sentry": {
    "Dsn": "https://[email protected]/...",
    "MinimumBreadcrumbLevel": "Information",
    "MinimumEventLevel": "Error",
    "SendDefaultPii": true,
    "MaxBreadcrumbs": 100
  }
}
builder.Logging.AddSentry(); // reads Sentry section from appsettings.json

Direct Setup (no DI)

var loggerFactory = LoggerFactory.Create(logging =>
{
    logging.AddSentry(o => o.Dsn = "https://[email protected]/...");
});
ILogger logger = loggerFactory.CreateLogger<MyClass>();

Usage

Once configured, use standard ILogger<T> — no Sentry-specific code required:

public class OrderService
{
    private readonly ILogger<OrderService> _logger;

    public OrderService(ILogger<OrderService> logger) => _logger = logger;

    public async Task ProcessOrderAsync(int orderId)
    {
        _logger.LogInformation("Processing order {OrderId}", orderId); // → breadcrumb

        try
        {
            await _paymentService.ChargeAsync(orderId);
            _logger.LogInformation("Order {OrderId} paid successfully", orderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process order {OrderId}", orderId); // → Sentry event
            throw;
        }
    }
}

Configuration Options

OptionTypeDefaultDescription
MinimumBreadcrumbLevelLogLevelInformationThreshold for breadcrumb storage
MinimumEventLevelLogLevelErrorThreshold for sending Sentry error events
InitializeSdkbooltrueAuto-init SDK. Set false when using SentrySdk.Init
FiltersICollection<ILogEntryFilter>Custom pre-processing filters
TagFiltersICollection<string>Prefix-based tag exclusions

Important Behavior Notes

  • Breadcrumb cascade: A LogError event includes ALL breadcrumbs accumulated since the last event — so the full Info/Warning/Error history is attached.
  • Self-filtering: Messages from assemblies starting with "Sentry" are excluded to prevent infinite loops.
  • Single init: Set InitializeSdk = false if calling SentrySdk.Init() elsewhere in your startup.
  • Empty DSN disables the SDK entirely.

Integration: Serilog

Install

dotnet add package Sentry.Serilog

What it does

Same three capabilities as MEL: breadcrumbs, Sentry error events, and native structured logs.

Basic Setup (Serilog initializes Sentry)

Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .WriteTo.Sentry(o =>
    {
        o.Dsn = "https://[email protected]/...";
        o.MinimumBreadcrumbLevel = LogEventLevel.Debug;   // default: Information
        o.MinimumEventLevel = LogEventLevel.Warning;      // default: Error
    })
    .WriteTo.Console()
    .CreateLogger();

Setup (Sentry initialized separately)

SentrySdk.Init(o => o.Dsn = "...");

Log.Logger = new LoggerConfiguration()
    .WriteTo.Sentry(o =>
    {
        o.InitializeSdk = false; // ← avoid double-init
        o.MinimumBreadcrumbLevel = LogEventLevel.Information;
        o.MinimumEventLevel = LogEventLevel.Error;
    })
    .CreateLogger();

ASP.NET Core with Serilog

builder.Host.UseSerilog((ctx, cfg) =>
{
    cfg.ReadFrom.Configuration(ctx.Configuration)
       .WriteTo.Sentry(o =>
       {
           o.Dsn = ctx.Configuration["Sentry:Dsn"];
           o.MinimumBreadcrumbLevel = LogEventLevel.Debug;
           o.MinimumEventLevel = LogEventLevel.Error;
       });
});

Usage

var log = Log.ForContext<OrderService>();

log.Information("Processing order {OrderId} for {CustomerId}", orderId, customerId); // → breadcrumb
log.Error(ex, "Payment failed for order {OrderId}", orderId); // → Sentry event

Configuration Options

OptionDefaultDescription
MinimumBreadcrumbLevelInformationMinimum LogEventLevel for breadcrumbs
MinimumEventLevelErrorMinimum level for Sentry error events
InitializeSdktrueWhether this sink initializes the SDK

Integration: NLog

Install

dotnet add package Sentry.NLog

Code-Based Configuration

LogManager.Configuration = new LoggingConfiguration();

LogManager.Configuration.AddSentry(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.Layout = "${message}";
    options.BreadcrumbLayout = "${logger}: ${message}";
    options.MinimumBreadcrumbLevel = LogLevel.Debug;  // default: Info
    options.MinimumEventLevel = LogLevel.Error;        // default: Error
    options.AddTag("logger", "${logger}");
    options.IgnoreEventsWithNoException = false;
    options.SendEventPropertiesAsData = true;
    options.SendEventPropertiesAsTags = false;
});

LogManager.ReconfigExistingLoggers();

XML Configuration (nlog.config)

<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <extensions>
    <add assembly="Sentry.NLog"/>
  </extensions>

  <targets>
    <target xsi:type="Sentry"
            name="sentry"
            dsn="https://[email protected]/..."
            minimumBreadcrumbLevel="Debug"
            minimumEventLevel="Error"
            layout="${message}"
            breadcrumbLayout="${logger}: ${message}"
            sendEventPropertiesAsData="true"
            ignoreEventsWithNoException="false">
      <tag name="logger" layout="${logger}"/>
    </target>
  </targets>

  <rules>
    <!-- Set minlevel LOWER than breadcrumbLevel so SentryTarget sees all entries -->
    <logger name="*" minlevel="Debug" writeTo="sentry"/>
  </rules>
</nlog>

Usage

private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

public void ProcessOrder(int orderId)
{
    Logger.Info("Processing order {orderId}", orderId); // → breadcrumb

    try { /* ... */ }
    catch (Exception ex)
    {
        Logger.Error(ex, "Failed to process order {orderId}", orderId); // → Sentry event
    }
}

Configuration Options

OptionDefaultDescription
MinimumBreadcrumbLevelInfoThreshold for breadcrumb storage
MinimumEventLevelErrorThreshold for Sentry error events
InitializeSdktrueAuto-init SDK when DSN provided
IgnoreEventsWithNoExceptionfalseSkip entries with no attached exception
SendEventPropertiesAsDatatrueForward NLog properties as Sentry event data
SendEventPropertiesAsTagsfalseForward NLog properties as Sentry tags
IncludeEventDataOnBreadcrumbsfalseAttach event property data to breadcrumbs
BreadcrumbLayoutNLog layout string for breadcrumb text
LayoutNLog layout string for event message
TagsAdditional static tags attached to all messages

⚠️ Critical NLog Detail

The SentryTarget must receive all log entries to correctly classify them as breadcrumbs vs events. Configure NLog’s minlevel lower than MinimumBreadcrumbLevel:

<!-- If MinimumBreadcrumbLevel = Info, set minlevel = Debug or Trace -->
<logger name="*" minlevel="Debug" writeTo="sentry"/>

Integration: log4net

Install

dotnet add package Sentry.Log4Net

XML Configuration (app.config / web.config)

<configuration>
  <configSections>
    <section name="log4net"
             type="log4net.Config.Log4NetConfigurationSectionHandler, log4net"/>
  </configSections>

  <log4net>
    <appender name="SentryAppender" type="Sentry.Log4Net.SentryAppender, Sentry.Log4Net">
      <Dsn value="https://[email protected]/..."/>
      <SendIdentity value="true"/>  <!-- send log4net Identity as Sentry user.id -->
      <threshold value="INFO"/>     <!-- minimum level for this appender -->
    </appender>

    <root>
      <level value="DEBUG"/>
      <appender-ref ref="SentryAppender"/>
    </root>
  </log4net>
</configuration>

Programmatic Setup (for full SDK control)

The XML appender supports only a subset of Sentry options. For full control, init the SDK separately and omit the Dsn element to skip auto-init:

// Startup code
SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.Release = "[email protected]";
    options.TracesSampleRate = 0.1;
});
<!-- In app.config — no <Dsn> element means SDK won't be re-initialized -->
<appender name="SentryAppender" type="Sentry.Log4Net.SentryAppender, Sentry.Log4Net">
  <SendIdentity value="true"/>
  <threshold value="INFO"/>
</appender>

Usage

private static readonly ILog Logger = LogManager.GetLogger(typeof(MyClass));

Logger.Info("Processing started");       // → breadcrumb
Logger.Warn("Low disk space warning");   // → breadcrumb
Logger.Error("DB connection failed");    // → Sentry event
Logger.Fatal("Application crash", ex);  // → Sentry event

Key Appender Options

OptionDescription
DsnAuto-initializes SDK when provided
SendIdentityReports log4net Identity as user.id
thresholdMinimum log4net level for this appender

Log-to-Trace Correlation

Every log entry from any integration automatically carries the active trace and span IDs:

FieldDescription
TraceIdLinks the log to an active distributed trace
SpanIdLinks the log to the currently active span

In the Sentry UI you can navigate from an error or trace directly to the logs that occurred during that trace, and vice versa. No extra configuration required — correlation is automatic when TracesSampleRate > 0.


Log Level Mapping

Sentry LevelMEL (ILogger)SerilogNLoglog4net
TraceTraceVerboseTrace
DebugDebugDebugDebugDEBUG
InfoInformationInformationInfoINFO
WarningWarningWarningWarnWARN
ErrorErrorErrorErrorERROR
FatalCriticalFatalFatalFATAL

SDK Version Matrix

FeatureMin SDK Version
Native SentrySdk.Logger + EnableLogs5.14.0
Sentry.Extensions.Logging4.x
Sentry.Serilog4.x
Sentry.NLog4.x
Sentry.Log4Net4.x
Native logs forwarded via integration packages6.1.0

Troubleshooting

IssueSolution
Native logs not appearing in SentryVerify EnableLogs = true in SentrySdk.Init() — without it, all SentrySdk.Logger.* calls are silently discarded
MEL/Serilog/NLog logs not triggering Sentry eventsCheck MinimumEventLevel — only logs at or above this threshold are sent as events; lower it if needed
NLog: only Error/Fatal seen, no breadcrumbsNLog <logger minlevel> must be set lower than MinimumBreadcrumbLevel so the SentryTarget receives all entries
SDK initialized twice (double events)Set InitializeSdk = false in the logging integration when you also call SentrySdk.Init() in startup
Logs not linked to tracesEnsure TracesSampleRate > 0 and the log is emitted inside an active span
Sensitive data appearing in logsAdd filtering in SetBeforeSendLog; better yet, avoid logging sensitive values at the call site
SetBeforeSendLog not firingConfirm EnableLogs = true — without it, no logs are processed and the hook never runs
log4net: SDK not receiving Identity as user.idSet <SendIdentity value="true"/> in the appender config
High log volume / rate limitsUse SetBeforeSendLog to drop Trace and Debug levels in production

Reference: Profiling

Profiling — Sentry .NET SDK

Alpha featureSentry.Profiling NuGet package
Minimum SDK: Sentry.Profiling ≥ 4.0.0 · .NET 8.0+ required
Not supported: .NET Framework, Android, Blazor WASM, Native AOT (except iOS/Mac Catalyst)


Overview

The Sentry .NET SDK captures CPU profiles using the .NET EventPipe (System.Diagnostics.DiagnosticSource) sampling infrastructure. Profiles attach to transactions — they are not standalone events.

PlatformMechanismPackage required
.NET 8+ on WindowsEventPipe CPU samplingSentry.Profiling
.NET 8+ on LinuxEventPipe CPU samplingSentry.Profiling ⚠️ see Linux note
.NET 8+ on macOSEventPipe CPU samplingSentry.Profiling
iOS / Mac CatalystNative Mono AOT profilerNone (built into Sentry.Maui)
.NET Framework❌ Not supported
Android❌ Not supported
Blazor WebAssembly❌ Not supported
Native AOT (non-iOS)❌ Not supported

How Profiling Attaches to Traces

Profiles are always tied to a transaction — you must have tracing enabled first:

TracesSampleRate × ProfilesSampleRate = net profiling rate

Example:
  TracesSampleRate   = 0.5  →  50% of requests create transactions
  ProfilesSampleRate = 0.4  →  40% of those transactions get profiled
  Net profiling rate        =  20% of all requests

When a transaction starts:

  1. ProfilingIntegration checks whether this transaction should be profiled (per ProfilesSampleRate)
  2. If yes, an EventPipe session starts collecting CPU samples (~100 Hz)
  3. When transaction.Finish() is called, the profiler stops and attaches the profile data to the transaction envelope
  4. Both the transaction and the profile are sent to Sentry together — you can drill from a slow span directly into a flame graph

One profiler at a time: Only one profile can be active per process. Nested transactions will not each receive their own profile.


Installation

dotnet add package Sentry.Profiling

Do NOT install Sentry.Profiling for iOS or Mac Catalyst. Those platforms use the native Mono AOT profiler bundled inside Sentry.Maui — installing this package on those targets has no effect.


Basic Setup

Profiling requires three additions to your SentrySdk.Init call:

SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/0";

    // Step 1: Enable tracing (REQUIRED — profiling won't work without it)
    options.TracesSampleRate = 1.0;

    // Step 2: Set what fraction of sampled transactions get profiled
    options.ProfilesSampleRate = 1.0;  // 1.0 = 100% for development; lower in production

    // Step 3: Register the profiling integration
    options.AddProfilingIntegration();
});

ASP.NET Core

// Program.cs
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseSentry(options =>
{
    options.Dsn = "https://[email protected]/0";
    options.TracesSampleRate = 1.0;
    options.ProfilesSampleRate = 0.1;   // Profile 10% of sampled transactions in production
    options.AddProfilingIntegration();
});

var app = builder.Build();
app.UseSentry();  // Must appear before other middleware
app.MapControllers();
app.Run();

AddProfilingIntegration() initializes the EventPipe session asynchronously on a background thread. Transactions that start immediately after SentrySdk.Init() may not get a profile because the profiler isn’t ready yet.

// ❌ Problem: profiler may not be ready when first transaction starts
SentrySdk.Init(options => {
    options.AddProfilingIntegration();  // async startup
});
var tx = SentrySdk.StartTransaction("startup", "init");  // profiler might miss this
// ✅ Fix: provide a timeout to block until profiler is ready
SentrySdk.Init(options => {
    options.AddProfilingIntegration(TimeSpan.FromMilliseconds(500));
});
var tx = SentrySdk.StartTransaction("startup", "init");  // profiler guaranteed ready

iOS/Mac Catalyst note: The native Mono profiler always starts synchronously. The TimeSpan parameter is accepted but has no effect on those platforms.


Configuration Options

OptionTypeDefaultDescription
TracesSampleRatedouble?nullRequired. Fraction of requests that create transactions (0.0–1.0). Profiling does nothing without this.
TracesSamplerFunc<TransactionSamplingContext, double?>nullAlternative to TracesSampleRate for dynamic per-request sampling. Takes precedence when set.
ProfilesSampleRatedouble?nullFraction of sampled transactions that get profiled (0.0–1.0). Null = profiling disabled.
AddProfilingIntegration()Registers SamplingTransactionProfilerFactory. Required.
AddProfilingIntegration(TimeSpan)TimeSpanSame as above, but blocks synchronously until the EventPipe session starts (or timeout). Recommended for most apps.
SentrySdk.Init(options =>
{
    options.Dsn = "...";

    // Sample 20% of transactions
    options.TracesSampleRate = 0.2;

    // Profile 50% of those — net 10% of all requests get profiled
    options.ProfilesSampleRate = 0.5;

    // Block up to 500ms so early-startup transactions are captured
    options.AddProfilingIntegration(TimeSpan.FromMilliseconds(500));
});

Platform-Specific Notes

Linux

⚠️ Known issue (open as of Feb 2026, #4815): AddProfilingIntegration() can throw a ReflectionTypeLoadException on startup in Linux containers. This is caused by Dia2Lib.dll and TraceReloggerLib.dll — Windows-only PDB resolver DLLs that are referenced by the profiler’s tracing stack.

Mitigation: Wrap AddProfilingIntegration() in a try/catch and log the failure gracefully:

try
{
    options.AddProfilingIntegration();
}
catch (Exception ex)
{
    Console.Error.WriteLine($"[Sentry] Profiling unavailable on this platform: {ex.Message}");
}

Test profiling thoroughly on your specific Linux image before enabling in production.

iOS / Mac Catalyst

Use the native Mono AOT profiler. No installation needed — it’s built into Sentry.Maui.

// iOS: same configuration, but do NOT install Sentry.Profiling
SentrySdk.Init(options =>
{
    options.Dsn = "...";
    options.TracesSampleRate = 1.0;
    options.ProfilesSampleRate = 1.0;
    options.AddProfilingIntegration();  // delegates to native profiler on iOS/Mac Catalyst
});

Windows

Fully supported. Dia2Lib.dll is a Windows-native dependency and loads correctly. No extra steps required.


Limitations and Known Issues

LimitationDetails
Alpha statusThe profiling feature is officially in Alpha as of Feb 2026. APIs may change and it is not recommended for mission-critical production use without testing.
One profile at a timeOnly one transaction profiler can be active per process. If two transactions run concurrently, only the first one gets a profile.
30-second capProfiles are hard-capped at 30 seconds. Transactions longer than 30 seconds have their profile truncated.
.NET 8+ onlyThe EventPipe sampling profiler requires the .NET 8 CLR. .NET 6/7 are not supported even though the SDK targets netstandard2.0.
Linux crash bugReflectionTypeLoadException on startup in Linux containers (issue #4815, open). See Linux section above.
OTel conflictWhen using UseOpenTelemetry() + AddProfilingIntegration(), profiles may only show Program.Main with no application frames (issue #4820, reported closed — verify in your SDK version).
”Unknown frames”Some stack frames appear as “unknown” in the Sentry UI. This is expected — they are anonymous JIT helper methods in System assemblies that can’t be resolved to named methods.
No Android / WASMAndroid and Blazor WebAssembly are not supported.

Complete Setup Example

// Program.cs — ASP.NET Core with tracing + profiling

using Sentry;

var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseSentry(options =>
{
    options.Dsn = "https://[email protected]/0";
    options.Environment = builder.Environment.EnvironmentName;
    options.Release = "[email protected]";

    // Tracing: sample 10% of requests in production
    options.TracesSampleRate = builder.Environment.IsProduction() ? 0.1 : 1.0;

    // Profiling: profile 50% of sampled transactions
    // Net result: 5% of all production requests are profiled
    options.ProfilesSampleRate = 0.5;

    // Block up to 500ms so early-startup transactions aren't missed
    options.AddProfilingIntegration(TimeSpan.FromMilliseconds(500));
});

var app = builder.Build();
app.UseSentry();
app.MapControllers();
app.Run();

Console / Worker Service

using Sentry;

SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/0";
    options.TracesSampleRate = 1.0;
    options.ProfilesSampleRate = 1.0;
    options.AddProfilingIntegration(TimeSpan.FromMilliseconds(500));
});

// The profiler is ready — this transaction will be profiled
var transaction = SentrySdk.StartTransaction("data-import", "task");
SentrySdk.ConfigureScope(s => s.Transaction = transaction);

// ... your work here ...

transaction.Finish(SpanStatus.Ok);
// Profile is bundled with the transaction and sent to Sentry

Troubleshooting

IssueSolution
No profiles appearing in SentryVerify ProfilesSampleRate > 0 AND TracesSampleRate > 0. Both must be set. Check that AddProfilingIntegration() is called.
Early-startup transactions not profiledUse AddProfilingIntegration(TimeSpan.FromMilliseconds(500)) to block until the EventPipe session is ready before the first transaction starts.
ReflectionTypeLoadException on startup (Linux)Known issue #4815. Wrap AddProfilingIntegration() in try/catch. Test on your specific Linux image. Consider disabling profiling on Linux until the fix ships.
Profiles show only Program.Main framePossible OTel conflict (issue #4820). If using UseOpenTelemetry(), verify your SDK version has the fix. Try disabling one integration to isolate.
Concurrent transactions — second one not profiledExpected behavior. Only one profiler runs at a time. The first concurrent transaction wins the profiler slot.
Profile truncated after 30 secondsHard cap in the SDK. Split long-running operations into multiple shorter transactions if full profiling coverage is needed.
.NET 6 or .NET 7 — profiling not workingNot supported. EventPipe profiling requires .NET 8+.
”Unknown frames” in flame graphExpected for JIT internals. Focus on named application frames.
iOS profiles not appearing (using Sentry.Profiling package)Remove Sentry.Profiling from iOS targets. iOS/Mac Catalyst use the native Mono AOT profiler built into Sentry.Maui — the NuGet package is not needed and may conflict.

Reference: Tracing

Tracing — Sentry .NET SDK

Minimum SDK: Sentry ≥4.0.0
OpenTelemetry integration: Sentry.OpenTelemetry ≥6.1.0
Custom measurements: Sentry ≥3.23.0
Profiling (Alpha): Sentry.Profiling ≥4.0.0, .NET 8+ only


How Tracing Is Activated

Tracing is disabled by default. Enable it by setting TracesSampleRate or TracesSampler during SentrySdk.Init():

SentrySdk.Init(options =>
{
    options.Dsn = "https://[email protected]/0";
    options.TracesSampleRate = 1.0; // capture all transactions (lower in production)
});

Without one of these set, no spans or transactions are created regardless of other configuration.


TracesSampleRate — Uniform Sampling

A double? between 0.0 (capture nothing) and 1.0 (capture everything). Defaults to null (disabled).

options.TracesSampleRate = 0.2; // sample 20% of transactions

TracesSampler — Dynamic Per-Transaction Sampling

When set, takes precedence over TracesSampleRate. Receives a TransactionSamplingContext and returns double? (0.0–1.0) or null (falls back to TracesSampleRate).

options.TracesSampler = context =>
{
    var name = context.TransactionContext.Name;
    var op   = context.TransactionContext.Operation;

    // Drop health checks entirely
    if (name.Contains("/health") || name.Contains("/ping"))
        return 0.0;

    // Always capture checkout flow
    if (name == "checkout" || op == "perform-checkout")
        return 1.0;

    // Read caller-supplied hint
    if (context.CustomSamplingContext.TryGetValue("isCritical", out var flag) && flag is true)
        return 1.0;

    return 0.1; // default: 10%
};

Passing Custom Sampling Context

var transaction = SentrySdk.StartTransaction(
    new TransactionContext("checkout", "http.server"),
    new Dictionary<string, object?> { ["isCritical"] = true }
);

TransactionSamplingContext shape:

public class TransactionSamplingContext
{
    public ITransactionContext TransactionContext { get; }
    public IReadOnlyDictionary<string, object?> CustomSamplingContext { get; }
}

ASP.NET Core Middleware Integration

Setup

// Program.cs — .NET 6+ minimal API
var builder = WebApplication.CreateBuilder(args);

builder.WebHost.UseSentry(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.TracesSampleRate = 1.0;
    options.SendDefaultPii = true; // include user info in transactions
});

var app = builder.Build();

// Place UseSentry() BEFORE all other middleware to capture the full request lifecycle
app.UseSentry();

app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();

app.Run();

What Happens Automatically

BehaviorDetail
One transaction per requestSentryMiddleware creates an ITransactionTracer for every HTTP request
Route-based namingTransaction name = route template (e.g., GET /api/users/{id}) with TransactionNameSource.Route
Error linkingTransaction is set on scope → all errors captured during the request are linked to it
Distributed trace continuationIncoming sentry-trace + baggage headers are read; ContinueTrace() is called automatically
Outgoing HTTP spansSentryHttpMessageHandler auto-registered with IHttpClientFactory → child spans on every outbound call
EF Core / SQLClient spansDiagnosticSource integration adds db.* child spans automatically (≥3.9.0)
Transaction sendFinished and sent to Sentry when the response is written

Dropping or Renaming Transactions

options.BeforeSendTransaction = transaction =>
{
    // Drop internal/health routes
    if (transaction.Name.StartsWith("GET /internal/")) return null;

    return transaction;
};

Auto-Instrumentation Reference

IntegrationSpans CreatedPackageNotes
ASP.NET Core requestshttp.server transaction per requestSentry.AspNetCoreEnabled automatically by UseSentry()
Outgoing HTTP (IHttpClientFactory)http.client spansSentry.AspNetCoreRequires active transaction on scope
EF Core queriesdb.query_compiler, db.connection, db.querySentry.DiagnosticSource (auto in Sentry.AspNetCore ≥3.9.0)Opt out with DisableDiagnosticSourceIntegration()
SQLClientdb.connection, db.querySentry.DiagnosticSourceSame as EF Core
Azure Functions WorkerTransaction per invocationSentry.AzureFunctions.WorkerAuto-registered
Hangfire jobsTransaction per jobSentry.HangfireAuto-registered
Microsoft.Extensions.AIai.* spansSentry.Extensions.AI

EF Core Span Types

Three spans are created automatically per EF Core query:

db.query_compiler  — query compilation / optimization (cached after first run)
db.connection      — database connection lifecycle
db.query           — actual SQL execution

Outgoing HTTP Auto-Instrumentation

Spans are only created when there is an active transaction on scope. For manual HttpClient construction outside of IHttpClientFactory:

var sentryHandler = new SentryHttpMessageHandler();
var httpClient = new HttpClient(sentryHandler);

// Must have a transaction active first
var tx = SentrySdk.StartTransaction("my-op", "http.client");
SentrySdk.ConfigureScope(s => s.Transaction = tx);

var response = await httpClient.GetStringAsync("https://api.example.com");
// ^ creates a "GET https://api.example.com" child span

tx.Finish();

Custom Instrumentation

Minimal Example

var transaction = SentrySdk.StartTransaction("test-transaction", "test-operation");

var span = transaction.StartChild("test-child-operation");
// ... do work ...
span.Finish();

transaction.Finish(); // sends everything to Sentry

Real-World Example: Checkout Flow

public async Task PerformCheckoutAsync()
{
    var transaction = SentrySdk.StartTransaction("checkout", "perform-checkout");

    // Set on scope so that:
    // 1. Errors during this transaction are linked to it
    // 2. Auto-instrumentation (HTTP, EF Core) attaches child spans to it
    SentrySdk.ConfigureScope(scope => scope.Transaction = transaction);

    // Validate cart
    var validationSpan = transaction.StartChild("validation", "validating shopping cart");
    try
    {
        await ValidateShoppingCartAsync();
        validationSpan.Finish(SpanStatus.Ok);
    }
    catch (Exception ex)
    {
        validationSpan.Finish(ex); // auto-maps exception type → SpanStatus
        transaction.Finish(ex);
        throw;
    }

    // Process payment
    var paymentSpan = transaction.StartChild("payment", "processing payment");
    await ProcessPaymentAsync();
    paymentSpan.Finish(SpanStatus.Ok);

    // Send confirmation
    var emailSpan = transaction.StartChild("email", "sending confirmation email");
    await SendConfirmationEmailAsync();
    emailSpan.Finish(SpanStatus.Ok);

    transaction.Finish(SpanStatus.Ok);
}

Attaching to an Active Transaction

public async Task DoSomethingAsync()
{
    var activeSpan = SentrySdk.GetSpan();

    if (activeSpan == null)
    {
        // No transaction in scope — start a new root transaction
        activeSpan = SentrySdk.StartTransaction("task", "background-job");
    }
    else
    {
        // Transaction already running — add a child
        activeSpan = activeSpan.StartChild("subtask");
    }

    // ... work ...
    activeSpan.Finish();
}

Nested Spans with Data

var transaction = SentrySdk.StartTransaction("data-pipeline", "pipeline");

var fetchSpan = transaction.StartChild("http.client", "fetch raw data");
    var parseSpan = fetchSpan.StartChild("serialize", "parse JSON response");
    parseSpan.Finish();
fetchSpan.Finish();

var processSpan = transaction.StartChild("function", "transform data");
    var dbSpan = processSpan.StartChild("db.query", "INSERT INTO results");
    dbSpan.SetData("db.system", "postgresql");
    dbSpan.SetData("db.statement", "INSERT INTO results (data) VALUES (?)");
    dbSpan.Finish();
processSpan.Finish();

transaction.Finish();

DI-Friendly Pattern (IHub)

In ASP.NET Core, inject IHub instead of using the static SentrySdk API:

public class OrderService
{
    private readonly IHub _hub;
    public OrderService(IHub hub) => _hub = hub;

    public async Task ProcessOrderAsync(int orderId)
    {
        var transaction = _hub.StartTransaction("process-order", "task");
        SentrySdk.ConfigureScope(s => s.Transaction = transaction);

        var span = transaction.StartChild("db.query", $"SELECT * FROM orders WHERE id = {orderId}");
        try
        {
            await FetchOrderAsync(orderId);
            span.Finish(SpanStatus.Ok);
            transaction.Finish(SpanStatus.Ok);
        }
        catch (Exception ex)
        {
            span.Finish(ex);
            transaction.Finish(ex);
            throw;
        }
    }
}

Distributed Tracing

Propagation Headers

Sentry uses two HTTP headers to propagate trace context between services:

HeaderFormatPurpose
sentry-tracetraceId-spanId-samplingDecisionLinks spans across services into one trace
baggageW3C BaggageCarries Dynamic Sampling Context (DSC): sentry-trace_id, sentry-public_key, sentry-environment, sentry-release, sentry-transaction, etc.

CORS note: If you have browser frontends, explicitly allowlist sentry-trace and baggage in your CORS policy — they’re blocked by default as non-simple headers.

Automatic Propagation (ASP.NET Core)

No configuration needed:

  • Incoming: SentryMiddleware reads sentry-trace + baggage and calls ContinueTrace() automatically
  • Outgoing: SentryHttpMessageHandler injects sentry-trace + baggage into all IHttpClientFactory requests

Restrict Which Hosts Receive Trace Headers

By default, headers are injected into all outgoing requests. Restrict with:

options.TracePropagationTargets = new List<StringOrRegex>
{
    "api.mycompany.com",
    new StringOrRegex(new Regex(@"^https://.*\.mycompany\.com")),
};

Manual Propagation — Outgoing

// Read from active transaction
var sentryTrace = SentrySdk.GetTraceHeader()?.ToString();
var baggage     = SentrySdk.GetBaggage()?.ToString();

// W3C traceparent (alternative format)
var traceparent = SentrySdk.GetTraceparentHeader()?.ToString();

// Inject into your request
request.Headers["sentry-trace"] = sentryTrace;
request.Headers["baggage"]      = baggage;

Manual Propagation — Incoming (ContinueTrace)

// Service B receives an HTTP request from Service A
var sentryTraceHeader = httpRequest.Headers["sentry-trace"];
var baggageHeader     = httpRequest.Headers["baggage"];

// ContinueTrace parses headers and returns a pre-populated TransactionContext
// with the upstream traceId, parentSpanId, and sampling decision
var ctx = SentrySdk.ContinueTrace(
    sentryTraceHeader,
    baggageHeader,
    name: "process-incoming-request",
    operation: "http.server"
);

var transaction = SentrySdk.StartTransaction(ctx);
// Now this transaction is part of the same distributed trace as Service A

Producer / Consumer Queue Example

Producer:

var transaction = SentrySdk.StartTransaction("order-submitted", "function");

var publishSpan = transaction.StartChild("queue.publish", "orders");
publishSpan.SetData("messaging.message.id", messageId);
publishSpan.SetData("messaging.destination.name", "orders-queue");
publishSpan.SetData("messaging.message.body.size", Encoding.UTF8.GetByteCount(payload));

// Embed trace context in the message envelope
var envelope = new MessageEnvelope
{
    Payload    = payload,
    SentryTrace = SentrySdk.GetTraceHeader()?.ToString(),
    Baggage     = SentrySdk.GetBaggage()?.ToString(),
};

await queue.PublishAsync("orders-queue", envelope);
publishSpan.Finish();
transaction.Finish();

Consumer:

var envelope = await queue.ConsumeAsync("orders-queue");

// Link consumer to producer's trace
var ctx         = SentrySdk.ContinueTrace(envelope.SentryTrace, envelope.Baggage);
var transaction = SentrySdk.StartTransaction(ctx, "process-order", "function");

var processSpan = transaction.StartChild("queue.process", "orders");
processSpan.SetData("messaging.message.id", envelope.MessageId);
processSpan.SetData("messaging.destination.name", "orders-queue");
processSpan.SetData("messaging.message.receive.latency", latencyMs);
processSpan.SetData("messaging.message.retry.count", retryCount);

try
{
    await ProcessOrderAsync(envelope.Payload);
    processSpan.Finish(SpanStatus.Ok);
    transaction.Finish(SpanStatus.Ok);
}
catch (Exception ex)
{
    processSpan.Finish(ex);
    SentrySdk.CaptureException(ex);
    transaction.Finish(ex);
}

OpenTelemetry Integration

Version Requirements

PackageMinimum Version
Sentry6.1.0
Sentry.OpenTelemetry6.1.0
OpenTelemetry1.5.0
dotnet add package Sentry.OpenTelemetry

How It Works

The AddSentry() extension registers a SentrySpanProcessor with the OTel TracerProvider. Span mapping:

  • The first OTel Span flowing through the processor becomes a Sentry Transaction
  • Child OTel Spans with the same parent become Sentry child Spans on that transaction
  • A new top-level OTel Span from a different service creates a new Sentry Transaction, linked via the same distributed trace

Full ASP.NET Core Setup

Two parts are required — both must be configured:

var builder = WebApplication.CreateBuilder(args);

// Part 1: Configure Sentry with UseOpenTelemetry()
builder.WebHost.UseSentry(options =>
{
    options.Dsn = "https://[email protected]/...";
    options.TracesSampleRate = 1.0;
    options.UseOpenTelemetry(); // ← tells Sentry to use OTel for trace context propagation
    // Do NOT also configure Sentry's own DiagnosticSource integration —
    // let OTel instrumentation libraries handle it instead
});

// Part 2: Register OTel TracerProvider with AddSentry()
builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddEntityFrameworkCoreInstrumentation()
        .AddSentry() // ← routes all OTel spans to Sentry
    );

var app = builder.Build();
app.UseSentry();
app.Run();

⚠️ Exception Capture in OTel Mode

Do NOT use OTel’s exception APIs — they strip exception data before Sentry can see it:

// ❌ These lose exception details
activity.RecordException(ex);
activity.AddException(ex);

// ✅ Use these instead
_logger.LogError(ex, "Something went wrong"); // ILogger (Sentry captures via logging integration)
SentrySdk.CaptureException(ex);               // or capture directly

Dynamic Sampling

Sentry’s dynamic sampling uses the Dynamic Sampling Context (DSC) carried in baggage to make consistent sampling decisions across a distributed trace.

How It Works

  1. The head service (first in the trace) makes the sampling decision.
  2. The decision is encoded in baggage as sentry-sampled=true|false.
  3. All downstream services receive baggage and honor the upstream decision.
  4. DSC fields: sentry-trace_id, sentry-public_key, sentry-sample_rate, sentry-sampled, sentry-release, sentry-environment, sentry-transaction, sentry-user_segment.

TransactionNameSource — Critical for Grouping

High-cardinality names (raw URLs) break dynamic sampling grouping. Use parameterized routes:

// ❌ Raw URL — creates unbounded unique groups, defeats sampling
SentrySdk.StartTransaction("/users/12345/orders/9876", "http.server");

// ✅ Parameterized — clean grouping, correct dynamic sampling
SentrySdk.StartTransaction(new TransactionContext(
    "/users/{userId}/orders/{orderId}",
    "http.server",
    nameSource: TransactionNameSource.Route
));
TransactionNameSourceCardinalityUse For
RouteLow ✅Parameterized route templates (e.g., GET /users/{id})
CustomLow ✅User-defined names (background jobs, tasks)
ViewLow ✅Controller / view class names
ComponentMediumFunction / component names
UrlHigh ❌Raw URLs — avoid for dynamic sampling
TaskLow ✅Background task names

Operation Types and Naming Conventions

The operation string categorizes and color-codes spans in the Sentry UI. Follow these conventions:

CategoryOperationExample Description
HTTP serverhttp.serverGET /api/users
HTTP clienthttp.clientGET https://api.stripe.com/v1/charges
DB querydb.querySELECT * FROM orders WHERE id = ?
DB connectiondb.connection
DB compiledb.query_compilerThe LINQ/HQL expression
Cache readcache.getThe cache key
Cache writecache.putThe cache key
Queue publishqueue.publishQueue or topic name
Queue consumequeue.processQueue or topic name
FunctionfunctionFunction or method name
Background tasktaskTask name
Serializationserialize
ValidationvalidationWhat is being validated
AI inferenceai.*Model name

Origin Field

Indicates whether a span was created by auto-instrumentation or by your code:

ValueSource
auto.http.aspnetcoreASP.NET Core middleware
auto.http.system_net_httpSentryHttpMessageHandler
auto.db.ef_coreEF Core DiagnosticSource
auto.db.sql_clientSQLClient DiagnosticSource
manualUser code

Custom Measurements

Attach numeric measurements to transactions (requires Sentry ≥3.23.0):

var span = SentrySdk.GetSpan();
if (span != null)
{
    var transaction = span.GetTransaction();

    transaction.SetMeasurement("memory_used",            64,   MeasurementUnit.Information.Megabyte);
    transaction.SetMeasurement("profile_loading_time",   1.3,  MeasurementUnit.Duration.Second);
    transaction.SetMeasurement("items_processed",        1500);           // unitless
    transaction.SetMeasurement("cache_hit_rate",         0.85, MeasurementUnit.Fraction.Ratio);
}

MeasurementUnit Quick Reference

CategoryValues
DurationNanosecond, Microsecond, Millisecond, Second, Minute, Hour, Day, Week
InformationBit, Byte, Kilobyte/Kibibyte, Megabyte/Mebibyte, Gigabyte/Gibibyte, …
FractionRatio, Percent
UnitlessOmit unit parameter

⚠️ Unit consistency: ("latency", 60, Second) and ("latency", 3, Minute) are stored as separate measurements, not aggregated. Always use the same unit per measurement name.


SpanStatus Reference

SpanStatus.Ok                // success
SpanStatus.Cancelled         // OperationCanceledException
SpanStatus.InvalidArgument   // ArgumentException, bad input
SpanStatus.DeadlineExceeded  // TimeoutException
SpanStatus.NotFound          // 404
SpanStatus.PermissionDenied  // UnauthorizedAccessException, 403
SpanStatus.ResourceExhausted // 429 / out of resources
SpanStatus.Unimplemented     // NotImplementedException, 501
SpanStatus.Unavailable       // 503
SpanStatus.InternalError     // unhandled exception, 500
SpanStatus.UnknownError      // unknown failure
SpanStatus.FailedPrecondition // InvalidOperationException
SpanStatus.Aborted           // conflicting operation
SpanStatus.DataLoss          // unrecoverable data loss

span.Finish(exception) auto-maps exception type → SpanStatus. HTTP status codes from SentryHttpMessageHandler are also mapped automatically (2xx→Ok, 401→Unauthenticated, 403→PermissionDenied, 404→NotFound, 429→ResourceExhausted, 5xx→InternalError).


Complete Configuration Reference

SentrySdk.Init(options =>
{
    // ── Identity ──────────────────────────────────────────────────────────
    options.Dsn         = "https://[email protected]/0";
    options.Environment = "production";
    options.Release     = "[email protected]";

    // ── Tracing ───────────────────────────────────────────────────────────
    options.TracesSampleRate = 0.2;  // 20% uniform rate

    // OR dynamic sampler (takes precedence when set)
    options.TracesSampler = ctx =>
    {
        if (ctx.TransactionContext.Name.Contains("/health")) return 0.0;
        return 0.1;
    };

    // Restrict which outbound hosts receive trace headers (default: all)
    options.TracePropagationTargets = new List<StringOrRegex>
    {
        "api.mycompany.internal",
        new StringOrRegex(new Regex(@"^https://.*\.mycompany\.com")),
    };

    // ── Auto-instrumentation control ──────────────────────────────────────
    // options.DisableDiagnosticSourceIntegration(); // opt out of EF Core / SQLClient spans

    // ── OpenTelemetry (optional) ──────────────────────────────────────────
    // options.UseOpenTelemetry(); // use when routing spans via OTel TracerProvider

    // ── Profiling (Alpha, .NET 8+ only) ───────────────────────────────────
    // options.ProfilesSampleRate = 0.1;
    // options.AddProfilingIntegration(TimeSpan.FromMilliseconds(500)); // sync startup
});

Key Options Table

OptionTypeDefaultPurpose
TracesSampleRatedouble?null (disabled)Uniform sampling rate 0.0–1.0
TracesSamplerFunc<TransactionSamplingContext, double?>nullDynamic sampler; overrides TracesSampleRate
TracePropagationTargetsIList<StringOrRegex>[".*"] (all)Hosts that receive sentry-trace + baggage headers
SendDefaultPiiboolfalseInclude user IP and username in transactions
MaxSpansint1000Maximum child spans per transaction
ProfilesSampleRatedouble?nullProfiling rate relative to traced transactions
UseOpenTelemetry()methodEnable OTel-based trace context propagation
DisableDiagnosticSourceIntegration()methodOpt out of EF Core / SQLClient auto-spans

Quick Reference Cheat Sheet

// ── Start a root transaction ──────────────────────────────────────────────
var tx = SentrySdk.StartTransaction("name", "operation");
SentrySdk.ConfigureScope(s => s.Transaction = tx);  // link errors + enable auto-spans

// ── Add child spans ───────────────────────────────────────────────────────
var span = tx.StartChild("operation", "description");
span.SetData("key", "value");
span.Finish(SpanStatus.Ok);

// ── Get the active span from anywhere ────────────────────────────────────
var active = SentrySdk.GetSpan();
var child  = active?.StartChild("nested-op");
child?.Finish();

// ── Access the transaction from a span ───────────────────────────────────
var txFromSpan = active?.GetTransaction();
txFromSpan?.SetMeasurement("count", 42, MeasurementUnit.Duration.Millisecond);

// ── Finish variants ───────────────────────────────────────────────────────
tx.Finish();                          // implicit Ok
tx.Finish(SpanStatus.InternalError);  // explicit status
tx.Finish(exception);                 // auto-maps exception → SpanStatus

// ── Distributed tracing: outgoing headers ─────────────────────────────────
var traceHeader  = SentrySdk.GetTraceHeader()?.ToString();   // "sentry-trace" value
var baggage      = SentrySdk.GetBaggage()?.ToString();        // "baggage" value
var traceparent  = SentrySdk.GetTraceparentHeader()?.ToString(); // W3C format

// ── Distributed tracing: incoming headers ─────────────────────────────────
var ctx    = SentrySdk.ContinueTrace(incomingTraceHeader, incomingBaggageHeader);
var linked = SentrySdk.StartTransaction(ctx, "name", "op");

Troubleshooting

IssueLikely CauseFix
No transactions appear in SentryTracesSampleRate and TracesSampler are both unsetSet options.TracesSampleRate = 1.0 (or >0) during SentrySdk.Init()
Transactions appear but have no child spansTransaction not set on scopeCall SentrySdk.ConfigureScope(s => s.Transaction = tx) after starting the transaction
Outgoing HTTP spans missingHttpClient created manually without SentryHttpMessageHandlerUse IHttpClientFactory, or wrap with new HttpClient(new SentryHttpMessageHandler())
EF Core spans missingSentry.DiagnosticSource not installed or version < 3.9.0Install Sentry.DiagnosticSource, or upgrade Sentry.AspNetCore to ≥3.9.0
Distributed trace not connected across servicesMissing ContinueTrace() on receiving endCall SentrySdk.ContinueTrace(traceHeader, baggageHeader) and use the returned context to start the transaction
sentry-trace header stripped by browser preflightCORS policy blocks non-simple headersAdd sentry-trace and baggage to Access-Control-Allow-Headers in your CORS config
OTel spans not appearing in SentryAddSentry() missing from TracerProvider OR UseOpenTelemetry() missing from SentryOptionsBoth are required: AddSentry() in OTel builder AND options.UseOpenTelemetry() in Sentry init
OTel mode: exceptions captured with no contextUsing activity.RecordException() or activity.AddException()Use SentrySdk.CaptureException(ex) or _logger.LogError(ex, ...) instead
High-cardinality transaction groupsTransaction names are raw URLsUse TransactionNameSource.Route with parameterized route templates
#sentry #dotnet #sdk

数据统计

总访客 -- 总访问 --
ESC
输入关键词开始搜索