Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions src/Sentry/HubExtensions.Record.cs
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using Sentry.Internal;

namespace Sentry;

public static partial class HubExtensions
{
/// <summary>
/// Records a transaction that has already completed elsewhere — for example, spans measured on another
/// machine or process and replayed through a proxy. Timing is supplied explicitly rather than measured
/// live, so unlike <see cref="StartTransaction(IHub, string, string)"/> there is no stopwatch, idle timer,
/// or sampling decision involved; the transaction is materialized and captured once when
/// <paramref name="configure"/> returns.
/// </summary>
/// <param name="hub">The hub.</param>
/// <param name="name">The transaction name.</param>
/// <param name="operation">The transaction operation.</param>
/// <param name="startTimestamp">When the transaction started.</param>
/// <param name="duration">How long the transaction ran. Must not be negative.</param>
/// <param name="traceId">Optional trace id to preserve from the originating system; generated when omitted.</param>
/// <param name="spanId">Optional root span id to preserve; generated when omitted.</param>
/// <param name="parentSpanId">Optional parent span id, when continuing a trace from another service.</param>
/// <param name="configure">
/// Optional callback to set metadata and record the span tree via
/// <see cref="ISpanRecorder.RecordSpan(string, DateTimeOffset, TimeSpan, SpanId?, Action{ISpanRecorder}?)"/>.
/// </param>
/// <returns>The event id of the captured transaction.</returns>
public static SentryId RecordTransaction(
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
this IHub hub,
string name,
string operation,
DateTimeOffset startTimestamp,
TimeSpan duration,
SentryId? traceId = null,
SpanId? spanId = null,
SpanId? parentSpanId = null,
Action<ITransactionRecorder>? configure = null)
{
if (duration < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(duration), duration, "Transaction duration cannot be negative.");
}

var context = new TransactionContext(
name,
operation,
spanId: spanId,
parentSpanId: parentSpanId,
traceId: traceId,
isSampled: true);

var tracer = new TransactionTracer(hub, context)
{
StartTimestamp = startTimestamp,
EndTimestamp = startTimestamp + duration,
};
tracer.Status ??= SpanStatus.Ok;

var recorder = new TransactionRecorder(tracer);
configure?.Invoke(recorder);

var transaction = new SentryTransaction(tracer);

// A recorded transaction represents work that happened elsewhere, so we don't want the current
// (live) scope's breadcrumbs/user/tags/contexts leaking onto it. Capture against a clean scope when
// we have options to construct one; otherwise fall back (e.g. a disabled hub, where this is a no-op).
var options = hub.GetSentryOptions();
if (options is not null)
{
hub.CaptureTransaction(transaction, new Scope(options), null);
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
}
else
{
hub.CaptureTransaction(transaction);
}

return transaction.EventId;
}
}
2 changes: 1 addition & 1 deletion src/Sentry/HubExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ namespace Sentry;
/// Extension methods for <see cref="IHub"/>.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class HubExtensions
public static partial class HubExtensions
{
/// <summary>
/// Starts a transaction.
Expand Down
80 changes: 80 additions & 0 deletions src/Sentry/ISpanRecorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
namespace Sentry;

/// <summary>
/// Builds a span while recording a transaction that has already completed elsewhere (for example, work
/// measured on another machine and replayed through a proxy). Unlike <see cref="ISpan"/>, timing is supplied
/// up-front rather than measured live, so a recorded span can never be left half-specified.
/// </summary>
/// <remarks>
/// Obtained from <see cref="ITransactionRecorder"/> or a parent <see cref="ISpanRecorder"/> via
/// <see cref="RecordSpan(string, DateTimeOffset, TimeSpan, SpanId?, Action{ISpanRecorder}?)"/>.
/// See <see cref="HubExtensions.RecordTransaction"/>.
/// </remarks>
public interface ISpanRecorder
{
/// <summary>
/// The span's id. When not overridden at creation, one is generated.
/// </summary>
SpanId SpanId { get; }

/// <summary>
/// Span description.
/// </summary>
string? Description { get; set; }

/// <summary>
/// Span status. Defaults to <see cref="SpanStatus.Ok"/> when not set.
/// </summary>
SpanStatus? Status { get; set; }

/// <summary>
/// Sets a tag on the span.
/// </summary>
void SetTag(string key, string value);

/// <summary>
/// Sets arbitrary data on the span.
/// </summary>
void SetData(string key, object? value);

/// <summary>
/// Records a child span nested under this one. The parent is structural (this span), so no parent id
/// needs to be supplied. Pass <paramref name="spanId"/> to preserve an id from the originating system.
/// </summary>
/// <param name="operation">The span operation.</param>
/// <param name="startTimestamp">When the child span started.</param>
/// <param name="duration">How long the child span ran. Must not be negative.</param>
/// <param name="spanId">Optional span id to preserve; generated when omitted.</param>
/// <param name="configure">Optional callback to set metadata and record further nested spans.</param>
/// <returns>The recorder for the child span.</returns>
ISpanRecorder RecordSpan(
string operation,
DateTimeOffset startTimestamp,
TimeSpan duration,
SpanId? spanId = null,
Action<ISpanRecorder>? configure = null);
}

/// <summary>
/// Builds a transaction (and its span tree) that has already completed elsewhere. Obtained inside the
/// <c>configure</c> callback of <see cref="HubExtensions.RecordTransaction"/>. When the callback returns, the
/// whole tree is materialized and captured once — no live tracing, stopwatch, or sampling roll is involved.
/// </summary>
public interface ITransactionRecorder : ISpanRecorder
Comment thread
jamescrosswell marked this conversation as resolved.
{
/// <summary>
/// The trace this transaction belongs to. Set at creation (via <see cref="HubExtensions.RecordTransaction"/>)
/// and inherited by every recorded span.
/// </summary>
SentryId TraceId { get; }

/// <summary>
/// The release that produced this transaction. Useful when the origin system differs from this process.
/// </summary>
string? Release { get; set; }

/// <summary>
/// The environment the transaction ran in. Useful when the origin system differs from this process.
/// </summary>
string? Environment { get; set; }
}
132 changes: 132 additions & 0 deletions src/Sentry/Internal/SpanRecorder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
namespace Sentry.Internal;

/// <summary>
/// Shared logic for materializing a recorded child span onto an existing <see cref="TransactionTracer"/>.
/// </summary>
internal static class SpanRecorderInternals
Comment thread
jamescrosswell marked this conversation as resolved.
Outdated
{
public static SpanRecorder RecordChild(
TransactionTracer owner,
SpanId parentSpanId,
string operation,
DateTimeOffset startTimestamp,
TimeSpan duration,
SpanId? spanId,
Action<ISpanRecorder>? configure)
{
if (duration < TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(duration), duration, "Span duration cannot be negative.");
}

// Reuse the existing child-span machinery so the span is added to the transaction's flat span
// collection with the correct parent pointer; then override the timing the tracer would otherwise
// have measured with a stopwatch.
var span = (SpanTracer)owner.StartChild(spanId, parentSpanId, operation);
span.StartTimestamp = startTimestamp;
span.EndTimestamp = startTimestamp + duration;
span.Status ??= SpanStatus.Ok;

var recorder = new SpanRecorder(owner, span);
configure?.Invoke(recorder);
return recorder;
}
}

/// <summary>
/// <see cref="ISpanRecorder"/> backed by a <see cref="SpanTracer"/> whose timing has been set explicitly.
/// </summary>
internal sealed class SpanRecorder : ISpanRecorder
{
private readonly TransactionTracer _owner;
private readonly SpanTracer _span;

public SpanRecorder(TransactionTracer owner, SpanTracer span)
{
_owner = owner;
_span = span;
}

public SpanId SpanId => _span.SpanId;

public string? Description
{
get => _span.Description;
set => _span.Description = value;
}

public SpanStatus? Status
{
get => _span.Status;
set => _span.Status = value;
}

public void SetTag(string key, string value) => _span.SetTag(key, value);

public void SetData(string key, object? value) => _span.SetData(key, value);

public ISpanRecorder RecordSpan(
string operation,
DateTimeOffset startTimestamp,
TimeSpan duration,
SpanId? spanId = null,
Action<ISpanRecorder>? configure = null) =>
SpanRecorderInternals.RecordChild(_owner, _span.SpanId, operation, startTimestamp, duration, spanId, configure);
}

/// <summary>
/// <see cref="ITransactionRecorder"/> backed by a <see cref="TransactionTracer"/> whose timing has been set
/// explicitly. The tracer is never <c>Finish()</c>-ed (which would reset the live scope); instead the caller
/// converts it to a <see cref="SentryTransaction"/> and captures it directly.
/// </summary>
internal sealed class TransactionRecorder : ITransactionRecorder
{
private readonly TransactionTracer _tracer;

public TransactionRecorder(TransactionTracer tracer)
{
_tracer = tracer;
}

internal TransactionTracer Tracer => _tracer;

public SpanId SpanId => _tracer.SpanId;

public SentryId TraceId => _tracer.TraceId;

public string? Description
{
get => _tracer.Description;
set => _tracer.Description = value;
}

public SpanStatus? Status
{
get => _tracer.Status;
set => _tracer.Status = value;
}

public string? Release
{
get => _tracer.Release;
set => _tracer.Release = value;
}

public string? Environment
{
get => _tracer.Environment;
set => _tracer.Environment = value;
}

public void SetTag(string key, string value) => _tracer.SetTag(key, value);

public void SetData(string key, object? value) => _tracer.SetData(key, value);

public ISpanRecorder RecordSpan(
string operation,
DateTimeOffset startTimestamp,
TimeSpan duration,
SpanId? spanId = null,
Action<ISpanRecorder>? configure = null) =>
SpanRecorderInternals.RecordChild(_tracer, _tracer.SpanId, operation, startTimestamp, duration, spanId, configure);
}
16 changes: 16 additions & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.DotNet10_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ namespace Sentry
public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action<Sentry.Scope> configureScope, Sentry.SentryLevel level = 1) { }
public static void LockScope(this Sentry.IHub hub) { }
public static System.IDisposable PushAndLockScope(this Sentry.IHub hub) { }
public static Sentry.SentryId RecordTransaction(this Sentry.IHub hub, string name, string operation, System.DateTimeOffset startTimestamp, System.TimeSpan duration, Sentry.SentryId? traceId = default, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, System.Action<Sentry.ITransactionRecorder>? configure = null) { }
public static Sentry.ISpan StartSpan(this Sentry.IHub hub, string operation, string description) { }
public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, Sentry.ITransactionContext context) { }
public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, string name, string operation) { }
Expand Down Expand Up @@ -293,6 +294,15 @@ namespace Sentry
Sentry.SentryTraceHeader GetTraceHeader();
void SetMeasurement(string name, Sentry.Protocol.Measurement measurement);
}
public interface ISpanRecorder
{
string? Description { get; set; }
Sentry.SpanId SpanId { get; }
Sentry.SpanStatus? Status { get; set; }
Sentry.ISpanRecorder RecordSpan(string operation, System.DateTimeOffset startTimestamp, System.TimeSpan duration, Sentry.SpanId? spanId = default, System.Action<Sentry.ISpanRecorder>? configure = null);
void SetData(string key, object? value);
void SetTag(string key, string value);
}
public interface ITransactionContext : Sentry.Protocol.ITraceContext
{
bool? IsParentSampled { get; }
Expand All @@ -303,6 +313,12 @@ namespace Sentry
{
string? Platform { get; set; }
}
public interface ITransactionRecorder : Sentry.ISpanRecorder
{
string? Environment { get; set; }
string? Release { get; set; }
Sentry.SentryId TraceId { get; }
}
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; }
Expand Down
16 changes: 16 additions & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ namespace Sentry
public static Sentry.SentryId CaptureMessage(this Sentry.IHub hub, string message, System.Action<Sentry.Scope> configureScope, Sentry.SentryLevel level = 1) { }
public static void LockScope(this Sentry.IHub hub) { }
public static System.IDisposable PushAndLockScope(this Sentry.IHub hub) { }
public static Sentry.SentryId RecordTransaction(this Sentry.IHub hub, string name, string operation, System.DateTimeOffset startTimestamp, System.TimeSpan duration, Sentry.SentryId? traceId = default, Sentry.SpanId? spanId = default, Sentry.SpanId? parentSpanId = default, System.Action<Sentry.ITransactionRecorder>? configure = null) { }
public static Sentry.ISpan StartSpan(this Sentry.IHub hub, string operation, string description) { }
public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, Sentry.ITransactionContext context) { }
public static Sentry.ITransactionTracer StartTransaction(this Sentry.IHub hub, string name, string operation) { }
Expand Down Expand Up @@ -293,6 +294,15 @@ namespace Sentry
Sentry.SentryTraceHeader GetTraceHeader();
void SetMeasurement(string name, Sentry.Protocol.Measurement measurement);
}
public interface ISpanRecorder
{
string? Description { get; set; }
Sentry.SpanId SpanId { get; }
Sentry.SpanStatus? Status { get; set; }
Sentry.ISpanRecorder RecordSpan(string operation, System.DateTimeOffset startTimestamp, System.TimeSpan duration, Sentry.SpanId? spanId = default, System.Action<Sentry.ISpanRecorder>? configure = null);
void SetData(string key, object? value);
void SetTag(string key, string value);
}
public interface ITransactionContext : Sentry.Protocol.ITraceContext
{
bool? IsParentSampled { get; }
Expand All @@ -303,6 +313,12 @@ namespace Sentry
{
string? Platform { get; set; }
}
public interface ITransactionRecorder : Sentry.ISpanRecorder
{
string? Environment { get; set; }
string? Release { get; set; }
Sentry.SentryId TraceId { get; }
}
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; }
Expand Down
Loading
Loading