Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

- `BreadcrumbLevel.Critical` has been renamed to `BreadcrumbLevel.Fatal` for consistency with the other Sentry SDKs ([#4605](https://github.com/getsentry/sentry-dotnet/pull/4605))
- SentryOptions.IsEnvironmentUser now defaults to false on MAUI. The means the User.Name will no longer be set, by default, to the name of the device ([#4606](https://github.com/getsentry/sentry-dotnet/pull/4606))
- Spans and Transactions now implement `IDisposable` so that they can be used with `using` statements/declarations that will automatically finish the span with a status of OK when it passes out of scope, if it has not already been finished, to be consistent with `Activity` classes when using OpenTelemetry ([#4627](https://github.com/getsentry/sentry-dotnet/pull/4627))
- SpanTracer and TransactionTracer are still public but these are now `sealed` (see also [#4627](https://github.com/getsentry/sentry-dotnet/pull/4627))
- Remove unnecessary files from SentryCocoaFramework before packing ([#4602](https://github.com/getsentry/sentry-dotnet/pull/4602))
- Backpressure handling is now enabled by default, meaning that the SDK will monitor system health and reduce the sampling rate of events and transactions when the system is under load. When the system is determined to be healthy again, the sampling rates are returned to their original levels. ([#4615](https://github.com/getsentry/sentry-dotnet/pull/4615))
- ScopeExtensions.Populate is now internal ([#4611](https://github.com/getsentry/sentry-dotnet/pull/4611))
Expand Down
34 changes: 16 additions & 18 deletions samples/Sentry.Samples.Console.Basic/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@

// Always try to finish the transaction successfully.
// Unhandled exceptions will fail the transaction automatically.
// Optionally, you can try/catch the exception, and call transaction.Finish(exception) on failure.
// Optionally, you can try/catch the exception and call transaction.Finish(exception) on failure.
transaction.Finish();

async Task FirstFunction()
Expand All @@ -79,7 +79,6 @@ async Task FirstFunction()
async Task SecondFunction()
{
var span = transaction.StartChild("function", nameof(SecondFunction));

try
{
// Simulate doing some work
Expand All @@ -97,26 +96,25 @@ async Task SecondFunction()
SentrySdk.Logger.LogError(static log => log.SetAttribute("method", nameof(SecondFunction)),
"Error with message: {0}", exception.Message);
}

span.Finish();
finally
{
span.Finish();
}
}

async Task ThirdFunction()
{
var span = transaction.StartChild("function", nameof(ThirdFunction));
try
{
// Simulate doing some work
await Task.Delay(100);
// The `using` here ensures the span gets finished when we leave this method... This is unnecessary here,
// since the method always throws and the span will be finished automatically when the exception is captured,
// but this gives you another way to ensure spans are finished.
using var span = transaction.StartChild("function", nameof(ThirdFunction));

SentrySdk.Logger.LogFatal(static log => log.SetAttribute("suppress", true),
"Crash imminent!");
// Simulate doing some work
await Task.Delay(100);

// This is an example of an unhandled exception. It will be captured automatically.
throw new InvalidOperationException("Something happened that crashed the app!");
}
finally
{
span.Finish();
}
SentrySdk.Logger.LogFatal(static log => log.SetAttribute("suppress", true),
"Crash imminent!");

// This is an example of an unhandled exception. It will be captured automatically.
throw new InvalidOperationException("Something happened that crashed the app!");
}
2 changes: 1 addition & 1 deletion src/Sentry/ISpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace Sentry;
/// <summary>
/// SpanTracer interface
/// </summary>
public interface ISpan : ISpanData
public interface ISpan : ISpanData, IDisposable
{
/// <summary>
/// Span description.
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/Internal/NoOpSpan.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,8 @@ public void SetMeasurement(string name, Measurement measurement)
}

public string? Origin { get; set; }

public void Dispose()
{
}
}
18 changes: 16 additions & 2 deletions src/Sentry/SpanTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ namespace Sentry;
/// <summary>
/// Transaction span tracer.
/// </summary>
public class SpanTracer : IBaseTracer, ISpan
public sealed class SpanTracer : IBaseTracer, ISpan
{
private readonly IHub _hub;
private readonly SentryStopwatch _stopwatch = SentryStopwatch.StartNew();
private string? _origin;

private readonly Instrumenter _instrumenter = Instrumenter.Sentry;

Expand Down Expand Up @@ -190,5 +191,18 @@ internal set
_origin = value;
}
}
private string? _origin;

/// <summary>
/// <para>
/// Automatically finishes the span with a status of <see cref="SpanStatus.Ok" /> at the end of a
/// <c>using</c> block, if it has not already been finished.
/// </para>
/// <para>
/// This is the equivalent of calling <see cref="Finish()" /> when the span passes out of scope.
/// </para>
/// /// </summary>
public void Dispose()
{
Finish();
}
}
23 changes: 22 additions & 1 deletion src/Sentry/TransactionTracer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ namespace Sentry;
/// <summary>
/// Transaction tracer.
/// </summary>
public class TransactionTracer : IBaseTracer, ITransactionTracer
public sealed class TransactionTracer : IBaseTracer, ITransactionTracer
{
private readonly IHub _hub;
private readonly SentryOptions? _options;
private readonly Timer? _idleTimer;
private readonly SentryStopwatch _stopwatch = SentryStopwatch.StartNew();
private InterlockedBoolean _hasFinished;

private InterlockedBoolean _cancelIdleTimeout;

Expand Down Expand Up @@ -362,6 +363,11 @@ public void Clear()
/// <inheritdoc />
public void Finish()
{
if (_hasFinished.Exchange(true))
{
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: Timer

Should we, in the case of a Timer still being active, disable/dispose the timer too when the transactions is transitioning from unfinished to finished?

Wait ... I read this wrong ... this is actually what is happening:

  • the first Finish will Stop/Dispose the Timer, if active
  • subsequent Finishes will early-out, but the Timer, if active, has been stopped/disposed previously

Copy link
Collaborator Author

@jamescrosswell jamescrosswell Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timer is only relevant if idle timeouts are enabled. That was added by Sean here:

I think there were plans to maybe use transactions that timed out automatically to instrument MAUI, but this hasn't yet been done. So, in practice, that code path is never executed.

I'm not sure I understand though - do you think there's a potential problem here? In what scenario?

}

_options?.LogDebug("Attempting to finish Transaction '{0}'.", SpanId);
if (_cancelIdleTimeout.Exchange(false) == true)
{
Expand Down Expand Up @@ -432,4 +438,19 @@ private void ReleaseSpans()
#endif
_activeSpanTracker.Clear();
}

/// <summary>
/// <para>
/// Automatically finishes the transaction with a status of <see cref="SpanStatus.Ok" /> at the end of a
/// <c>using</c> block, if it has not already been finished.
/// </para>
/// <para>
/// This is the equivalent of calling <see cref="Finish()" /> when the transaction passes out of scope.
/// </para>
/// </summary>
/// <remarks>This is a convenience method only. Disposing is not required.</remarks>
public void Dispose()
{
Finish();
}
}
10 changes: 6 additions & 4 deletions test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ namespace Sentry
{
Sentry.SentryUser? Create();
}
public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext
public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext, System.IDisposable
{
new string? Description { get; set; }
new string Operation { get; set; }
Expand Down Expand Up @@ -291,7 +291,7 @@ namespace Sentry
{
string? Platform { get; set; }
}
public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext
public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext, System.IDisposable
{
new bool? IsParentSampled { get; set; }
new string Name { get; set; }
Expand Down Expand Up @@ -1160,7 +1160,7 @@ namespace Sentry
OutOfRange = 15,
DataLoss = 16,
}
public class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext
public sealed class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext, System.IDisposable
{
public SpanTracer(Sentry.IHub hub, Sentry.TransactionTracer transaction, Sentry.SpanId? parentSpanId, Sentry.SentryId traceId, string operation) { }
public System.Collections.Generic.IReadOnlyDictionary<string, object?> Data { get; }
Expand All @@ -1178,6 +1178,7 @@ namespace Sentry
public Sentry.SpanStatus? Status { get; set; }
public System.Collections.Generic.IReadOnlyDictionary<string, string> Tags { get; }
public Sentry.SentryId TraceId { get; }
public void Dispose() { }
public void Finish() { }
public void Finish(Sentry.SpanStatus status) { }
public void Finish(System.Exception exception) { }
Expand Down Expand Up @@ -1239,7 +1240,7 @@ namespace Sentry
public System.Collections.Generic.IReadOnlyDictionary<string, object?> CustomSamplingContext { get; }
public Sentry.ITransactionContext TransactionContext { get; }
}
public class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext
public sealed class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext, System.IDisposable
{
public TransactionTracer(Sentry.IHub hub, Sentry.ITransactionContext context) { }
public System.Collections.Generic.IReadOnlyCollection<Sentry.Breadcrumb> Breadcrumbs { get; }
Expand Down Expand Up @@ -1275,6 +1276,7 @@ namespace Sentry
public Sentry.SentryId TraceId { get; }
public Sentry.SentryUser User { get; set; }
public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { }
public void Dispose() { }
public void Finish() { }
public void Finish(Sentry.SpanStatus status) { }
public void Finish(System.Exception exception) { }
Expand Down
10 changes: 6 additions & 4 deletions test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ namespace Sentry
{
Sentry.SentryUser? Create();
}
public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext
public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext, System.IDisposable
{
new string? Description { get; set; }
new string Operation { get; set; }
Expand Down Expand Up @@ -291,7 +291,7 @@ namespace Sentry
{
string? Platform { get; set; }
}
public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext
public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext, System.IDisposable
{
new bool? IsParentSampled { get; set; }
new string Name { get; set; }
Expand Down Expand Up @@ -1160,7 +1160,7 @@ namespace Sentry
OutOfRange = 15,
DataLoss = 16,
}
public class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext
public sealed class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext, System.IDisposable
{
public SpanTracer(Sentry.IHub hub, Sentry.TransactionTracer transaction, Sentry.SpanId? parentSpanId, Sentry.SentryId traceId, string operation) { }
public System.Collections.Generic.IReadOnlyDictionary<string, object?> Data { get; }
Expand All @@ -1178,6 +1178,7 @@ namespace Sentry
public Sentry.SpanStatus? Status { get; set; }
public System.Collections.Generic.IReadOnlyDictionary<string, string> Tags { get; }
public Sentry.SentryId TraceId { get; }
public void Dispose() { }
public void Finish() { }
public void Finish(Sentry.SpanStatus status) { }
public void Finish(System.Exception exception) { }
Expand Down Expand Up @@ -1239,7 +1240,7 @@ namespace Sentry
public System.Collections.Generic.IReadOnlyDictionary<string, object?> CustomSamplingContext { get; }
public Sentry.ITransactionContext TransactionContext { get; }
}
public class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext
public sealed class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext, System.IDisposable
{
public TransactionTracer(Sentry.IHub hub, Sentry.ITransactionContext context) { }
public System.Collections.Generic.IReadOnlyCollection<Sentry.Breadcrumb> Breadcrumbs { get; }
Expand Down Expand Up @@ -1275,6 +1276,7 @@ namespace Sentry
public Sentry.SentryId TraceId { get; }
public Sentry.SentryUser User { get; set; }
public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { }
public void Dispose() { }
public void Finish() { }
public void Finish(Sentry.SpanStatus status) { }
public void Finish(System.Exception exception) { }
Expand Down
10 changes: 6 additions & 4 deletions test/Sentry.Tests/ApiApprovalTests.Run.DotNet9_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ namespace Sentry
{
Sentry.SentryUser? Create();
}
public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext
public interface ISpan : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanData, Sentry.Protocol.ITraceContext, System.IDisposable
{
new string? Description { get; set; }
new string Operation { get; set; }
Expand Down Expand Up @@ -291,7 +291,7 @@ namespace Sentry
{
string? Platform { get; set; }
}
public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext
public interface ITransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.Protocol.ITraceContext, System.IDisposable
{
new bool? IsParentSampled { get; set; }
new string Name { get; set; }
Expand Down Expand Up @@ -1160,7 +1160,7 @@ namespace Sentry
OutOfRange = 15,
DataLoss = 16,
}
public class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext
public sealed class SpanTracer : Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.Protocol.ITraceContext, System.IDisposable
{
public SpanTracer(Sentry.IHub hub, Sentry.TransactionTracer transaction, Sentry.SpanId? parentSpanId, Sentry.SentryId traceId, string operation) { }
public System.Collections.Generic.IReadOnlyDictionary<string, object?> Data { get; }
Expand All @@ -1178,6 +1178,7 @@ namespace Sentry
public Sentry.SpanStatus? Status { get; set; }
public System.Collections.Generic.IReadOnlyDictionary<string, string> Tags { get; }
public Sentry.SentryId TraceId { get; }
public void Dispose() { }
public void Finish() { }
public void Finish(Sentry.SpanStatus status) { }
public void Finish(System.Exception exception) { }
Expand Down Expand Up @@ -1239,7 +1240,7 @@ namespace Sentry
public System.Collections.Generic.IReadOnlyDictionary<string, object?> CustomSamplingContext { get; }
public Sentry.ITransactionContext TransactionContext { get; }
}
public class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext
public sealed class TransactionTracer : Sentry.IEventLike, Sentry.IHasData, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpan, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.ITransactionData, Sentry.ITransactionTracer, Sentry.Protocol.ITraceContext, System.IDisposable
{
public TransactionTracer(Sentry.IHub hub, Sentry.ITransactionContext context) { }
public System.Collections.Generic.IReadOnlyCollection<Sentry.Breadcrumb> Breadcrumbs { get; }
Expand Down Expand Up @@ -1275,6 +1276,7 @@ namespace Sentry
public Sentry.SentryId TraceId { get; }
public Sentry.SentryUser User { get; set; }
public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { }
public void Dispose() { }
public void Finish() { }
public void Finish(Sentry.SpanStatus status) { }
public void Finish(System.Exception exception) { }
Expand Down
Loading
Loading