From f31adac00f837b63071d5142ce1d8742e00541af Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 30 Nov 2020 21:18:06 +0200 Subject: [PATCH 01/54] wip --- .../Internal/Extensions/JsonExtensions.cs | 9 ++ src/Sentry/Protocol/Envelopes/Envelope.cs | 19 +++ src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 26 ++++ src/Sentry/Protocol/ISpan.cs | 32 +++++ src/Sentry/Protocol/Span.cs | 105 ++++++++++++++++ src/Sentry/Protocol/Transaction.cs | 112 ++++++++++++++++++ 6 files changed, 303 insertions(+) create mode 100644 src/Sentry/Protocol/ISpan.cs create mode 100644 src/Sentry/Protocol/Span.cs create mode 100644 src/Sentry/Protocol/Transaction.cs diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index a1d728ff02..fe86c70e30 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -57,6 +57,15 @@ public static void WriteDictionaryValue( } } + public static void WriteDictionary( + this Utf8JsonWriter writer, + string propertyName, + IEnumerable>? dic) + { + writer.WritePropertyName(propertyName); + writer.WriteDictionaryValue(dic); + } + public static void WriteDictionary( this Utf8JsonWriter writer, string propertyName, diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index d49708322d..1161d6cdc4 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -106,6 +106,25 @@ public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback) return new Envelope(header, items); } + /// + /// Creates an envelope that contains a single transaction. + /// + public static Envelope FromTransaction(Transaction transaction) + { + var header = new Dictionary + { + // TODO: Is this right? + [EventIdKey] = transaction.SpanId.ToString() + }; + + var items = new[] + { + EnvelopeItem.FromTransaction(transaction) + }; + + return new Envelope(header, items); + } + private static async Task> DeserializeHeaderAsync( Stream stream, CancellationToken cancellationToken = default) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 641b43e065..ceb317e423 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -18,6 +18,7 @@ internal sealed class EnvelopeItem : ISerializable, IDisposable private const string TypeKey = "type"; private const string TypeValueEvent = "event"; private const string TypeValueUserReport = "user_report"; + private const string TypeValueTransaction = "transaction"; private const string LengthKey = "length"; private const string FileNameKey = "file_name"; @@ -176,6 +177,19 @@ public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback) return new EnvelopeItem(header, new JsonSerializable(sentryUserFeedback)); } + /// + /// Creates an envelope item from transaction. + /// + public static EnvelopeItem FromTransaction(Transaction transaction) + { + var header = new Dictionary(StringComparer.Ordinal) + { + [TypeKey] = TypeValueTransaction + }; + + return new EnvelopeItem(header, new JsonSerializable(transaction)); + } + private static async Task> DeserializeHeaderAsync( Stream stream, CancellationToken cancellationToken = default) @@ -237,6 +251,18 @@ private static async Task DeserializePayloadAsync( ); } + // Transaction + if (string.Equals(payloadType, TypeValueTransaction, StringComparison.OrdinalIgnoreCase)) + { + var bufferLength = (int)(payloadLength ?? stream.Length); + var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); + using var jsonDocument = JsonDocument.Parse(buffer); + + return new JsonSerializable( + Transaction.FromJson(jsonDocument.RootElement.Clone()) + ); + } + // Arbitrary payload var payloadStream = new PartialStream(stream, stream.Position, payloadLength); diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs new file mode 100644 index 0000000000..611ad49813 --- /dev/null +++ b/src/Sentry/Protocol/ISpan.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Sentry.Protocol +{ + public interface ISpan + { + SentryId SpanId { get; set; } + + SentryId? ParentSpanId { get; set; } + + SentryId TraceId { get; set; } + + DateTimeOffset StartTimestamp { get; set; } + + DateTimeOffset EndTimestamp { get; set; } + + string? Operation { get; set; } + + string? Description { get; set; } + + string? Status { get; set; } + + bool IsSampled { get; set; } + + IReadOnlyDictionary Tags { get; } + + IReadOnlyDictionary Data { get; } + + ISpan StartChild(); + } +} diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs new file mode 100644 index 0000000000..f620c1d2d1 --- /dev/null +++ b/src/Sentry/Protocol/Span.cs @@ -0,0 +1,105 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol +{ + public class Span : ISpan, IJsonSerializable + { + public SentryId SpanId { get; set; } + public SentryId? ParentSpanId { get; set; } + public SentryId TraceId { get; set; } + public DateTimeOffset StartTimestamp { get; set; } + public DateTimeOffset EndTimestamp { get; set; } + public string? Operation { get; set; } + public string? Description { get; set; } + public string? Status { get; set; } + public bool IsSampled { get; set; } + + private ConcurrentDictionary? _tags; + public IReadOnlyDictionary Tags => _tags ??= new ConcurrentDictionary(); + + private ConcurrentDictionary? _data; + public IReadOnlyDictionary Data => _data ??= new ConcurrentDictionary(); + + public ISpan StartChild() => new Span {ParentSpanId = SpanId}; + + public void WriteTo(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WriteString("span_id", SpanId); + + if (ParentSpanId is {} parentSpanId) + { + writer.WriteString("parent_span_id", parentSpanId); + } + + writer.WriteString("trace_id", TraceId); + writer.WriteString("start_timestamp", StartTimestamp); + writer.WriteString("timestamp", EndTimestamp); + + if (!string.IsNullOrWhiteSpace(Operation)) + { + writer.WriteString("op", Operation); + } + + if (!string.IsNullOrWhiteSpace(Description)) + { + writer.WriteString("description", Description); + } + + if (!string.IsNullOrWhiteSpace(Status)) + { + writer.WriteString("status", Status); + } + + writer.WriteBoolean("sampled", IsSampled); + + if (_tags is {} tags && tags.Any()) + { + writer.WriteDictionary("tags", tags!); + } + + if (_data is {} data && data.Any()) + { + writer.WriteDictionary("data", data!); + } + + writer.WriteEndObject(); + } + + public static Span FromJson(JsonElement json) + { + var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); + var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; + var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; + var operation = json.GetPropertyOrNull("op")?.GetString(); + var description = json.GetPropertyOrNull("description")?.GetString(); + var status = json.GetPropertyOrNull("status")?.GetString(); + var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; + var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); + var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); + + return new Span + { + SpanId = spanId, + ParentSpanId = parentSpanId, + TraceId = traceId, + StartTimestamp = startTimestamp, + EndTimestamp = endTimestamp, + Operation = operation, + Description = description, + Status = status, + IsSampled = sampled, + _tags = tags, + _data = data + }; + } + } +} diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs new file mode 100644 index 0000000000..2edefd1aae --- /dev/null +++ b/src/Sentry/Protocol/Transaction.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol +{ + public class Transaction : ISpan, IJsonSerializable + { + public string? Name { get; set; } + public SentryId SpanId { get; set; } + public SentryId? ParentSpanId { get; set; } + public SentryId TraceId { get; set; } + public DateTimeOffset StartTimestamp { get; set; } + public DateTimeOffset EndTimestamp { get; set; } + public string? Operation { get; set; } + public string? Description { get; set; } + public string? Status { get; set; } + public bool IsSampled { get; set; } + + private Dictionary? _tags; + public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); + + private Dictionary? _data; + public IReadOnlyDictionary Data => _data ??= new Dictionary(); + + public ISpan StartChild() => new Span {ParentSpanId = SpanId}; + + public void WriteTo(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + if (!string.IsNullOrWhiteSpace(Name)) + { + writer.WriteString("name", Name); + } + + writer.WriteString("span_id", SpanId); + + if (ParentSpanId is {} parentSpanId) + { + writer.WriteString("parent_span_id", parentSpanId); + } + + writer.WriteString("trace_id", TraceId); + writer.WriteString("start_timestamp", StartTimestamp); + writer.WriteString("timestamp", EndTimestamp); + + if (!string.IsNullOrWhiteSpace(Operation)) + { + writer.WriteString("op", Operation); + } + + if (!string.IsNullOrWhiteSpace(Description)) + { + writer.WriteString("description", Description); + } + + if (!string.IsNullOrWhiteSpace(Status)) + { + writer.WriteString("status", Status); + } + + writer.WriteBoolean("sampled", IsSampled); + + if (_tags is {} tags && tags.Any()) + { + writer.WriteDictionary("tags", tags!); + } + + if (_data is {} data && data.Any()) + { + writer.WriteDictionary("data", data!); + } + + writer.WriteEndObject(); + } + + public static Transaction FromJson(JsonElement json) + { + var name = json.GetPropertyOrNull("name")?.GetString(); + var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); + var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; + var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; + var operation = json.GetPropertyOrNull("op")?.GetString(); + var description = json.GetPropertyOrNull("description")?.GetString(); + var status = json.GetPropertyOrNull("status")?.GetString(); + var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; + var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new Dictionary(v!)); + var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new Dictionary(v!)); + + return new Transaction + { + Name = name, + SpanId = spanId, + ParentSpanId = parentSpanId, + TraceId = traceId, + StartTimestamp = startTimestamp, + EndTimestamp = endTimestamp, + Operation = operation, + Description = description, + Status = status, + IsSampled = sampled, + _tags = tags, + _data = data + }; + } + } +} From 79d2397c129786d8625167e712f253148d3864d5 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 30 Nov 2020 21:25:25 +0200 Subject: [PATCH 02/54] wip --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5caa0636e..5d0b7bf2a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ * Add net5.0 TFM to libraries #606 * Add more logging to CachingTransport #619 * Bump Microsoft.Bcl.AsyncInterfaces to 5.0.0 #618 +* Add support for performance ## 3.0.0-alpha.5 From d7c9974f3061d26ae0b5c9bdfffd29790ffc5f10 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 1 Dec 2020 19:42:43 +0200 Subject: [PATCH 03/54] wip --- src/Sentry/ISentryTraceSampler.cs | 7 +++ src/Sentry/Protocol/ISpan.cs | 2 +- src/Sentry/Protocol/Span.cs | 9 ++-- src/Sentry/Protocol/SpanStatus.cs | 78 ++++++++++++++++++++++++++++++ src/Sentry/Protocol/Transaction.cs | 36 +++++++++++--- src/Sentry/SentryOptions.cs | 29 +++++++++++ 6 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 src/Sentry/ISentryTraceSampler.cs create mode 100644 src/Sentry/Protocol/SpanStatus.cs diff --git a/src/Sentry/ISentryTraceSampler.cs b/src/Sentry/ISentryTraceSampler.cs new file mode 100644 index 0000000000..2e695f5312 --- /dev/null +++ b/src/Sentry/ISentryTraceSampler.cs @@ -0,0 +1,7 @@ +namespace Sentry +{ + public interface ISentryTraceSampler + { + double GetSampleRate(); + } +} diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 611ad49813..54f6703d3b 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -19,7 +19,7 @@ public interface ISpan string? Description { get; set; } - string? Status { get; set; } + SpanStatus? Status { get; set; } bool IsSampled { get; set; } diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index f620c1d2d1..04ad693112 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -7,6 +7,7 @@ namespace Sentry.Protocol { + // https://develop.sentry.dev/sdk/event-payloads/span public class Span : ISpan, IJsonSerializable { public SentryId SpanId { get; set; } @@ -16,7 +17,7 @@ public class Span : ISpan, IJsonSerializable public DateTimeOffset EndTimestamp { get; set; } public string? Operation { get; set; } public string? Description { get; set; } - public string? Status { get; set; } + public SpanStatus? Status { get; set; } public bool IsSampled { get; set; } private ConcurrentDictionary? _tags; @@ -52,9 +53,9 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("description", Description); } - if (!string.IsNullOrWhiteSpace(Status)) + if (Status is {} status) { - writer.WriteString("status", Status); + writer.WriteString("status", status.ToString().ToLowerInvariant()); } writer.WriteBoolean("sampled", IsSampled); @@ -81,7 +82,7 @@ public static Span FromJson(JsonElement json) var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; var operation = json.GetPropertyOrNull("op")?.GetString(); var description = json.GetPropertyOrNull("description")?.GetString(); - var status = json.GetPropertyOrNull("status")?.GetString(); + var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); diff --git a/src/Sentry/Protocol/SpanStatus.cs b/src/Sentry/Protocol/SpanStatus.cs new file mode 100644 index 0000000000..7c76d0d0d4 --- /dev/null +++ b/src/Sentry/Protocol/SpanStatus.cs @@ -0,0 +1,78 @@ +using System.Runtime.Serialization; + +namespace Sentry.Protocol +{ + /// + /// Span status. + /// + public enum SpanStatus + { + /// The operation completed successfully. + [EnumMember(Value = "ok")] + Ok, + + /// Deadline expired before operation could complete. + [EnumMember(Value = "deadlineExceeded")] + DeadlineExceeded, + + /// 401 Unauthorized (actually does mean unauthenticated according to RFC 7235). + [EnumMember(Value = "unauthenticated")] + Unauthenticated, + + /// 403 Forbidden + [EnumMember(Value = "permissionDenied")] + PermissionDenied, + + /// 404 Not Found. Some requested entity (file or directory) was not found. + [EnumMember(Value = "notFound")] + NotFound, + + /// 429 Too Many Requests + [EnumMember(Value = "resourceExhausted")] + ResourceExhausted, + + /// Client specified an invalid argument. 4xx. + [EnumMember(Value = "invalidArgument")] + InvalidArgument, + + /// 501 Not Implemented + [EnumMember(Value = "unimplemented")] + Unimplemented, + + /// 503 Service Unavailable + [EnumMember(Value = "unavailable")] + Unavailable, + + /// Other/generic 5xx. + [EnumMember(Value = "internalError")] + InternalError, + + /// Unknown. Any non-standard HTTP status code. + [EnumMember(Value = "unknownError")] + UnknownError, + + /// The operation was cancelled (typically by the user). + [EnumMember(Value = "cancelled")] + Cancelled, + + /// Already exists (409). + [EnumMember(Value = "alreadyExists")] + AlreadyExists, + + /// Operation was rejected because the system is not in a state required for the operation's + [EnumMember(Value = "failedPrecondition")] + FailedPrecondition, + + /// The operation was aborted, typically due to a concurrency issue. + [EnumMember(Value = "aborted")] + Aborted, + + /// Operation was attempted past the valid range. + [EnumMember(Value = "outOfRange")] + OutOfRange, + + /// Unrecoverable data loss or corruption + [EnumMember(Value = "dataLoss")] + DataLoss, + } +} diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 2edefd1aae..8b2630f6fa 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -6,6 +6,7 @@ namespace Sentry.Protocol { + // https://develop.sentry.dev/sdk/event-payloads/transaction public class Transaction : ISpan, IJsonSerializable { public string? Name { get; set; } @@ -16,7 +17,7 @@ public class Transaction : ISpan, IJsonSerializable public DateTimeOffset EndTimestamp { get; set; } public string? Operation { get; set; } public string? Description { get; set; } - public string? Status { get; set; } + public SpanStatus? Status { get; set; } public bool IsSampled { get; set; } private Dictionary? _tags; @@ -25,7 +26,16 @@ public class Transaction : ISpan, IJsonSerializable private Dictionary? _data; public IReadOnlyDictionary Data => _data ??= new Dictionary(); - public ISpan StartChild() => new Span {ParentSpanId = SpanId}; + private List? _children; + public IReadOnlyList Children => _children ??= new List(); + + public ISpan StartChild() + { + var span = new Span {ParentSpanId = SpanId}; + (_children ??= new List()).Add(span); + + return span; + } public void WriteTo(Utf8JsonWriter writer) { @@ -57,9 +67,9 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("description", Description); } - if (!string.IsNullOrWhiteSpace(Status)) + if (Status is {} status) { - writer.WriteString("status", Status); + writer.WriteString("status", status.ToString().ToLowerInvariant()); } writer.WriteBoolean("sampled", IsSampled); @@ -74,6 +84,18 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteDictionary("data", data!); } + if (_children is {} children && children.Any()) + { + writer.WriteStartArray("spans"); + + foreach (var i in children) + { + writer.WriteSerializableValue(i); + } + + writer.WriteEndArray(); + } + writer.WriteEndObject(); } @@ -87,10 +109,11 @@ public static Transaction FromJson(JsonElement json) var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; var operation = json.GetPropertyOrNull("op")?.GetString(); var description = json.GetPropertyOrNull("description")?.GetString(); - var status = json.GetPropertyOrNull("status")?.GetString(); + var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new Dictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new Dictionary(v!)); + var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); return new Transaction { @@ -105,7 +128,8 @@ public static Transaction FromJson(JsonElement json) Status = status, IsSampled = sampled, _tags = tags, - _data = data + _data = data, + _children = children }; } } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index de9b3766c9..5a06261641 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -383,6 +383,35 @@ public IDiagnosticLogger? DiagnosticLogger /// public TimeSpan CacheFlushTimeout { get; set; } = TimeSpan.FromSeconds(1); + private double _traceSampleRate = 1.0; + + /// + /// Indicates the percentage of the tracing data that is collected. + /// Setting this to 0 discards all trace data. + /// Setting this to 1.0 collects all trace data. + /// Values outside of this range are invalid. + /// + public double TraceSampleRate + { + get => _traceSampleRate; + set + { + if (value < 0 || value > 1) + { + throw new InvalidOperationException( + $"The value {value} is not a valid tracing sample rate. Use values between 0 and 1." + ); + } + + _traceSampleRate = value; + } + } + + /// + /// Custom logic that determines trace sample rate depending on the context. + /// + public ISentryTraceSampler? TraceSampler { get; set; } + /// /// Creates a new instance of /// From 51783e88f38d6d1e7658226f341726b6aabab48b Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 1 Dec 2020 21:19:31 +0200 Subject: [PATCH 04/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 9 +++++++ src/Sentry.AspNetCore/SentryStartupFilter.cs | 11 ++++----- src/Sentry.AspNetCore/SpanStatusMapper.cs | 23 ++++++++++++++++++ src/Sentry/Protocol/IScope.cs | 11 --------- src/Sentry/Protocol/ISpan.cs | 6 +++-- src/Sentry/Protocol/SentryEvent.cs | 10 +++++++- src/Sentry/Protocol/SentryId.cs | 5 ++++ src/Sentry/Protocol/Span.cs | 25 +++++++++++++------- src/Sentry/Protocol/Transaction.cs | 25 +++++++++++++------- src/Sentry/Scope.cs | 6 ++--- 10 files changed, 92 insertions(+), 39 deletions(-) create mode 100644 src/Sentry.AspNetCore/SpanStatusMapper.cs diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 76cd437e62..7e00d491d5 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -104,7 +104,9 @@ public async Task InvokeAsync(HttpContext context) // event creation will be sent to Sentry scope.OnEvaluating += (_, __) => PopulateScope(context, scope); + scope.Transaction.StartTimestamp = DateTimeOffset.Now; }); + try { await _next(context).ConfigureAwait(false); @@ -122,6 +124,13 @@ public async Task InvokeAsync(HttpContext context) ExceptionDispatchInfo.Capture(e).Throw(); } + finally + { + hub.ConfigureScope(scope => + { + scope.Transaction.Finish(); + }); + } void CaptureException(Exception e) { diff --git a/src/Sentry.AspNetCore/SentryStartupFilter.cs b/src/Sentry.AspNetCore/SentryStartupFilter.cs index 80f0a84726..23ab0aa2c6 100644 --- a/src/Sentry.AspNetCore/SentryStartupFilter.cs +++ b/src/Sentry.AspNetCore/SentryStartupFilter.cs @@ -7,12 +7,11 @@ namespace Sentry.AspNetCore /// internal class SentryStartupFilter : IStartupFilter { - public Action Configure(Action next) - => e => - { - _ = e.UseSentry(); + public Action Configure(Action next) => e => + { + e.UseSentry(); - next(e); - }; + next(e); + }; } } diff --git a/src/Sentry.AspNetCore/SpanStatusMapper.cs b/src/Sentry.AspNetCore/SpanStatusMapper.cs new file mode 100644 index 0000000000..39868b7edb --- /dev/null +++ b/src/Sentry.AspNetCore/SpanStatusMapper.cs @@ -0,0 +1,23 @@ +using Sentry.Protocol; + +namespace Sentry.AspNetCore +{ + internal static class SpanStatusMapper + { + public static SpanStatus FromStatusCode(int statusCode) => statusCode switch + { + 400 => SpanStatus.InvalidArgument, + 401 => SpanStatus.Unauthenticated, + 403 => SpanStatus.PermissionDenied, + 404 => SpanStatus.NotFound, + 409 => SpanStatus.AlreadyExists, + 429 => SpanStatus.ResourceExhausted, + 499 => SpanStatus.Cancelled, + 500 => SpanStatus.InternalError, + 501 => SpanStatus.Unimplemented, + 503 => SpanStatus.Unavailable, + 504 => SpanStatus.DeadlineExceeded, + _ => SpanStatus.UnknownError + }; + } +} diff --git a/src/Sentry/Protocol/IScope.cs b/src/Sentry/Protocol/IScope.cs index c69f302a09..eb79defd4d 100644 --- a/src/Sentry/Protocol/IScope.cs +++ b/src/Sentry/Protocol/IScope.cs @@ -28,17 +28,6 @@ public interface IScope /// SentryLevel? Level { get; set; } - /// - /// The name of the transaction in which there was an event. - /// - /// - /// A transaction should only be defined when it can be well defined. - /// On a Web framework, for example, a transaction is the route template - /// rather than the actual request path. That is so GET /user/10 and /user/20 - /// (which have route template /user/{id}) are identified as the same transaction. - /// - string? Transaction { get; set; } - /// /// Gets or sets the HTTP. /// diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 54f6703d3b..3e9289b2bb 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -5,9 +5,9 @@ namespace Sentry.Protocol { public interface ISpan { - SentryId SpanId { get; set; } + SentryId SpanId { get; } - SentryId? ParentSpanId { get; set; } + SentryId? ParentSpanId { get; } SentryId TraceId { get; set; } @@ -28,5 +28,7 @@ public interface ISpan IReadOnlyDictionary Data { get; } ISpan StartChild(); + + void Finish(); } } diff --git a/src/Sentry/Protocol/SentryEvent.cs b/src/Sentry/Protocol/SentryEvent.cs index c8d087d11d..51749ad7ad 100644 --- a/src/Sentry/Protocol/SentryEvent.cs +++ b/src/Sentry/Protocol/SentryEvent.cs @@ -111,7 +111,15 @@ public IEnumerable? SentryThreads /// public SentryLevel? Level { get; set; } - /// + /// + /// The name of the transaction in which there was an event. + /// + /// + /// A transaction should only be defined when it can be well defined. + /// On a Web framework, for example, a transaction is the route template + /// rather than the actual request path. That is so GET /user/10 and /user/20 + /// (which have route template /user/{id}) are identified as the same transaction. + /// public string? Transaction { get; set; } private Request? _request; diff --git a/src/Sentry/Protocol/SentryId.cs b/src/Sentry/Protocol/SentryId.cs index 2f09db670d..c9547b4ecf 100644 --- a/src/Sentry/Protocol/SentryId.cs +++ b/src/Sentry/Protocol/SentryId.cs @@ -39,6 +39,11 @@ namespace Sentry.Protocol /// public override int GetHashCode() => _eventId.GetHashCode(); + /// + /// Generates a new Sentry ID. + /// + public static SentryId Create() => new SentryId(Guid.NewGuid()); + /// public void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(ToString()); diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 04ad693112..e93d9f3f71 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -10,11 +10,11 @@ namespace Sentry.Protocol // https://develop.sentry.dev/sdk/event-payloads/span public class Span : ISpan, IJsonSerializable { - public SentryId SpanId { get; set; } - public SentryId? ParentSpanId { get; set; } + public SentryId SpanId { get; } + public SentryId? ParentSpanId { get; } public SentryId TraceId { get; set; } - public DateTimeOffset StartTimestamp { get; set; } - public DateTimeOffset EndTimestamp { get; set; } + public DateTimeOffset StartTimestamp { get; set; } = DateTimeOffset.Now; + public DateTimeOffset EndTimestamp { get; set; } = DateTimeOffset.Now; public string? Operation { get; set; } public string? Description { get; set; } public SpanStatus? Status { get; set; } @@ -26,7 +26,18 @@ public class Span : ISpan, IJsonSerializable private ConcurrentDictionary? _data; public IReadOnlyDictionary Data => _data ??= new ConcurrentDictionary(); - public ISpan StartChild() => new Span {ParentSpanId = SpanId}; + public Span(SentryId? spanId = null, SentryId? parentSpanId = null) + { + SpanId = spanId ?? SentryId.Create(); + ParentSpanId = parentSpanId; + } + + public ISpan StartChild() => new Span(parentSpanId: SpanId); + + public void Finish() + { + EndTimestamp = DateTimeOffset.Now; + } public void WriteTo(Utf8JsonWriter writer) { @@ -87,10 +98,8 @@ public static Span FromJson(JsonElement json) var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); - return new Span + return new Span(spanId, parentSpanId) { - SpanId = spanId, - ParentSpanId = parentSpanId, TraceId = traceId, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 8b2630f6fa..ce9e9d2367 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -10,11 +10,11 @@ namespace Sentry.Protocol public class Transaction : ISpan, IJsonSerializable { public string? Name { get; set; } - public SentryId SpanId { get; set; } - public SentryId? ParentSpanId { get; set; } + public SentryId SpanId { get; } + public SentryId? ParentSpanId { get; } public SentryId TraceId { get; set; } - public DateTimeOffset StartTimestamp { get; set; } - public DateTimeOffset EndTimestamp { get; set; } + public DateTimeOffset StartTimestamp { get; set; } = DateTimeOffset.Now; + public DateTimeOffset EndTimestamp { get; set; } = DateTimeOffset.Now; public string? Operation { get; set; } public string? Description { get; set; } public SpanStatus? Status { get; set; } @@ -29,14 +29,25 @@ public class Transaction : ISpan, IJsonSerializable private List? _children; public IReadOnlyList Children => _children ??= new List(); + public Transaction(SentryId? spanId = null, SentryId? parentSpanId = null) + { + SpanId = spanId ?? SentryId.Create(); + ParentSpanId = parentSpanId; + } + public ISpan StartChild() { - var span = new Span {ParentSpanId = SpanId}; + var span = new Span(parentSpanId: SpanId); (_children ??= new List()).Add(span); return span; } + public void Finish() + { + EndTimestamp = DateTimeOffset.Now; + } + public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); @@ -115,11 +126,9 @@ public static Transaction FromJson(JsonElement json) var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new Dictionary(v!)); var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); - return new Transaction + return new Transaction(spanId, parentSpanId) { Name = name, - SpanId = spanId, - ParentSpanId = parentSpanId, TraceId = traceId, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 7408aa7d5e..06fe03d153 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -83,9 +83,6 @@ internal SentryId LastEventId /// public SentryLevel? Level { get; set; } - /// - public string? Transaction { get; set; } - private Request? _request; /// @@ -131,6 +128,9 @@ public User User /// public IReadOnlyDictionary Tags { get; } = new ConcurrentDictionary(); + private Transaction? _transaction; + public Transaction Transaction => _transaction ??= new Transaction(); + /// /// Creates a scope with the specified options. /// From c99635da62183de25b1e29c0e82f8802181a41cf Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 1 Dec 2020 21:20:48 +0200 Subject: [PATCH 05/54] wip --- src/Sentry/Protocol/Span.cs | 5 +++-- src/Sentry/Protocol/Transaction.cs | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index e93d9f3f71..e314ef3fff 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -13,8 +13,8 @@ public class Span : ISpan, IJsonSerializable public SentryId SpanId { get; } public SentryId? ParentSpanId { get; } public SentryId TraceId { get; set; } - public DateTimeOffset StartTimestamp { get; set; } = DateTimeOffset.Now; - public DateTimeOffset EndTimestamp { get; set; } = DateTimeOffset.Now; + public DateTimeOffset StartTimestamp { get; set; } + public DateTimeOffset EndTimestamp { get; set; } public string? Operation { get; set; } public string? Description { get; set; } public SpanStatus? Status { get; set; } @@ -30,6 +30,7 @@ public Span(SentryId? spanId = null, SentryId? parentSpanId = null) { SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; + StartTimestamp = EndTimestamp = DateTimeOffset.Now; } public ISpan StartChild() => new Span(parentSpanId: SpanId); diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index ce9e9d2367..d1781e17e9 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -13,8 +13,8 @@ public class Transaction : ISpan, IJsonSerializable public SentryId SpanId { get; } public SentryId? ParentSpanId { get; } public SentryId TraceId { get; set; } - public DateTimeOffset StartTimestamp { get; set; } = DateTimeOffset.Now; - public DateTimeOffset EndTimestamp { get; set; } = DateTimeOffset.Now; + public DateTimeOffset StartTimestamp { get; set; } + public DateTimeOffset EndTimestamp { get; set; } public string? Operation { get; set; } public string? Description { get; set; } public SpanStatus? Status { get; set; } @@ -33,6 +33,7 @@ public Transaction(SentryId? spanId = null, SentryId? parentSpanId = null) { SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; + StartTimestamp = EndTimestamp = DateTimeOffset.Now; } public ISpan StartChild() From 7ad5ab5e2bd45ebb60aa2729db1f3dc7fb6e743d Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 17:58:34 +0200 Subject: [PATCH 06/54] wip --- src/Sentry.AspNetCore/ScopeExtensions.cs | 4 +++- src/Sentry.AspNetCore/SentryMiddleware.cs | 15 ++++++++++----- src/Sentry.AspNetCore/SpanStatusMapper.cs | 5 ++++- src/Sentry/Protocol/Envelopes/Envelope.cs | 3 +-- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 2 +- src/Sentry/Protocol/SentryEvent.cs | 2 +- src/Sentry/Protocol/Transaction.cs | 8 ++++---- src/Sentry/Scope.cs | 6 +++++- src/Sentry/ScopeExtensions.cs | 2 +- .../Protocol/Envelopes/EnvelopeTests.cs | 2 +- test/Sentry.Tests/Protocol/SentryIdTests.cs | 2 +- 11 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index 10c7ce706f..2375d1d55d 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -87,7 +87,9 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC scope.SetTag("route.area", area); } - scope.Transaction = area == null ? $"{controller}.{action}" : $"{area}.{controller}.{action}"; + scope.Transaction.Name = area == null + ? $"{controller}.{action}" + : $"{area}.{controller}.{action}"; } } catch(Exception e) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 7e00d491d5..c8f0d6a58d 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -94,6 +94,8 @@ public async Task InvokeAsync(HttpContext context) }); } + Transaction? transaction = null; + hub.ConfigureScope(scope => { // At the point lots of stuff from the request are not yet filled @@ -104,11 +106,17 @@ public async Task InvokeAsync(HttpContext context) // event creation will be sent to Sentry scope.OnEvaluating += (_, __) => PopulateScope(context, scope); - scope.Transaction.StartTimestamp = DateTimeOffset.Now; + transaction = scope.Transaction; }); try { + if (transaction is {}) + { + transaction.StartTimestamp = DateTimeOffset.Now; + transaction.Operation = "http.server"; + } + await _next(context).ConfigureAwait(false); // When an exception was handled by other component (i.e: UseExceptionHandler feature). @@ -126,10 +134,7 @@ public async Task InvokeAsync(HttpContext context) } finally { - hub.ConfigureScope(scope => - { - scope.Transaction.Finish(); - }); + transaction?.Finish(); } void CaptureException(Exception e) diff --git a/src/Sentry.AspNetCore/SpanStatusMapper.cs b/src/Sentry.AspNetCore/SpanStatusMapper.cs index 39868b7edb..08bff889ea 100644 --- a/src/Sentry.AspNetCore/SpanStatusMapper.cs +++ b/src/Sentry.AspNetCore/SpanStatusMapper.cs @@ -6,6 +6,7 @@ internal static class SpanStatusMapper { public static SpanStatus FromStatusCode(int statusCode) => statusCode switch { + < 400 => SpanStatus.Ok, 400 => SpanStatus.InvalidArgument, 401 => SpanStatus.Unauthenticated, 403 => SpanStatus.PermissionDenied, @@ -13,11 +14,13 @@ internal static class SpanStatusMapper 409 => SpanStatus.AlreadyExists, 429 => SpanStatus.ResourceExhausted, 499 => SpanStatus.Cancelled, + < 500 => SpanStatus.InvalidArgument, 500 => SpanStatus.InternalError, 501 => SpanStatus.Unimplemented, 503 => SpanStatus.Unavailable, 504 => SpanStatus.DeadlineExceeded, - _ => SpanStatus.UnknownError + < 600 => SpanStatus.InternalError, + _ => SpanStatus.UnknownError }; } } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 1161d6cdc4..d0dcb08b38 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -111,9 +111,8 @@ public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback) /// public static Envelope FromTransaction(Transaction transaction) { - var header = new Dictionary + var header = new Dictionary { - // TODO: Is this right? [EventIdKey] = transaction.SpanId.ToString() }; diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index ceb317e423..bb8d3a19e6 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -182,7 +182,7 @@ public static EnvelopeItem FromUserFeedback(UserFeedback sentryUserFeedback) /// public static EnvelopeItem FromTransaction(Transaction transaction) { - var header = new Dictionary(StringComparer.Ordinal) + var header = new Dictionary(StringComparer.Ordinal) { [TypeKey] = TypeValueTransaction }; diff --git a/src/Sentry/Protocol/SentryEvent.cs b/src/Sentry/Protocol/SentryEvent.cs index 51749ad7ad..478f52669b 100644 --- a/src/Sentry/Protocol/SentryEvent.cs +++ b/src/Sentry/Protocol/SentryEvent.cs @@ -201,7 +201,7 @@ internal SentryEvent( { Exception = exception; Timestamp = timestamp ?? DateTimeOffset.UtcNow; - EventId = eventId != default ? eventId : (SentryId)Guid.NewGuid(); + EventId = eventId != default ? eventId : SentryId.Create(); ScopeOptions = options; Platform = Constants.Platform; } diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index d1781e17e9..b4f4e830ba 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -123,8 +123,8 @@ public static Transaction FromJson(JsonElement json) var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; - var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new Dictionary(v!)); - var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new Dictionary(v!)); + var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.ToDictionary(); + var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.ToDictionary(); var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); return new Transaction(spanId, parentSpanId) @@ -137,8 +137,8 @@ public static Transaction FromJson(JsonElement json) Description = description, Status = status, IsSampled = sampled, - _tags = tags, - _data = data, + _tags = tags!, + _data = data!, _children = children }; } diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 06fe03d153..15fcc26b07 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -129,7 +129,11 @@ public User User public IReadOnlyDictionary Tags { get; } = new ConcurrentDictionary(); private Transaction? _transaction; - public Transaction Transaction => _transaction ??= new Transaction(); + public Transaction Transaction + { + get => _transaction ??= new Transaction(); + set => _transaction = value; + } /// /// Creates a scope with the specified options. diff --git a/src/Sentry/ScopeExtensions.cs b/src/Sentry/ScopeExtensions.cs index 73f4c00d5d..c9d15fd818 100644 --- a/src/Sentry/ScopeExtensions.cs +++ b/src/Sentry/ScopeExtensions.cs @@ -280,7 +280,7 @@ public static void UnsetTag(this IScope scope, string key) /// Conflicting keys are not overriden. /// This is a shallow copy. /// - public static void Apply(this IScope from, IScope to) + public static void Apply(this Scope from, Scope to) { // Not to throw on code that ignores nullability warnings. // ReSharper disable ConditionIsAlwaysTrueOrFalse diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index 4c3af34d5d..e41c607e88 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -405,7 +405,7 @@ public async Task Roundtrip_WithUserFeedback_Success() { // Arrange var feedback = new UserFeedback( - new SentryId(Guid.NewGuid()), + SentryId.Create(), "foo@bar.com", "Everything sucks", "Donald J. Trump" diff --git a/test/Sentry.Tests/Protocol/SentryIdTests.cs b/test/Sentry.Tests/Protocol/SentryIdTests.cs index 3a38c8ab2f..3ad949a92a 100644 --- a/test/Sentry.Tests/Protocol/SentryIdTests.cs +++ b/test/Sentry.Tests/Protocol/SentryIdTests.cs @@ -17,7 +17,7 @@ public void ToString_Equal_GuidToStringN() [Fact] public void Implicit_ToGuid() { - var expected = new SentryId(Guid.NewGuid()); + var expected = SentryId.Create(); Guid actual = expected; Assert.Equal(expected.ToString(), actual.ToString("N")); } From 96c443e72ee895971cb9e0095f4f6ec92fb50cf1 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 18:13:36 +0200 Subject: [PATCH 07/54] wip --- src/Sentry.AspNetCore/ScopeExtensions.cs | 9 ++++++--- src/Sentry.AspNetCore/SentryMiddleware.cs | 13 +++---------- src/Sentry/Extensibility/DisabledHub.cs | 2 ++ src/Sentry/Extensibility/HubAdapter.cs | 4 ++++ src/Sentry/ISentryScopeManager.cs | 3 +++ src/Sentry/Internal/Hub.cs | 5 +++++ src/Sentry/Internal/SentryScopeManager.cs | 7 +++++++ src/Sentry/Protocol/ISpan.cs | 4 ++-- src/Sentry/Protocol/Span.cs | 12 ++++++------ src/Sentry/Protocol/Transaction.cs | 16 +++++++++------- src/Sentry/Scope.cs | 10 ++++------ src/Sentry/SentrySdk.cs | 4 ++++ 12 files changed, 55 insertions(+), 34 deletions(-) diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index 2375d1d55d..efef93130f 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -87,9 +87,12 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC scope.SetTag("route.area", area); } - scope.Transaction.Name = area == null - ? $"{controller}.{action}" - : $"{area}.{controller}.{action}"; + if (scope.Transaction is {} transaction) + { + transaction.Name = area == null + ? $"{controller}.{action}" + : $"{area}.{controller}.{action}"; + } } } catch(Exception e) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index c8f0d6a58d..becf40e593 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -94,8 +94,6 @@ public async Task InvokeAsync(HttpContext context) }); } - Transaction? transaction = null; - hub.ConfigureScope(scope => { // At the point lots of stuff from the request are not yet filled @@ -106,17 +104,12 @@ public async Task InvokeAsync(HttpContext context) // event creation will be sent to Sentry scope.OnEvaluating += (_, __) => PopulateScope(context, scope); - transaction = scope.Transaction; }); + var transaction = hub.GetTransaction("http.server"); + try { - if (transaction is {}) - { - transaction.StartTimestamp = DateTimeOffset.Now; - transaction.Operation = "http.server"; - } - await _next(context).ConfigureAwait(false); // When an exception was handled by other component (i.e: UseExceptionHandler feature). @@ -134,7 +127,7 @@ public async Task InvokeAsync(HttpContext context) } finally { - transaction?.Finish(); + transaction.Finish(); } void CaptureException(Exception e) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 80f93cb4a0..d477b26307 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -52,6 +52,8 @@ public void WithScope(Action scopeCallback) { } + public Transaction GetTransaction(string operation) => new Transaction(operation); + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index f0d77482f6..2cbf681853 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -71,6 +71,10 @@ public IDisposable PushScope(TState state) public void WithScope(Action scopeCallback) => SentrySdk.WithScope(scopeCallback); + [DebuggerStepThrough] + public Transaction GetTransaction(string operation) + => SentrySdk.GetTransaction(operation); + /// /// Forwards the call to . /// diff --git a/src/Sentry/ISentryScopeManager.cs b/src/Sentry/ISentryScopeManager.cs index e4f866c761..4cba771403 100644 --- a/src/Sentry/ISentryScopeManager.cs +++ b/src/Sentry/ISentryScopeManager.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Sentry.Protocol; namespace Sentry { @@ -55,5 +56,7 @@ public interface ISentryScopeManager /// /// The callback to run with the one time scope. void WithScope(Action scopeCallback); + + Transaction GetTransaction(string operation); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 3acb6e03dd..c30f761645 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -98,6 +98,11 @@ public void WithScope(Action scopeCallback) } } + public Transaction GetTransaction(string operation) + { + return ScopeManager.GetTransaction(operation); + } + public void BindClient(ISentryClient client) => ScopeManager.BindClient(client); public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) diff --git a/src/Sentry/Internal/SentryScopeManager.cs b/src/Sentry/Internal/SentryScopeManager.cs index 1d0cce1443..27be62d778 100644 --- a/src/Sentry/Internal/SentryScopeManager.cs +++ b/src/Sentry/Internal/SentryScopeManager.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Sentry.Extensibility; +using Sentry.Protocol; namespace Sentry.Internal { @@ -96,6 +97,12 @@ public void WithScope(Action scopeCallback) } } + public Transaction GetTransaction(string operation) + { + var (scope, _) = GetCurrent(); + return scope.Transaction ?? scope.CreateTransaction(operation); + } + public void BindClient(ISentryClient? client) { _options.DiagnosticLogger?.LogDebug("Binding a new client to the current scope."); diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 3e9289b2bb..6968e2d57c 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -15,7 +15,7 @@ public interface ISpan DateTimeOffset EndTimestamp { get; set; } - string? Operation { get; set; } + string Operation { get; } string? Description { get; set; } @@ -27,7 +27,7 @@ public interface ISpan IReadOnlyDictionary Data { get; } - ISpan StartChild(); + ISpan StartChild(string operation); void Finish(); } diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index e314ef3fff..88978b6097 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -15,7 +15,7 @@ public class Span : ISpan, IJsonSerializable public SentryId TraceId { get; set; } public DateTimeOffset StartTimestamp { get; set; } public DateTimeOffset EndTimestamp { get; set; } - public string? Operation { get; set; } + public string Operation { get; } public string? Description { get; set; } public SpanStatus? Status { get; set; } public bool IsSampled { get; set; } @@ -26,14 +26,15 @@ public class Span : ISpan, IJsonSerializable private ConcurrentDictionary? _data; public IReadOnlyDictionary Data => _data ??= new ConcurrentDictionary(); - public Span(SentryId? spanId = null, SentryId? parentSpanId = null) + internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") { SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; + Operation = operation; StartTimestamp = EndTimestamp = DateTimeOffset.Now; } - public ISpan StartChild() => new Span(parentSpanId: SpanId); + public ISpan StartChild(string operation) => new Span(null, SpanId, operation); public void Finish() { @@ -92,19 +93,18 @@ public static Span FromJson(JsonElement json) var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; - var operation = json.GetPropertyOrNull("op")?.GetString(); + var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); - return new Span(spanId, parentSpanId) + return new Span(spanId, parentSpanId, operation) { TraceId = traceId, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, - Operation = operation, Description = description, Status = status, IsSampled = sampled, diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index b4f4e830ba..88940e1b57 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -15,7 +15,7 @@ public class Transaction : ISpan, IJsonSerializable public SentryId TraceId { get; set; } public DateTimeOffset StartTimestamp { get; set; } public DateTimeOffset EndTimestamp { get; set; } - public string? Operation { get; set; } + public string Operation { get; } public string? Description { get; set; } public SpanStatus? Status { get; set; } public bool IsSampled { get; set; } @@ -29,16 +29,19 @@ public class Transaction : ISpan, IJsonSerializable private List? _children; public IReadOnlyList Children => _children ??= new List(); - public Transaction(SentryId? spanId = null, SentryId? parentSpanId = null) + internal Transaction(SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") { SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; + Operation = operation; StartTimestamp = EndTimestamp = DateTimeOffset.Now; } - public ISpan StartChild() + public Transaction(string operation) : this(null, null, operation) {} + + public ISpan StartChild(string operation) { - var span = new Span(parentSpanId: SpanId); + var span = new Span(null, SpanId, operation); (_children ??= new List()).Add(span); return span; @@ -119,7 +122,7 @@ public static Transaction FromJson(JsonElement json) var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; - var operation = json.GetPropertyOrNull("op")?.GetString(); + var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; @@ -127,13 +130,12 @@ public static Transaction FromJson(JsonElement json) var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.ToDictionary(); var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); - return new Transaction(spanId, parentSpanId) + return new Transaction(spanId, parentSpanId, operation) { Name = name, TraceId = traceId, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, - Operation = operation, Description = description, Status = status, IsSampled = sampled, diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 15fcc26b07..11e9f748e6 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -128,12 +128,7 @@ public User User /// public IReadOnlyDictionary Tags { get; } = new ConcurrentDictionary(); - private Transaction? _transaction; - public Transaction Transaction - { - get => _transaction ??= new Transaction(); - set => _transaction = value; - } + public Transaction? Transaction { get; set; } /// /// Creates a scope with the specified options. @@ -149,6 +144,9 @@ internal Scope() { } + public Transaction CreateTransaction(string operation) => + Transaction = new Transaction(operation); + /// /// Clones the current . /// diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index c003adf8a1..193974fb7c 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -307,5 +307,9 @@ public static void CaptureUserFeedback(UserFeedback userFeedback) [DebuggerStepThrough] public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null) => _hub.CaptureUserFeedback(new UserFeedback(eventId, email, comments, name)); + + [DebuggerStepThrough] + public static Transaction GetTransaction(string operation) + => _hub.GetTransaction(operation); } } From 98e567d32b043f09ad3a7e915bbb5a4abf10879f Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 18:31:28 +0200 Subject: [PATCH 08/54] wip --- src/Sentry.AspNetCore/ScopeExtensions.cs | 7 ------- src/Sentry.AspNetCore/SentryMiddleware.cs | 20 ++++++++++++++++++-- src/Sentry/Extensibility/DisabledHub.cs | 2 -- src/Sentry/Extensibility/HubAdapter.cs | 4 ---- src/Sentry/ISentryScopeManager.cs | 3 --- src/Sentry/Internal/Hub.cs | 5 ----- src/Sentry/Internal/SentryScopeManager.cs | 6 ------ src/Sentry/Protocol/Span.cs | 4 ++-- src/Sentry/Protocol/Transaction.cs | 20 ++++++++++++-------- src/Sentry/SentrySdk.cs | 4 ---- 10 files changed, 32 insertions(+), 43 deletions(-) diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index efef93130f..d773e17f7a 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -86,13 +86,6 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC { scope.SetTag("route.area", area); } - - if (scope.Transaction is {} transaction) - { - transaction.Name = area == null - ? $"{controller}.{action}" - : $"{area}.{controller}.{action}"; - } } } catch(Exception e) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index becf40e593..54a89f5de2 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Runtime.ExceptionServices; using System.Threading.Tasks; +using System.Transactions; using Microsoft.AspNetCore.Diagnostics; #if NETSTANDARD2_0 using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; @@ -9,11 +10,13 @@ using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IWebHostEnvironment; #endif using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Sentry.Extensibility; using Sentry.Protocol; using Sentry.Reflection; +using Transaction = Sentry.Protocol.Transaction; namespace Sentry.AspNetCore { @@ -94,6 +97,19 @@ public async Task InvokeAsync(HttpContext context) }); } + var routeData = context.GetRouteData(); + var controller = routeData.Values["controller"]?.ToString(); + var action = routeData.Values["action"]?.ToString(); + var area = routeData.Values["area"]?.ToString(); + + // TODO: What if it's not using controllers (i.e. endpoints)? + + var transactionName = area == null + ? $"{controller}.{action}" + : $"{area}.{controller}.{action}"; + + var transaction = new Transaction(transactionName, "http.server"); + hub.ConfigureScope(scope => { // At the point lots of stuff from the request are not yet filled @@ -104,12 +120,12 @@ public async Task InvokeAsync(HttpContext context) // event creation will be sent to Sentry scope.OnEvaluating += (_, __) => PopulateScope(context, scope); + scope.Transaction = transaction; }); - var transaction = hub.GetTransaction("http.server"); - try { + transaction.StartTimestamp = DateTimeOffset.Now; await _next(context).ConfigureAwait(false); // When an exception was handled by other component (i.e: UseExceptionHandler feature). diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index d477b26307..80f93cb4a0 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -52,8 +52,6 @@ public void WithScope(Action scopeCallback) { } - public Transaction GetTransaction(string operation) => new Transaction(operation); - /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 2cbf681853..f0d77482f6 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -71,10 +71,6 @@ public IDisposable PushScope(TState state) public void WithScope(Action scopeCallback) => SentrySdk.WithScope(scopeCallback); - [DebuggerStepThrough] - public Transaction GetTransaction(string operation) - => SentrySdk.GetTransaction(operation); - /// /// Forwards the call to . /// diff --git a/src/Sentry/ISentryScopeManager.cs b/src/Sentry/ISentryScopeManager.cs index 4cba771403..e4f866c761 100644 --- a/src/Sentry/ISentryScopeManager.cs +++ b/src/Sentry/ISentryScopeManager.cs @@ -1,6 +1,5 @@ using System; using System.Threading.Tasks; -using Sentry.Protocol; namespace Sentry { @@ -56,7 +55,5 @@ public interface ISentryScopeManager /// /// The callback to run with the one time scope. void WithScope(Action scopeCallback); - - Transaction GetTransaction(string operation); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index c30f761645..3acb6e03dd 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -98,11 +98,6 @@ public void WithScope(Action scopeCallback) } } - public Transaction GetTransaction(string operation) - { - return ScopeManager.GetTransaction(operation); - } - public void BindClient(ISentryClient client) => ScopeManager.BindClient(client); public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) diff --git a/src/Sentry/Internal/SentryScopeManager.cs b/src/Sentry/Internal/SentryScopeManager.cs index 27be62d778..331011157b 100644 --- a/src/Sentry/Internal/SentryScopeManager.cs +++ b/src/Sentry/Internal/SentryScopeManager.cs @@ -97,12 +97,6 @@ public void WithScope(Action scopeCallback) } } - public Transaction GetTransaction(string operation) - { - var (scope, _) = GetCurrent(); - return scope.Transaction ?? scope.CreateTransaction(operation); - } - public void BindClient(ISentryClient? client) { _options.DiagnosticLogger?.LogDebug("Binding a new client to the current scope."); diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 88978b6097..cc8eabfb1f 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -91,8 +91,8 @@ public static Span FromJson(JsonElement json) var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; - var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; + var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); + var endTimestamp = json.GetProperty("timestamp").GetDateTimeOffset(); var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 88940e1b57..e86aaca6c0 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -9,7 +9,7 @@ namespace Sentry.Protocol // https://develop.sentry.dev/sdk/event-payloads/transaction public class Transaction : ISpan, IJsonSerializable { - public string? Name { get; set; } + public string Name { get; } public SentryId SpanId { get; } public SentryId? ParentSpanId { get; } public SentryId TraceId { get; set; } @@ -29,15 +29,20 @@ public class Transaction : ISpan, IJsonSerializable private List? _children; public IReadOnlyList Children => _children ??= new List(); - internal Transaction(SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") + internal Transaction( + string name, + SentryId? spanId = null, + SentryId? parentSpanId = null, + string operation = "unknown") { + Name = name; SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; Operation = operation; StartTimestamp = EndTimestamp = DateTimeOffset.Now; } - public Transaction(string operation) : this(null, null, operation) {} + public Transaction(string name, string operation) : this(name, null, null, operation) {} public ISpan StartChild(string operation) { @@ -116,12 +121,12 @@ public void WriteTo(Utf8JsonWriter writer) public static Transaction FromJson(JsonElement json) { - var name = json.GetPropertyOrNull("name")?.GetString(); + var name = json.GetProperty("name").GetStringOrThrow(); var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var startTimestamp = json.GetPropertyOrNull("start_timestamp")?.GetDateTimeOffset() ?? default; - var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset() ?? default; + var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); + var endTimestamp = json.GetProperty("timestamp").GetDateTimeOffset(); var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); @@ -130,9 +135,8 @@ public static Transaction FromJson(JsonElement json) var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.ToDictionary(); var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); - return new Transaction(spanId, parentSpanId, operation) + return new Transaction(name, spanId, parentSpanId, operation) { - Name = name, TraceId = traceId, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 193974fb7c..c003adf8a1 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -307,9 +307,5 @@ public static void CaptureUserFeedback(UserFeedback userFeedback) [DebuggerStepThrough] public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null) => _hub.CaptureUserFeedback(new UserFeedback(eventId, email, comments, name)); - - [DebuggerStepThrough] - public static Transaction GetTransaction(string operation) - => _hub.GetTransaction(operation); } } From e0eb73081c07c95a23f88fc6892291ad4a1d98b5 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 18:44:00 +0200 Subject: [PATCH 09/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 54a89f5de2..25590a5953 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -144,6 +144,7 @@ public async Task InvokeAsync(HttpContext context) finally { transaction.Finish(); + transaction.Status = SpanStatusMapper.FromStatusCode(context.Response.StatusCode); } void CaptureException(Exception e) From 748670c5b55d9aba31e6678de37ff878564d3be8 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 18:46:32 +0200 Subject: [PATCH 10/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 5 +++-- src/Sentry/Protocol/ISpan.cs | 2 +- src/Sentry/Protocol/Span.cs | 3 ++- src/Sentry/Protocol/Transaction.cs | 3 ++- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 25590a5953..af72492661 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -143,8 +143,9 @@ public async Task InvokeAsync(HttpContext context) } finally { - transaction.Finish(); - transaction.Status = SpanStatusMapper.FromStatusCode(context.Response.StatusCode); + transaction.Finish( + SpanStatusMapper.FromStatusCode(context.Response.StatusCode) + ); } void CaptureException(Exception e) diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 6968e2d57c..dd9cdece1d 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -29,6 +29,6 @@ public interface ISpan ISpan StartChild(string operation); - void Finish(); + void Finish(SpanStatus status = SpanStatus.Ok); } } diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index cc8eabfb1f..b58c6c3247 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -36,9 +36,10 @@ internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string ope public ISpan StartChild(string operation) => new Span(null, SpanId, operation); - public void Finish() + public void Finish(SpanStatus status = SpanStatus.Ok) { EndTimestamp = DateTimeOffset.Now; + Status = status; } public void WriteTo(Utf8JsonWriter writer) diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index e86aaca6c0..d102f413f5 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -52,9 +52,10 @@ public ISpan StartChild(string operation) return span; } - public void Finish() + public void Finish(SpanStatus status = SpanStatus.Ok) { EndTimestamp = DateTimeOffset.Now; + Status = status; } public void WriteTo(Utf8JsonWriter writer) From dfdec3199204b72df158646d3e5028df51ff0253 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 18:49:21 +0200 Subject: [PATCH 11/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 21 +++++++++++++++++- src/Sentry.AspNetCore/SpanStatusMapper.cs | 26 ----------------------- 2 files changed, 20 insertions(+), 27 deletions(-) delete mode 100644 src/Sentry.AspNetCore/SpanStatusMapper.cs diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index af72492661..2eefb3f1c9 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -144,7 +144,7 @@ public async Task InvokeAsync(HttpContext context) finally { transaction.Finish( - SpanStatusMapper.FromStatusCode(context.Response.StatusCode) + GetSpanStatusFromCode(context.Response.StatusCode) ); } @@ -186,5 +186,24 @@ internal void PopulateScope(HttpContext context, Scope scope) scope.Populate(Activity.Current); } } + + private static SpanStatus GetSpanStatusFromCode(int statusCode) => statusCode switch + { + < 400 => SpanStatus.Ok, + 400 => SpanStatus.InvalidArgument, + 401 => SpanStatus.Unauthenticated, + 403 => SpanStatus.PermissionDenied, + 404 => SpanStatus.NotFound, + 409 => SpanStatus.AlreadyExists, + 429 => SpanStatus.ResourceExhausted, + 499 => SpanStatus.Cancelled, + < 500 => SpanStatus.InvalidArgument, + 500 => SpanStatus.InternalError, + 501 => SpanStatus.Unimplemented, + 503 => SpanStatus.Unavailable, + 504 => SpanStatus.DeadlineExceeded, + < 600 => SpanStatus.InternalError, + _ => SpanStatus.UnknownError + }; } } diff --git a/src/Sentry.AspNetCore/SpanStatusMapper.cs b/src/Sentry.AspNetCore/SpanStatusMapper.cs deleted file mode 100644 index 08bff889ea..0000000000 --- a/src/Sentry.AspNetCore/SpanStatusMapper.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Sentry.Protocol; - -namespace Sentry.AspNetCore -{ - internal static class SpanStatusMapper - { - public static SpanStatus FromStatusCode(int statusCode) => statusCode switch - { - < 400 => SpanStatus.Ok, - 400 => SpanStatus.InvalidArgument, - 401 => SpanStatus.Unauthenticated, - 403 => SpanStatus.PermissionDenied, - 404 => SpanStatus.NotFound, - 409 => SpanStatus.AlreadyExists, - 429 => SpanStatus.ResourceExhausted, - 499 => SpanStatus.Cancelled, - < 500 => SpanStatus.InvalidArgument, - 500 => SpanStatus.InternalError, - 501 => SpanStatus.Unimplemented, - 503 => SpanStatus.Unavailable, - 504 => SpanStatus.DeadlineExceeded, - < 600 => SpanStatus.InternalError, - _ => SpanStatus.UnknownError - }; - } -} From fa6405390fc204aafc3ec090c18fafab6a9ffca3 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 2 Dec 2020 19:20:45 +0200 Subject: [PATCH 12/54] wip --- src/Sentry/ISentryTraceSampler.cs | 2 +- src/Sentry/TraceSamplingContext.cs | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/Sentry/TraceSamplingContext.cs diff --git a/src/Sentry/ISentryTraceSampler.cs b/src/Sentry/ISentryTraceSampler.cs index 2e695f5312..95508bc18d 100644 --- a/src/Sentry/ISentryTraceSampler.cs +++ b/src/Sentry/ISentryTraceSampler.cs @@ -2,6 +2,6 @@ namespace Sentry { public interface ISentryTraceSampler { - double GetSampleRate(); + double GetSampleRate(TraceSamplingContext context); } } diff --git a/src/Sentry/TraceSamplingContext.cs b/src/Sentry/TraceSamplingContext.cs new file mode 100644 index 0000000000..52f95be0a7 --- /dev/null +++ b/src/Sentry/TraceSamplingContext.cs @@ -0,0 +1,17 @@ +using Sentry.Protocol; + +namespace Sentry +{ + public class TraceSamplingContext + { + public ISpan Span { get; } + + public ISpan? ParentSpan { get; } + + public TraceSamplingContext(ISpan span, ISpan? parentSpan = null) + { + Span = span; + ParentSpan = parentSpan; + } + } +} From 83383812a75c0f5689c5831ef9cfeff3691b7930 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Thu, 3 Dec 2020 20:57:30 +0200 Subject: [PATCH 13/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 1 + src/Sentry/Extensibility/HubAdapter.cs | 8 ++++++ src/Sentry/ISentryClient.cs | 2 ++ src/Sentry/Internal/Hub.cs | 12 +++++++++ src/Sentry/Protocol/Transaction.cs | 2 ++ src/Sentry/SentryClient.cs | 30 ++++++++++++++++++++--- src/Sentry/SentrySdk.cs | 4 +++ 7 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 2eefb3f1c9..1733d17b58 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -88,6 +88,7 @@ public async Task InvokeAsync(HttpContext context) { context.Request.EnableBuffering(); } + if (_options.FlushOnCompletedRequest) { context.Response.OnCompleted(async () => diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index f0d77482f6..215b4de78f 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -132,6 +132,14 @@ public SentryId CaptureException(Exception exception) public SentryId CaptureEvent(SentryEvent evt, Scope? scope) => SentrySdk.CaptureEvent(evt, scope); + /// + /// Forwards the call to . + /// + [DebuggerStepThrough] + [EditorBrowsable(EditorBrowsableState.Never)] + public void CaptureTransaction(Transaction transaction) + => SentrySdk.CaptureTransaction(transaction); + /// /// Forwards the call to /// diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index c979b22f76..cec5d283c3 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -28,6 +28,8 @@ public interface ISentryClient /// The user feedback to send to Sentry. void CaptureUserFeedback(UserFeedback userFeedback); + void CaptureTransaction(Transaction transaction); + /// /// Flushes events queued up. /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 3acb6e03dd..9089c42aed 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -129,6 +129,18 @@ public void CaptureUserFeedback(UserFeedback userFeedback) } } + public void CaptureTransaction(Transaction transaction) + { + try + { + _ownedClient.CaptureTransaction(transaction); + } + catch (Exception e) + { + _options.DiagnosticLogger?.LogError("Failure to capture transaction: {0}", e, transaction.SpanId); + } + } + public async Task FlushAsync(TimeSpan timeout) { try diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index d102f413f5..21eb2a7108 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -56,6 +56,8 @@ public void Finish(SpanStatus status = SpanStatus.Ok) { EndTimestamp = DateTimeOffset.Now; Status = status; + + SentrySdk.CaptureTransaction(this); } public void WriteTo(Utf8JsonWriter writer) diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 2cda0c6069..55c641a9cc 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -112,15 +112,39 @@ public void CaptureUserFeedback(UserFeedback userFeedback) _options.DiagnosticLogger?.LogWarning("User feedback dropped due to empty id."); return; } - else if (string.IsNullOrWhiteSpace(userFeedback.Email) || - string.IsNullOrWhiteSpace(userFeedback.Comments)) + + if (string.IsNullOrWhiteSpace(userFeedback.Email) || + string.IsNullOrWhiteSpace(userFeedback.Comments)) { //Ignore the userfeedback if a required field is null or empty. _options.DiagnosticLogger?.LogWarning("User feedback discarded due to one or more required fields missing."); return; } - _ = CaptureEnvelope(Envelope.FromUserFeedback(userFeedback)); + CaptureEnvelope(Envelope.FromUserFeedback(userFeedback)); + } + + public void CaptureTransaction(Transaction transaction) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(SentryClient)); + } + + if (transaction.SpanId.Equals(SentryId.Empty)) + { + _options.DiagnosticLogger?.LogWarning("Transaction dropped due to empty id."); + return; + } + + if (string.IsNullOrWhiteSpace(transaction.Name) || + string.IsNullOrWhiteSpace(transaction.Operation)) + { + _options.DiagnosticLogger?.LogWarning("Transaction discarded due to one or more required fields missing."); + return; + } + + CaptureEnvelope(Envelope.FromTransaction(transaction)); } /// diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index c003adf8a1..79ca819484 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -307,5 +307,9 @@ public static void CaptureUserFeedback(UserFeedback userFeedback) [DebuggerStepThrough] public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null) => _hub.CaptureUserFeedback(new UserFeedback(eventId, email, comments, name)); + + [DebuggerStepThrough] + public static void CaptureTransaction(Transaction transaction) + => _hub.CaptureTransaction(transaction); } } From 8fc5c2b6828a0bbc9101d39197f08004536a6488 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 4 Dec 2020 17:06:48 +0200 Subject: [PATCH 14/54] wip --- src/Sentry/Extensibility/DisabledHub.cs | 7 +++++++ src/Sentry/Extensibility/HubAdapter.cs | 4 ++++ src/Sentry/IHub.cs | 2 ++ src/Sentry/Internal/Hub.cs | 8 ++++++++ src/Sentry/Internal/SentryScopeManager.cs | 1 - src/Sentry/Protocol/Transaction.cs | 16 ++++++++++++++-- src/Sentry/SentrySdk.cs | 4 ++++ 7 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 80f93cb4a0..b1235553c3 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -52,6 +52,9 @@ public void WithScope(Action scopeCallback) { } + public Transaction CreateTransaction(string name, string operation) => + new Transaction(this, name, operation); + /// /// No-Op. /// @@ -64,6 +67,10 @@ public void BindClient(ISentryClient client) /// public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) => SentryId.Empty; + public void CaptureTransaction(Transaction transaction) + { + } + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 215b4de78f..50358f0d34 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -71,6 +71,10 @@ public IDisposable PushScope(TState state) public void WithScope(Action scopeCallback) => SentrySdk.WithScope(scopeCallback); + [DebuggerStepThrough] + public Transaction CreateTransaction(string name, string operation) + => SentrySdk.CreateTransaction(name, operation); + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 7929793bbd..46d543a57b 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -20,5 +20,7 @@ public interface IHub : /// Last event id recorded in the current scope. /// SentryId LastEventId { get; } + + Transaction CreateTransaction(string name, string operation); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 9089c42aed..68ec4134ae 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -100,6 +100,14 @@ public void WithScope(Action scopeCallback) public void BindClient(ISentryClient client) => ScopeManager.BindClient(client); + public Transaction CreateTransaction(string name, string operation) + { + var trans = new Transaction(this, name, operation); + ConfigureScope(scope => scope.Transaction = trans); + + return trans; + } + public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) { try diff --git a/src/Sentry/Internal/SentryScopeManager.cs b/src/Sentry/Internal/SentryScopeManager.cs index 331011157b..1d0cce1443 100644 --- a/src/Sentry/Internal/SentryScopeManager.cs +++ b/src/Sentry/Internal/SentryScopeManager.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Sentry.Extensibility; -using Sentry.Protocol; namespace Sentry.Internal { diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 21eb2a7108..d4d55dd1f1 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -9,6 +9,8 @@ namespace Sentry.Protocol // https://develop.sentry.dev/sdk/event-payloads/transaction public class Transaction : ISpan, IJsonSerializable { + private readonly IHub _hub; + public string Name { get; } public SentryId SpanId { get; } public SentryId? ParentSpanId { get; } @@ -30,11 +32,13 @@ public class Transaction : ISpan, IJsonSerializable public IReadOnlyList Children => _children ??= new List(); internal Transaction( + IHub hub, string name, SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") { + _hub = hub; Name = name; SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; @@ -42,7 +46,15 @@ internal Transaction( StartTimestamp = EndTimestamp = DateTimeOffset.Now; } - public Transaction(string name, string operation) : this(name, null, null, operation) {} + public Transaction(IHub hub, string name, string operation) + : this( + hub, + name, + null, + null, + operation) + { + } public ISpan StartChild(string operation) { @@ -57,7 +69,7 @@ public void Finish(SpanStatus status = SpanStatus.Ok) EndTimestamp = DateTimeOffset.Now; Status = status; - SentrySdk.CaptureTransaction(this); + _hub.CaptureTransaction(this); } public void WriteTo(Utf8JsonWriter writer) diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 79ca819484..6b5b020ea5 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -311,5 +311,9 @@ public static void CaptureUserFeedback(SentryId eventId, string email, string co [DebuggerStepThrough] public static void CaptureTransaction(Transaction transaction) => _hub.CaptureTransaction(transaction); + + [DebuggerStepThrough] + public static Transaction CreateTransaction(string name, string operation) + => _hub.CreateTransaction(name, operation); } } From 6a0587167788fe6ad3011b55c6b6dafc0dea4542 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 4 Dec 2020 17:07:12 +0200 Subject: [PATCH 15/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 1733d17b58..03d6345645 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -109,7 +109,7 @@ public async Task InvokeAsync(HttpContext context) ? $"{controller}.{action}" : $"{area}.{controller}.{action}"; - var transaction = new Transaction(transactionName, "http.server"); + var transaction = hub.CreateTransaction(transactionName, "http.server"); hub.ConfigureScope(scope => { From 6c0b928022be950340faa2d448ac60929113a4bd Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 4 Dec 2020 17:15:33 +0200 Subject: [PATCH 16/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 03d6345645..30792c12e9 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -121,7 +121,6 @@ public async Task InvokeAsync(HttpContext context) // event creation will be sent to Sentry scope.OnEvaluating += (_, __) => PopulateScope(context, scope); - scope.Transaction = transaction; }); try From 5a47bd9904a8f3dfce5ae9df589ba8a1b5f4a1cf Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 4 Dec 2020 19:34:49 +0200 Subject: [PATCH 17/54] wip --- src/Sentry/Extensibility/DisabledHub.cs | 8 +++++ src/Sentry/Extensibility/HubAdapter.cs | 4 +++ src/Sentry/IHub.cs | 2 ++ src/Sentry/Internal/Hub.cs | 6 ++++ src/Sentry/Protocol/SentryId.cs | 2 ++ src/Sentry/Protocol/SentryTraceHeader.cs | 40 ++++++++++++++++++++++++ src/Sentry/Protocol/Transaction.cs | 6 ++++ src/Sentry/SentrySdk.cs | 4 +++ 8 files changed, 72 insertions(+) create mode 100644 src/Sentry/Protocol/SentryTraceHeader.cs diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index b1235553c3..60f735048e 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -52,9 +52,17 @@ public void WithScope(Action scopeCallback) { } + /// + /// Returns a dummy transaction. + /// public Transaction CreateTransaction(string name, string operation) => new Transaction(this, name, operation); + /// + /// Returns null. + /// + public SentryTraceHeader? GetTraceHeader() => null; + /// /// No-Op. /// diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 50358f0d34..740963652e 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -75,6 +75,10 @@ public void WithScope(Action scopeCallback) public Transaction CreateTransaction(string name, string operation) => SentrySdk.CreateTransaction(name, operation); + [DebuggerStepThrough] + public SentryTraceHeader? GetTraceHeader() + => SentrySdk.GetTraceHeader(); + /// /// Forwards the call to . /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 46d543a57b..54fd20a446 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -22,5 +22,7 @@ public interface IHub : SentryId LastEventId { get; } Transaction CreateTransaction(string name, string operation); + + SentryTraceHeader? GetTraceHeader(); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 68ec4134ae..daae22c2cb 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -108,6 +108,12 @@ public Transaction CreateTransaction(string name, string operation) return trans; } + public SentryTraceHeader? GetTraceHeader() + { + var (currentScope,_) = ScopeManager.GetCurrent(); + return currentScope.Transaction?.GetTraceHeader(); + } + public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) { try diff --git a/src/Sentry/Protocol/SentryId.cs b/src/Sentry/Protocol/SentryId.cs index c9547b4ecf..9c52caaa67 100644 --- a/src/Sentry/Protocol/SentryId.cs +++ b/src/Sentry/Protocol/SentryId.cs @@ -47,6 +47,8 @@ namespace Sentry.Protocol /// public void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(ToString()); + public static SentryId Parse(string value) => new SentryId(Guid.Parse(value)); + /// /// Parses from JSON. /// diff --git a/src/Sentry/Protocol/SentryTraceHeader.cs b/src/Sentry/Protocol/SentryTraceHeader.cs new file mode 100644 index 0000000000..96add964bb --- /dev/null +++ b/src/Sentry/Protocol/SentryTraceHeader.cs @@ -0,0 +1,40 @@ +using System; + +namespace Sentry.Protocol +{ + public class SentryTraceHeader + { + private readonly SentryId _traceId; + private readonly SentryId _spanId; + private readonly bool? _isSampled; + + public SentryTraceHeader(SentryId traceId, SentryId spanId, bool? isSampled) + { + _traceId = traceId; + _spanId = spanId; + _isSampled = isSampled; + } + + public override string ToString() => _isSampled is {} isSampled + ? $"{_traceId}-{_spanId}-{(isSampled ? 1 : 0)}" + : $"{_traceId}-{_spanId}"; + + public static SentryTraceHeader Parse(string value) + { + var components = value.Split('-', StringSplitOptions.RemoveEmptyEntries); + if (components.Length < 2) + { + throw new FormatException($"Invalid Sentry trace header: {value}."); + } + + var traceId = SentryId.Parse(components[0]); + var spanId = SentryId.Parse(components[1]); + + var isSampled = components.Length >= 3 + ? string.Equals(components[2], "1", StringComparison.OrdinalIgnoreCase) + : (bool?)null; + + return new SentryTraceHeader(traceId, spanId, isSampled); + } + } +} diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index d4d55dd1f1..1eb27e3af1 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -72,6 +72,12 @@ public void Finish(SpanStatus status = SpanStatus.Ok) _hub.CaptureTransaction(this); } + public SentryTraceHeader GetTraceHeader() => new SentryTraceHeader( + TraceId, + SpanId, + IsSampled + ); + public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index 6b5b020ea5..cdb9e88302 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -315,5 +315,9 @@ public static void CaptureTransaction(Transaction transaction) [DebuggerStepThrough] public static Transaction CreateTransaction(string name, string operation) => _hub.CreateTransaction(name, operation); + + [DebuggerStepThrough] + public static SentryTraceHeader? GetTraceHeader() + => _hub.GetTraceHeader(); } } From e67597e5bb0bd891bb87434d6fe009a6c273dbd0 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 7 Dec 2020 19:57:19 +0200 Subject: [PATCH 18/54] wip --- src/Sentry/SentryClient.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 55c641a9cc..223a7642f8 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -108,7 +108,7 @@ public void CaptureUserFeedback(UserFeedback userFeedback) if (userFeedback.EventId.Equals(SentryId.Empty)) { - //Ignore the userfeedback if EventId is empty + // Ignore the user feedback if EventId is empty _options.DiagnosticLogger?.LogWarning("User feedback dropped due to empty id."); return; } @@ -116,7 +116,7 @@ public void CaptureUserFeedback(UserFeedback userFeedback) if (string.IsNullOrWhiteSpace(userFeedback.Email) || string.IsNullOrWhiteSpace(userFeedback.Comments)) { - //Ignore the userfeedback if a required field is null or empty. + // Ignore the user feedback if a required field is null or empty. _options.DiagnosticLogger?.LogWarning("User feedback discarded due to one or more required fields missing."); return; } @@ -165,6 +165,7 @@ private SentryId DoSendEvent(SentryEvent @event, Scope? scope) return SentryId.Empty; } } + if (@event.Exception != null && _options.ExceptionFilters?.Length > 0) { if (_options.ExceptionFilters.Any(f => f.Filter(@event.Exception))) @@ -174,6 +175,7 @@ private SentryId DoSendEvent(SentryEvent @event, Scope? scope) return SentryId.Empty; } } + scope ??= new Scope(_options); _options.DiagnosticLogger?.LogInfo("Capturing event."); @@ -182,6 +184,12 @@ private SentryId DoSendEvent(SentryEvent @event, Scope? scope) scope.Evaluate(); scope.Apply(@event); + // Set transaction's span ID + if (scope.Transaction is {} transaction) + { + @event.SetTag("span_id", transaction.SpanId.ToString()); + } + if (scope.Level != null) { // Level on scope takes precedence over the one on event From 52ec0a59bcd3bc41cecc8c70819f9a44f89cbdec Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 7 Dec 2020 22:22:45 +0200 Subject: [PATCH 19/54] wip --- src/Sentry/ISentryClient.cs | 4 ++ src/Sentry/Internal/Hub.cs | 3 +- src/Sentry/Internal/Polyfills.cs | 13 ++++++- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 39 ++++++++----------- src/Sentry/Protocol/SentryTraceHeader.cs | 10 +++++ src/Sentry/Protocol/Span.cs | 9 +++-- src/Sentry/Protocol/Transaction.cs | 16 +++++--- src/Sentry/Scope.cs | 3 -- src/Sentry/SentryClient.cs | 14 +------ 9 files changed, 61 insertions(+), 50 deletions(-) diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index cec5d283c3..47e412e479 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -28,6 +28,10 @@ public interface ISentryClient /// The user feedback to send to Sentry. void CaptureUserFeedback(UserFeedback userFeedback); + /// + /// Captures a transaction. + /// + /// The transaction. void CaptureTransaction(Transaction transaction); /// diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index daae22c2cb..7c537d91ab 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Sentry.Extensibility; @@ -110,7 +111,7 @@ public Transaction CreateTransaction(string name, string operation) public SentryTraceHeader? GetTraceHeader() { - var (currentScope,_) = ScopeManager.GetCurrent(); + var (currentScope, _) = ScopeManager.GetCurrent(); return currentScope.Transaction?.GetTraceHeader(); } diff --git a/src/Sentry/Internal/Polyfills.cs b/src/Sentry/Internal/Polyfills.cs index f4600119f1..3da977007c 100644 --- a/src/Sentry/Internal/Polyfills.cs +++ b/src/Sentry/Internal/Polyfills.cs @@ -3,9 +3,16 @@ // Polyfills to bridge the missing APIs in older versions of the framework/standard. // In some cases, these just proxy calls to existing methods but also provide a signature that matches .netstd2.1 -using System.Linq; - #if NET461 || NETSTANDARD2_0 +namespace System +{ + internal static class Extensions + { + public static string[] Split(this string str, char c, StringSplitOptions options = StringSplitOptions.None) => + str.Split(new[] {c}, options); + } +} + namespace System.IO { using Threading; @@ -26,6 +33,8 @@ public static Task WriteAsync(this Stream stream, byte[] buffer, CancellationTok namespace System.Collections.Generic { + using Linq; + internal static class Extensions { public static void Deconstruct( diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index bb8d3a19e6..9c3dc1bda1 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -18,7 +18,6 @@ internal sealed class EnvelopeItem : ISerializable, IDisposable private const string TypeKey = "type"; private const string TypeValueEvent = "event"; private const string TypeValueUserReport = "user_report"; - private const string TypeValueTransaction = "transaction"; private const string LengthKey = "length"; private const string FileNameKey = "file_name"; @@ -184,7 +183,8 @@ public static EnvelopeItem FromTransaction(Transaction transaction) { var header = new Dictionary(StringComparer.Ordinal) { - [TypeKey] = TypeValueTransaction + // Transaction is an event + [TypeKey] = TypeValueEvent }; return new EnvelopeItem(header, new JsonSerializable(transaction)); @@ -227,16 +227,23 @@ private static async Task DeserializePayloadAsync( var payloadType = header.GetValueOrDefault(TypeKey) as string; - // Event + // Event (or transaction) if (string.Equals(payloadType, TypeValueEvent, StringComparison.OrdinalIgnoreCase)) { var bufferLength = (int)(payloadLength ?? stream.Length); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); - using var jsonDocument = JsonDocument.Parse(buffer); + var json = Json.Parse(buffer); + + var innerType = json.GetPropertyOrNull("type")?.GetString(); - return new JsonSerializable( - SentryEvent.FromJson(jsonDocument.RootElement.Clone()) - ); + if (string.Equals(innerType, "transaction", StringComparison.OrdinalIgnoreCase)) + { + return new JsonSerializable(Transaction.FromJson(json)); + } + else + { + return new JsonSerializable(SentryEvent.FromJson(json)); + } } // User report @@ -244,23 +251,9 @@ private static async Task DeserializePayloadAsync( { var bufferLength = (int)(payloadLength ?? stream.Length); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); - using var jsonDocument = JsonDocument.Parse(buffer); - - return new JsonSerializable( - UserFeedback.FromJson(jsonDocument.RootElement.Clone()) - ); - } - - // Transaction - if (string.Equals(payloadType, TypeValueTransaction, StringComparison.OrdinalIgnoreCase)) - { - var bufferLength = (int)(payloadLength ?? stream.Length); - var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); - using var jsonDocument = JsonDocument.Parse(buffer); + var json = Json.Parse(buffer); - return new JsonSerializable( - Transaction.FromJson(jsonDocument.RootElement.Clone()) - ); + return new JsonSerializable(UserFeedback.FromJson(json)); } // Arbitrary payload diff --git a/src/Sentry/Protocol/SentryTraceHeader.cs b/src/Sentry/Protocol/SentryTraceHeader.cs index 96add964bb..9051e9c1d2 100644 --- a/src/Sentry/Protocol/SentryTraceHeader.cs +++ b/src/Sentry/Protocol/SentryTraceHeader.cs @@ -2,12 +2,18 @@ namespace Sentry.Protocol { + /// + /// Sentry trace header. + /// public class SentryTraceHeader { private readonly SentryId _traceId; private readonly SentryId _spanId; private readonly bool? _isSampled; + /// + /// Initializes an instance of . + /// public SentryTraceHeader(SentryId traceId, SentryId spanId, bool? isSampled) { _traceId = traceId; @@ -15,10 +21,14 @@ public SentryTraceHeader(SentryId traceId, SentryId spanId, bool? isSampled) _isSampled = isSampled; } + /// public override string ToString() => _isSampled is {} isSampled ? $"{_traceId}-{_spanId}-{(isSampled ? 1 : 0)}" : $"{_traceId}-{_spanId}"; + /// + /// Parses from string. + /// public static SentryTraceHeader Parse(string value) { var components = value.Split('-', StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index b58c6c3247..32d98585b4 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -30,8 +30,9 @@ internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string ope { SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; - Operation = operation; + TraceId = SentryId.Create(); StartTimestamp = EndTimestamp = DateTimeOffset.Now; + Operation = operation; } public ISpan StartChild(string operation) => new Span(null, SpanId, operation); @@ -46,14 +47,14 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("span_id", SpanId); + writer.WriteSerializable("span_id", SpanId); if (ParentSpanId is {} parentSpanId) { - writer.WriteString("parent_span_id", parentSpanId); + writer.WriteSerializable("parent_span_id", parentSpanId); } - writer.WriteString("trace_id", TraceId); + writer.WriteSerializable("trace_id", TraceId); writer.WriteString("start_timestamp", StartTimestamp); writer.WriteString("timestamp", EndTimestamp); diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 1eb27e3af1..86a479abbc 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text.Json; +using Sentry.Extensibility; using Sentry.Internal.Extensions; namespace Sentry.Protocol @@ -42,8 +43,9 @@ internal Transaction( Name = name; SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; - Operation = operation; + TraceId = SentryId.Create(); StartTimestamp = EndTimestamp = DateTimeOffset.Now; + Operation = operation; } public Transaction(IHub hub, string name, string operation) @@ -82,19 +84,21 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); + writer.WriteString("type", "transaction"); + if (!string.IsNullOrWhiteSpace(Name)) { writer.WriteString("name", Name); } - writer.WriteString("span_id", SpanId); + writer.WriteSerializable("span_id", SpanId); if (ParentSpanId is {} parentSpanId) { - writer.WriteString("parent_span_id", parentSpanId); + writer.WriteSerializable("parent_span_id", parentSpanId); } - writer.WriteString("trace_id", TraceId); + writer.WriteSerializable("trace_id", TraceId); writer.WriteString("start_timestamp", StartTimestamp); writer.WriteString("timestamp", EndTimestamp); @@ -142,6 +146,8 @@ public void WriteTo(Utf8JsonWriter writer) public static Transaction FromJson(JsonElement json) { + var hub = HubAdapter.Instance; + var name = json.GetProperty("name").GetStringOrThrow(); var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); @@ -156,7 +162,7 @@ public static Transaction FromJson(JsonElement json) var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.ToDictionary(); var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); - return new Transaction(name, spanId, parentSpanId, operation) + return new Transaction(hub, name, spanId, parentSpanId, operation) { TraceId = traceId, StartTimestamp = startTimestamp, diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 11e9f748e6..19fd48e105 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -144,9 +144,6 @@ internal Scope() { } - public Transaction CreateTransaction(string operation) => - Transaction = new Transaction(operation); - /// /// Clones the current . /// diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 223a7642f8..645cf36f8a 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -63,14 +63,6 @@ internal SentryClient( } } - /// - /// Queues the event to be sent to Sentry. - /// - /// - /// An optional scope, if provided, will be applied to the event. - /// - /// The event to send to Sentry. - /// The optional scope to augment the event with. /// public SentryId CaptureEvent(SentryEvent? @event, Scope? scope = null) { @@ -95,10 +87,7 @@ public SentryId CaptureEvent(SentryEvent? @event, Scope? scope = null) } } - /// - /// Captures a user feedback. - /// - /// The user feedback to send to Sentry. + /// public void CaptureUserFeedback(UserFeedback userFeedback) { if (_disposed) @@ -124,6 +113,7 @@ public void CaptureUserFeedback(UserFeedback userFeedback) CaptureEnvelope(Envelope.FromUserFeedback(userFeedback)); } + /// public void CaptureTransaction(Transaction transaction) { if (_disposed) From 15d7001ff936ab7c666940c6b4a3412eaa5a0564 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 00:23:49 +0200 Subject: [PATCH 20/54] wip --- .../Sentry.Samples.AspNetCore.Basic.csproj | 2 +- src/Sentry.AspNetCore/SentryMiddleware.cs | 1 - src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 24 ++++++++++--------- .../ScopeExtensionsTests.cs | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj b/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj index c4100bab04..c24d1832df 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj +++ b/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj @@ -1,7 +1,7 @@  - netcoreapp2.1 + net5.0 diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 30792c12e9..90673dd001 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -16,7 +16,6 @@ using Sentry.Extensibility; using Sentry.Protocol; using Sentry.Reflection; -using Transaction = Sentry.Protocol.Transaction; namespace Sentry.AspNetCore { diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 9c3dc1bda1..8b3d7904fe 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -18,6 +18,7 @@ internal sealed class EnvelopeItem : ISerializable, IDisposable private const string TypeKey = "type"; private const string TypeValueEvent = "event"; private const string TypeValueUserReport = "user_report"; + private const string TypeValueTransaction = "transaction"; private const string LengthKey = "length"; private const string FileNameKey = "file_name"; @@ -227,23 +228,14 @@ private static async Task DeserializePayloadAsync( var payloadType = header.GetValueOrDefault(TypeKey) as string; - // Event (or transaction) + // Event if (string.Equals(payloadType, TypeValueEvent, StringComparison.OrdinalIgnoreCase)) { var bufferLength = (int)(payloadLength ?? stream.Length); var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); var json = Json.Parse(buffer); - var innerType = json.GetPropertyOrNull("type")?.GetString(); - - if (string.Equals(innerType, "transaction", StringComparison.OrdinalIgnoreCase)) - { - return new JsonSerializable(Transaction.FromJson(json)); - } - else - { - return new JsonSerializable(SentryEvent.FromJson(json)); - } + return new JsonSerializable(SentryEvent.FromJson(json)); } // User report @@ -256,6 +248,16 @@ private static async Task DeserializePayloadAsync( return new JsonSerializable(UserFeedback.FromJson(json)); } + // Transaction + if (string.Equals(payloadType, TypeValueTransaction, StringComparison.OrdinalIgnoreCase)) + { + var bufferLength = (int)(payloadLength ?? stream.Length); + var buffer = await stream.ReadByteChunkAsync(bufferLength, cancellationToken).ConfigureAwait(false); + var json = Json.Parse(buffer); + + return new JsonSerializable(Transaction.FromJson(json)); + } + // Arbitrary payload var payloadStream = new PartialStream(stream, stream.Position, payloadLength); diff --git a/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs b/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs index b2898a8ac9..5066956ec2 100644 --- a/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs +++ b/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs @@ -280,7 +280,7 @@ public void Populate_RouteData_SetToScope() _sut.Populate(_httpContext, SentryAspNetCoreOptions); - Assert.Equal($"{controller}.{action}", _sut.Transaction); + Assert.Equal($"{controller}.{action}", _sut.Transaction?.Name); } public static IEnumerable InvalidRequestBodies() From 3a705f03703474e22a340c0342877a2b34fa841f Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 18:49:32 +0200 Subject: [PATCH 21/54] wip --- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 8b3d7904fe..2a946fbabd 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -185,7 +185,7 @@ public static EnvelopeItem FromTransaction(Transaction transaction) var header = new Dictionary(StringComparer.Ordinal) { // Transaction is an event - [TypeKey] = TypeValueEvent + [TypeKey] = TypeValueTransaction }; return new EnvelopeItem(header, new JsonSerializable(transaction)); From c4f937101342c07ac5a370654e84e2e3e5bd126e Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 18:52:03 +0200 Subject: [PATCH 22/54] wip --- src/Sentry/Protocol/Envelopes/EnvelopeItem.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs index 2a946fbabd..b2c1d42af5 100644 --- a/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs +++ b/src/Sentry/Protocol/Envelopes/EnvelopeItem.cs @@ -184,7 +184,6 @@ public static EnvelopeItem FromTransaction(Transaction transaction) { var header = new Dictionary(StringComparer.Ordinal) { - // Transaction is an event [TypeKey] = TypeValueTransaction }; From 00ef2b563ee23e46f2a6fa562ab6266e1c0187fa Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 19:09:52 +0200 Subject: [PATCH 23/54] wip --- src/Sentry.AspNetCore/SentryMiddleware.cs | 1 - src/Sentry/Extensibility/DisabledHub.cs | 2 +- src/Sentry/Extensibility/HubAdapter.cs | 2 +- src/Sentry/IHub.cs | 2 +- src/Sentry/Internal/Hub.cs | 2 +- src/Sentry/Protocol/ISpan.cs | 8 ++++---- src/Sentry/Protocol/Span.cs | 16 ++++++++++------ src/Sentry/Protocol/Transaction.cs | 16 ++++++++++------ src/Sentry/SentrySdk.cs | 2 +- 9 files changed, 29 insertions(+), 22 deletions(-) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 90673dd001..43b27ce5ad 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Runtime.ExceptionServices; using System.Threading.Tasks; -using System.Transactions; using Microsoft.AspNetCore.Diagnostics; #if NETSTANDARD2_0 using IHostingEnvironment = Microsoft.AspNetCore.Hosting.IHostingEnvironment; diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 60f735048e..987681395a 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -61,7 +61,7 @@ public Transaction CreateTransaction(string name, string operation) => /// /// Returns null. /// - public SentryTraceHeader? GetTraceHeader() => null; + public SentryTraceHeader? GetSentryTrace() => null; /// /// No-Op. diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 740963652e..5ba399c93c 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -76,7 +76,7 @@ public Transaction CreateTransaction(string name, string operation) => SentrySdk.CreateTransaction(name, operation); [DebuggerStepThrough] - public SentryTraceHeader? GetTraceHeader() + public SentryTraceHeader? GetSentryTrace() => SentrySdk.GetTraceHeader(); /// diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 54fd20a446..8ce9bb69ba 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -23,6 +23,6 @@ public interface IHub : Transaction CreateTransaction(string name, string operation); - SentryTraceHeader? GetTraceHeader(); + SentryTraceHeader? GetSentryTrace(); } } diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 7c537d91ab..1f8a50b176 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -109,7 +109,7 @@ public Transaction CreateTransaction(string name, string operation) return trans; } - public SentryTraceHeader? GetTraceHeader() + public SentryTraceHeader? GetSentryTrace() { var (currentScope, _) = ScopeManager.GetCurrent(); return currentScope.Transaction?.GetTraceHeader(); diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index dd9cdece1d..77a3231326 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -9,17 +9,17 @@ public interface ISpan SentryId? ParentSpanId { get; } - SentryId TraceId { get; set; } + SentryId TraceId { get; } - DateTimeOffset StartTimestamp { get; set; } + DateTimeOffset StartTimestamp { get; } - DateTimeOffset EndTimestamp { get; set; } + DateTimeOffset? EndTimestamp { get; } string Operation { get; } string? Description { get; set; } - SpanStatus? Status { get; set; } + SpanStatus? Status { get; } bool IsSampled { get; set; } diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 32d98585b4..9c63c1d242 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -12,12 +12,12 @@ public class Span : ISpan, IJsonSerializable { public SentryId SpanId { get; } public SentryId? ParentSpanId { get; } - public SentryId TraceId { get; set; } - public DateTimeOffset StartTimestamp { get; set; } - public DateTimeOffset EndTimestamp { get; set; } + public SentryId TraceId { get; private set; } + public DateTimeOffset StartTimestamp { get; private set; } + public DateTimeOffset? EndTimestamp { get; private set; } public string Operation { get; } public string? Description { get; set; } - public SpanStatus? Status { get; set; } + public SpanStatus? Status { get; private set; } public bool IsSampled { get; set; } private ConcurrentDictionary? _tags; @@ -31,7 +31,7 @@ internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string ope SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; TraceId = SentryId.Create(); - StartTimestamp = EndTimestamp = DateTimeOffset.Now; + StartTimestamp = DateTimeOffset.Now; Operation = operation; } @@ -56,7 +56,11 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteSerializable("trace_id", TraceId); writer.WriteString("start_timestamp", StartTimestamp); - writer.WriteString("timestamp", EndTimestamp); + + if (EndTimestamp is {} endTimestamp) + { + writer.WriteString("timestamp", endTimestamp); + } if (!string.IsNullOrWhiteSpace(Operation)) { diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 86a479abbc..1a9da430e8 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -15,12 +15,12 @@ public class Transaction : ISpan, IJsonSerializable public string Name { get; } public SentryId SpanId { get; } public SentryId? ParentSpanId { get; } - public SentryId TraceId { get; set; } - public DateTimeOffset StartTimestamp { get; set; } - public DateTimeOffset EndTimestamp { get; set; } + public SentryId TraceId { get; private set; } + public DateTimeOffset StartTimestamp { get; private set; } + public DateTimeOffset? EndTimestamp { get; private set; } public string Operation { get; } public string? Description { get; set; } - public SpanStatus? Status { get; set; } + public SpanStatus? Status { get; private set; } public bool IsSampled { get; set; } private Dictionary? _tags; @@ -44,7 +44,7 @@ internal Transaction( SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; TraceId = SentryId.Create(); - StartTimestamp = EndTimestamp = DateTimeOffset.Now; + StartTimestamp = DateTimeOffset.Now; Operation = operation; } @@ -100,7 +100,11 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteSerializable("trace_id", TraceId); writer.WriteString("start_timestamp", StartTimestamp); - writer.WriteString("timestamp", EndTimestamp); + + if (EndTimestamp is {} endTimestamp) + { + writer.WriteString("timestamp", endTimestamp); + } if (!string.IsNullOrWhiteSpace(Operation)) { diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index cdb9e88302..dd18232356 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -318,6 +318,6 @@ public static Transaction CreateTransaction(string name, string operation) [DebuggerStepThrough] public static SentryTraceHeader? GetTraceHeader() - => _hub.GetTraceHeader(); + => _hub.GetSentryTrace(); } } From b0487cf2c999b733690e7e51ad4f2e45fee8f6cb Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 19:22:39 +0200 Subject: [PATCH 24/54] wip --- .../Program.cs | 2 +- src/Sentry.AspNetCore/ScopeExtensions.cs | 4 ++++ src/Sentry.AspNetCore/SentryMiddleware.cs | 5 ++--- src/Sentry/Protocol/IScope.cs | 11 +++++++++++ src/Sentry/Protocol/SentryEvent.cs | 18 +++++------------- src/Sentry/Scope.cs | 6 ++++++ test/Sentry.Tests/Protocol/BaseScopeTests.cs | 4 ++-- .../Protocol/Envelopes/EnvelopeTests.cs | 2 +- .../Protocol/ScopeExtensionsTests.cs | 14 +++++++------- test/Sentry.Tests/Protocol/SentryEventTests.cs | 2 +- 10 files changed, 40 insertions(+), 28 deletions(-) diff --git a/samples/Sentry.Samples.Console.Customized/Program.cs b/samples/Sentry.Samples.Console.Customized/Program.cs index ede8e48c1e..e0825237e4 100644 --- a/samples/Sentry.Samples.Console.Customized/Program.cs +++ b/samples/Sentry.Samples.Console.Customized/Program.cs @@ -139,7 +139,7 @@ await SentrySdk.ConfigureScopeAsync(async scope => SentrySdk.WithScope(s => { s.Level = SentryLevel.Fatal; - s.Transaction = "main"; + s.TransactionName = "main"; s.Environment = "SpecialEnvironment"; SentrySdk.CaptureMessage("Fatal message!"); diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index d773e17f7a..c42fcafa21 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -86,6 +86,10 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC { scope.SetTag("route.area", area); } + + scope.TransactionName = area == null + ? $"{controller}.{action}" + : $"{area}.{controller}.{action}"; } } catch(Exception e) diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index 43b27ce5ad..cb98949542 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -107,8 +107,6 @@ public async Task InvokeAsync(HttpContext context) ? $"{controller}.{action}" : $"{area}.{controller}.{action}"; - var transaction = hub.CreateTransaction(transactionName, "http.server"); - hub.ConfigureScope(scope => { // At the point lots of stuff from the request are not yet filled @@ -121,9 +119,10 @@ public async Task InvokeAsync(HttpContext context) scope.OnEvaluating += (_, __) => PopulateScope(context, scope); }); + var transaction = hub.CreateTransaction(transactionName, "http.server"); + try { - transaction.StartTimestamp = DateTimeOffset.Now; await _next(context).ConfigureAwait(false); // When an exception was handled by other component (i.e: UseExceptionHandler feature). diff --git a/src/Sentry/Protocol/IScope.cs b/src/Sentry/Protocol/IScope.cs index eb79defd4d..eba510b9c2 100644 --- a/src/Sentry/Protocol/IScope.cs +++ b/src/Sentry/Protocol/IScope.cs @@ -58,6 +58,17 @@ public interface IScope /// Requires Sentry 8.0 or higher. string? Environment { get; set; } + /// + /// The name of the transaction in which there was an event. + /// + /// + /// A transaction should only be defined when it can be well defined. + /// On a Web framework, for example, a transaction is the route template + /// rather than the actual request path. That is so GET /user/10 and /user/20 + /// (which have route template /user/{id}) are identified as the same transaction. + /// + string? TransactionName { get; set; } + /// /// SDK information. /// diff --git a/src/Sentry/Protocol/SentryEvent.cs b/src/Sentry/Protocol/SentryEvent.cs index 478f52669b..aaa0ff3710 100644 --- a/src/Sentry/Protocol/SentryEvent.cs +++ b/src/Sentry/Protocol/SentryEvent.cs @@ -111,16 +111,8 @@ public IEnumerable? SentryThreads /// public SentryLevel? Level { get; set; } - /// - /// The name of the transaction in which there was an event. - /// - /// - /// A transaction should only be defined when it can be well defined. - /// On a Web framework, for example, a transaction is the route template - /// rather than the actual request path. That is so GET /user/10 and /user/20 - /// (which have route template /user/{id}) are identified as the same transaction. - /// - public string? Transaction { get; set; } + /// + public string? TransactionName { get; set; } private Request? _request; /// @@ -273,9 +265,9 @@ public void WriteTo(Utf8JsonWriter writer) } // Transaction - if (!string.IsNullOrWhiteSpace(Transaction)) + if (!string.IsNullOrWhiteSpace(TransactionName)) { - writer.WriteString("transaction", Transaction); + writer.WriteString("transaction", TransactionName); } // Request @@ -398,7 +390,7 @@ public static SentryEvent FromJson(JsonElement json) SentryExceptionValues = exceptionValues, SentryThreadValues = threadValues, Level = level, - Transaction = transaction, + TransactionName = transaction, _request = request, _contexts = contexts, _user = user, diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 19fd48e105..3a51683f79 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -128,8 +128,14 @@ public User User /// public IReadOnlyDictionary Tags { get; } = new ConcurrentDictionary(); + /// + /// Transaction. + /// public Transaction? Transaction { get; set; } + /// + public string? TransactionName { get; set; } + /// /// Creates a scope with the specified options. /// diff --git a/test/Sentry.Tests/Protocol/BaseScopeTests.cs b/test/Sentry.Tests/Protocol/BaseScopeTests.cs index 5f17157194..75c618c8ba 100644 --- a/test/Sentry.Tests/Protocol/BaseScopeTests.cs +++ b/test/Sentry.Tests/Protocol/BaseScopeTests.cs @@ -77,8 +77,8 @@ public void Request_Settable() public void Transaction_Settable() { var expected = "Transaction"; - _sut.Transaction = expected; - Assert.Same(expected, _sut.Transaction); + _sut.TransactionName = expected; + Assert.Same(expected, _sut.TransactionName); } [Fact] diff --git a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs index e41c607e88..d423a36b5f 100644 --- a/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs +++ b/test/Sentry.Tests/Protocol/Envelopes/EnvelopeTests.cs @@ -372,7 +372,7 @@ public async Task Roundtrip_WithEvent_Success() SentryExceptions = new [] { new SentryException { Value = "exception_value" } }, SentryThreads = new[] { new SentryThread { Crashed = true } }, ServerName = "server_name", - Transaction = "transaction", + TransactionName = "transaction", }; @event.SetExtra("extra_key", "extra_value"); diff --git a/test/Sentry.Tests/Protocol/ScopeExtensionsTests.cs b/test/Sentry.Tests/Protocol/ScopeExtensionsTests.cs index 17fdb9c257..f0e66509e6 100644 --- a/test/Sentry.Tests/Protocol/ScopeExtensionsTests.cs +++ b/test/Sentry.Tests/Protocol/ScopeExtensionsTests.cs @@ -943,12 +943,12 @@ public void Apply_Environment_OnTarget_NotOverwritten() public void Apply_Transaction_Null() { var sut = _fixture.GetSut(); - sut.Transaction = null; + sut.TransactionName = null; var target = _fixture.GetSut(); sut.Apply(target); - Assert.Null(target.Transaction); + Assert.Null(target.TransactionName); } [Fact] @@ -957,11 +957,11 @@ public void Apply_Transaction_NotOnTarget_SetFromSource() const string expected = "transaction"; var sut = _fixture.GetSut(); - sut.Transaction = expected; + sut.TransactionName = expected; var target = _fixture.GetSut(); sut.Apply(target); - Assert.Equal(expected, target.Transaction); + Assert.Equal(expected, target.TransactionName); } [Fact] @@ -970,12 +970,12 @@ public void Apply_Transaction_OnTarget_NotOverwritten() const string expected = "transaction"; var sut = _fixture.GetSut(); var target = _fixture.GetSut(); - target.Transaction = expected; + target.TransactionName = expected; - sut.Transaction = "other"; + sut.TransactionName = "other"; sut.Apply(target); - Assert.Equal(expected, target.Transaction); + Assert.Equal(expected, target.TransactionName); } [Fact] diff --git a/test/Sentry.Tests/Protocol/SentryEventTests.cs b/test/Sentry.Tests/Protocol/SentryEventTests.cs index b1e447d2bd..df44c343c3 100644 --- a/test/Sentry.Tests/Protocol/SentryEventTests.cs +++ b/test/Sentry.Tests/Protocol/SentryEventTests.cs @@ -43,7 +43,7 @@ public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() SentryExceptions = new[] { new SentryException { Value = "exception_value"} }, SentryThreads = new[] { new SentryThread { Crashed = true } }, ServerName = "server_name", - Transaction = "transaction", + TransactionName = "transaction", }; sut.Sdk.AddPackage(new Package("name", "version")); From 0a713f38d0a058a427c7961fcbf1167e82dedffb Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 19:35:13 +0200 Subject: [PATCH 25/54] wip --- src/Sentry/Protocol/SentryId.cs | 21 +++++++++++++++------ src/Sentry/Protocol/Span.cs | 4 ++-- src/Sentry/Protocol/Transaction.cs | 4 ++-- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Sentry/Protocol/SentryId.cs b/src/Sentry/Protocol/SentryId.cs index 9c52caaa67..ae21452f6e 100644 --- a/src/Sentry/Protocol/SentryId.cs +++ b/src/Sentry/Protocol/SentryId.cs @@ -8,7 +8,7 @@ namespace Sentry.Protocol /// public readonly struct SentryId : IEquatable, IJsonSerializable { - private readonly Guid _eventId; + private readonly Guid _guid; /// /// An empty sentry id. @@ -18,7 +18,7 @@ namespace Sentry.Protocol /// /// Creates a new instance of a Sentry Id. /// - public SentryId(Guid guid) => _eventId = guid; + public SentryId(Guid guid) => _guid = guid; /// /// Sentry Id in the format Sentry recognizes. @@ -28,16 +28,25 @@ namespace Sentry.Protocol /// dashes which sentry doesn't expect when searching events. /// /// String representation of the event id. - public override string ToString() => _eventId.ToString("n"); + public override string ToString() => _guid.ToString("n"); + + // Note: spans are sentry IDs with only 16 characters, rest being truncated. + // This is obviously a bad idea as it invalidates GUID's uniqueness properties + // (https://devblogs.microsoft.com/oldnewthing/20080627-00/?p=21823) + // but all other SDKs do it this way, so we have no choice but to comply. + /// + /// Returns a truncated ID. + /// + public string ToShortString() => ToString().Substring(0, 16); /// - public bool Equals(SentryId other) => _eventId.Equals(other._eventId); + public bool Equals(SentryId other) => _guid.Equals(other._guid); /// public override bool Equals(object? obj) => obj is SentryId other && Equals(other); /// - public override int GetHashCode() => _eventId.GetHashCode(); + public override int GetHashCode() => _guid.GetHashCode(); /// /// Generates a new Sentry ID. @@ -74,7 +83,7 @@ public static SentryId FromJson(JsonElement json) /// /// The from the . /// - public static implicit operator Guid(SentryId sentryId) => sentryId._eventId; + public static implicit operator Guid(SentryId sentryId) => sentryId._guid; /// /// A from a . diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 9c63c1d242..7c68a0647f 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -47,11 +47,11 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteSerializable("span_id", SpanId); + writer.WriteString("span_id", SpanId.ToShortString()); if (ParentSpanId is {} parentSpanId) { - writer.WriteSerializable("parent_span_id", parentSpanId); + writer.WriteString("parent_span_id", parentSpanId.ToShortString()); } writer.WriteSerializable("trace_id", TraceId); diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 1a9da430e8..6d39801153 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -91,11 +91,11 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("name", Name); } - writer.WriteSerializable("span_id", SpanId); + writer.WriteString("span_id", SpanId.ToShortString()); if (ParentSpanId is {} parentSpanId) { - writer.WriteSerializable("parent_span_id", parentSpanId); + writer.WriteString("parent_span_id", parentSpanId.ToShortString()); } writer.WriteSerializable("trace_id", TraceId); From c314a667c281455ad3cb8d6f64640dd8e95b21c6 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 20:08:15 +0200 Subject: [PATCH 26/54] wip --- src/Sentry/Protocol/Envelopes/Envelope.cs | 3 +- src/Sentry/Protocol/Span.cs | 40 ++++++++------ src/Sentry/Protocol/Transaction.cs | 64 ++++++++++++++--------- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index d0dcb08b38..08a93b457d 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -111,9 +111,8 @@ public static Envelope FromUserFeedback(UserFeedback sentryUserFeedback) /// public static Envelope FromTransaction(Transaction transaction) { - var header = new Dictionary + var header = new Dictionary(StringComparer.Ordinal) { - [EventIdKey] = transaction.SpanId.ToString() }; var items = new[] diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 7c68a0647f..322a714169 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -47,6 +47,29 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); + writer.WriteString("type", "transaction"); + writer.WriteString("event_id", SentryId.Create().ToString()); + + writer.WriteString("start_timestamp", StartTimestamp); + + if (EndTimestamp is {} endTimestamp) + { + writer.WriteString("timestamp", endTimestamp); + } + + if (_tags is {} tags && tags.Any()) + { + writer.WriteDictionary("tags", tags!); + } + + if (_data is {} data && data.Any()) + { + writer.WriteDictionary("data", data!); + } + + writer.WriteStartObject("contexts"); + writer.WriteStartObject("trace"); + writer.WriteString("span_id", SpanId.ToShortString()); if (ParentSpanId is {} parentSpanId) @@ -55,12 +78,6 @@ public void WriteTo(Utf8JsonWriter writer) } writer.WriteSerializable("trace_id", TraceId); - writer.WriteString("start_timestamp", StartTimestamp); - - if (EndTimestamp is {} endTimestamp) - { - writer.WriteString("timestamp", endTimestamp); - } if (!string.IsNullOrWhiteSpace(Operation)) { @@ -79,15 +96,8 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteBoolean("sampled", IsSampled); - if (_tags is {} tags && tags.Any()) - { - writer.WriteDictionary("tags", tags!); - } - - if (_data is {} data && data.Any()) - { - writer.WriteDictionary("data", data!); - } + writer.WriteEndObject(); + writer.WriteEndObject(); writer.WriteEndObject(); } diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 6d39801153..60e7214b8c 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -82,23 +82,20 @@ public void Finish(SpanStatus status = SpanStatus.Ok) public void WriteTo(Utf8JsonWriter writer) { + // Transaction has a weird structure where some of the fields + // are apparently stored inside "contexts.trace" object for + // unknown reasons. + writer.WriteStartObject(); writer.WriteString("type", "transaction"); + writer.WriteString("event_id", SentryId.Create().ToString()); if (!string.IsNullOrWhiteSpace(Name)) { - writer.WriteString("name", Name); - } - - writer.WriteString("span_id", SpanId.ToShortString()); - - if (ParentSpanId is {} parentSpanId) - { - writer.WriteString("parent_span_id", parentSpanId.ToShortString()); + writer.WriteString("transaction", Name); } - writer.WriteSerializable("trace_id", TraceId); writer.WriteString("start_timestamp", StartTimestamp); if (EndTimestamp is {} endTimestamp) @@ -106,23 +103,6 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("timestamp", endTimestamp); } - if (!string.IsNullOrWhiteSpace(Operation)) - { - writer.WriteString("op", Operation); - } - - if (!string.IsNullOrWhiteSpace(Description)) - { - writer.WriteString("description", Description); - } - - if (Status is {} status) - { - writer.WriteString("status", status.ToString().ToLowerInvariant()); - } - - writer.WriteBoolean("sampled", IsSampled); - if (_tags is {} tags && tags.Any()) { writer.WriteDictionary("tags", tags!); @@ -145,6 +125,38 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteEndArray(); } + writer.WriteStartObject("contexts"); + writer.WriteStartObject("trace"); + + writer.WriteString("span_id", SpanId.ToShortString()); + + if (ParentSpanId is {} parentSpanId) + { + writer.WriteString("parent_span_id", parentSpanId.ToShortString()); + } + + writer.WriteSerializable("trace_id", TraceId); + + if (!string.IsNullOrWhiteSpace(Operation)) + { + writer.WriteString("op", Operation); + } + + if (!string.IsNullOrWhiteSpace(Description)) + { + writer.WriteString("description", Description); + } + + if (Status is {} status) + { + writer.WriteString("status", status.ToString().ToLowerInvariant()); + } + + writer.WriteBoolean("sampled", IsSampled); + + writer.WriteEndObject(); + writer.WriteEndObject(); + writer.WriteEndObject(); } From d0c5358f456e3d6c2e2afc57efcdae66592109a9 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 20:19:38 +0200 Subject: [PATCH 27/54] wip --- src/Sentry/IHub.cs | 8 +++++ src/Sentry/ISentryClient.cs | 1 + src/Sentry/Protocol/ISpan.cs | 42 ++++++++++++++++++++++++++ src/Sentry/Protocol/Span.cs | 30 +++++++++++++++++++ src/Sentry/Protocol/Transaction.cs | 47 ++++++++++++++++++++++++++++++ 5 files changed, 128 insertions(+) diff --git a/src/Sentry/IHub.cs b/src/Sentry/IHub.cs index 0470a4dbdd..174b28ebe2 100644 --- a/src/Sentry/IHub.cs +++ b/src/Sentry/IHub.cs @@ -1,3 +1,5 @@ +using Sentry.Protocol; + namespace Sentry { /// @@ -19,8 +21,14 @@ public interface IHub : /// SentryId LastEventId { get; } + /// + /// Creates a transaction. + /// Transaction CreateTransaction(string name, string operation); + /// + /// Gets the sentry trace header. + /// SentryTraceHeader? GetSentryTrace(); } } diff --git a/src/Sentry/ISentryClient.cs b/src/Sentry/ISentryClient.cs index a18ecb6717..47e412e479 100644 --- a/src/Sentry/ISentryClient.cs +++ b/src/Sentry/ISentryClient.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Sentry.Protocol; namespace Sentry { diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 77a3231326..b225a69628 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -3,32 +3,74 @@ namespace Sentry.Protocol { + /// + /// Span. + /// public interface ISpan { + /// + /// Span ID. + /// SentryId SpanId { get; } + /// + /// Parent ID. + /// SentryId? ParentSpanId { get; } + /// + /// Trace ID. + /// SentryId TraceId { get; } + /// + /// Start timestamp. + /// DateTimeOffset StartTimestamp { get; } + /// + /// End timestamp. + /// DateTimeOffset? EndTimestamp { get; } + /// + /// Operation. + /// string Operation { get; } + /// + /// Description. + /// string? Description { get; set; } + /// + /// Status. + /// SpanStatus? Status { get; } + /// + /// Is sampled. + /// bool IsSampled { get; set; } + /// + /// Tags. + /// IReadOnlyDictionary Tags { get; } + /// + /// Data. + /// IReadOnlyDictionary Data { get; } + /// + /// Starts a child span. + /// ISpan StartChild(string operation); + /// + /// Finishes the span. + /// void Finish(SpanStatus status = SpanStatus.Ok); } } diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 322a714169..386488953d 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -8,22 +8,46 @@ namespace Sentry.Protocol { // https://develop.sentry.dev/sdk/event-payloads/span + /// + /// Transaction span. + /// public class Span : ISpan, IJsonSerializable { + /// public SentryId SpanId { get; } + + /// public SentryId? ParentSpanId { get; } + + /// public SentryId TraceId { get; private set; } + + /// public DateTimeOffset StartTimestamp { get; private set; } + + /// public DateTimeOffset? EndTimestamp { get; private set; } + + /// public string Operation { get; } + + /// public string? Description { get; set; } + + /// public SpanStatus? Status { get; private set; } + + /// public bool IsSampled { get; set; } private ConcurrentDictionary? _tags; + + /// public IReadOnlyDictionary Tags => _tags ??= new ConcurrentDictionary(); private ConcurrentDictionary? _data; + + /// public IReadOnlyDictionary Data => _data ??= new ConcurrentDictionary(); internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") @@ -35,14 +59,17 @@ internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string ope Operation = operation; } + /// public ISpan StartChild(string operation) => new Span(null, SpanId, operation); + /// public void Finish(SpanStatus status = SpanStatus.Ok) { EndTimestamp = DateTimeOffset.Now; Status = status; } + /// public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); @@ -102,6 +129,9 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteEndObject(); } + /// + /// Parses a span from JSON. + /// public static Span FromJson(JsonElement json) { var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 60e7214b8c..997c5caeb0 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -8,30 +8,65 @@ namespace Sentry.Protocol { // https://develop.sentry.dev/sdk/event-payloads/transaction + /// + /// Sentry performance transaction. + /// public class Transaction : ISpan, IJsonSerializable { private readonly IHub _hub; + /// + /// Transaction name. + /// public string Name { get; } + + /// public SentryId SpanId { get; } + + /// public SentryId? ParentSpanId { get; } + + /// public SentryId TraceId { get; private set; } + + /// public DateTimeOffset StartTimestamp { get; private set; } + + /// public DateTimeOffset? EndTimestamp { get; private set; } + + /// public string Operation { get; } + + /// public string? Description { get; set; } + + /// public SpanStatus? Status { get; private set; } + + /// public bool IsSampled { get; set; } private Dictionary? _tags; + + /// public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); private Dictionary? _data; + + /// public IReadOnlyDictionary Data => _data ??= new Dictionary(); private List? _children; + + /// + /// Child spans. + /// public IReadOnlyList Children => _children ??= new List(); + /// + /// Initializes an instance of . + /// internal Transaction( IHub hub, string name, @@ -48,6 +83,9 @@ internal Transaction( Operation = operation; } + /// + /// Initializes an instance of . + /// public Transaction(IHub hub, string name, string operation) : this( hub, @@ -58,6 +96,7 @@ public Transaction(IHub hub, string name, string operation) { } + /// public ISpan StartChild(string operation) { var span = new Span(null, SpanId, operation); @@ -66,6 +105,7 @@ public ISpan StartChild(string operation) return span; } + /// public void Finish(SpanStatus status = SpanStatus.Ok) { EndTimestamp = DateTimeOffset.Now; @@ -74,12 +114,16 @@ public void Finish(SpanStatus status = SpanStatus.Ok) _hub.CaptureTransaction(this); } + /// + /// Get Sentry trace header. + /// public SentryTraceHeader GetTraceHeader() => new SentryTraceHeader( TraceId, SpanId, IsSampled ); + /// public void WriteTo(Utf8JsonWriter writer) { // Transaction has a weird structure where some of the fields @@ -160,6 +204,9 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteEndObject(); } + /// + /// Parses transaction from JSON. + /// public static Transaction FromJson(JsonElement json) { var hub = HubAdapter.Instance; From a4e01d8808a66cae58ed0ee7e6e7ac4b12b04cd1 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 20:23:02 +0200 Subject: [PATCH 28/54] wip --- src/Sentry/Extensibility/DisabledHub.cs | 1 + src/Sentry/Internal/Hub.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 54a83c56f2..987681395a 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Sentry.Protocol; namespace Sentry.Extensibility { diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 17e3f07678..1f8a50b176 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Sentry.Extensibility; using Sentry.Integrations; +using Sentry.Protocol; namespace Sentry.Internal { From d960d7ffa91ddebe46884aa4f45b3026e5d7c767 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 8 Dec 2020 20:28:52 +0200 Subject: [PATCH 29/54] wip --- src/Sentry/Extensibility/DisabledHub.cs | 3 +++ src/Sentry/Extensibility/HubAdapter.cs | 6 ++++++ src/Sentry/ISentryTraceSampler.cs | 6 ++++++ src/Sentry/Protocol/SentryId.cs | 3 +++ src/Sentry/TraceSamplingContext.cs | 14 ++++++++++++++ 5 files changed, 32 insertions(+) diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 987681395a..4e1492add3 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -75,6 +75,9 @@ public void BindClient(ISentryClient client) /// public SentryId CaptureEvent(SentryEvent evt, Scope? scope = null) => SentryId.Empty; + /// + /// No-Op. + /// public void CaptureTransaction(Transaction transaction) { } diff --git a/src/Sentry/Extensibility/HubAdapter.cs b/src/Sentry/Extensibility/HubAdapter.cs index 5ba399c93c..f884744429 100644 --- a/src/Sentry/Extensibility/HubAdapter.cs +++ b/src/Sentry/Extensibility/HubAdapter.cs @@ -71,10 +71,16 @@ public IDisposable PushScope(TState state) public void WithScope(Action scopeCallback) => SentrySdk.WithScope(scopeCallback); + /// + /// Forwards the call to . + /// [DebuggerStepThrough] public Transaction CreateTransaction(string name, string operation) => SentrySdk.CreateTransaction(name, operation); + /// + /// Forwards the call to . + /// [DebuggerStepThrough] public SentryTraceHeader? GetSentryTrace() => SentrySdk.GetTraceHeader(); diff --git a/src/Sentry/ISentryTraceSampler.cs b/src/Sentry/ISentryTraceSampler.cs index 95508bc18d..8b26d3d045 100644 --- a/src/Sentry/ISentryTraceSampler.cs +++ b/src/Sentry/ISentryTraceSampler.cs @@ -1,7 +1,13 @@ namespace Sentry { + /// + /// Trace sampler. + /// public interface ISentryTraceSampler { + /// + /// Gets the sample rate based on context. + /// double GetSampleRate(TraceSamplingContext context); } } diff --git a/src/Sentry/Protocol/SentryId.cs b/src/Sentry/Protocol/SentryId.cs index 9152239012..3b7de9abe2 100644 --- a/src/Sentry/Protocol/SentryId.cs +++ b/src/Sentry/Protocol/SentryId.cs @@ -57,6 +57,9 @@ namespace Sentry /// public void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(ToString()); + /// + /// Parses from string. + /// public static SentryId Parse(string value) => new SentryId(Guid.Parse(value)); /// diff --git a/src/Sentry/TraceSamplingContext.cs b/src/Sentry/TraceSamplingContext.cs index 52f95be0a7..99860a5ded 100644 --- a/src/Sentry/TraceSamplingContext.cs +++ b/src/Sentry/TraceSamplingContext.cs @@ -2,12 +2,26 @@ namespace Sentry { + /// + /// Trace sampling context. + /// public class TraceSamplingContext { + /// + /// Span. + /// public ISpan Span { get; } + /// + /// Span's parent. + /// public ISpan? ParentSpan { get; } + /// + /// Initializes an instance of . + /// + /// + /// public TraceSamplingContext(ISpan span, ISpan? parentSpan = null) { Span = span; From a0fb93e1420c5ea7de9dbd66403430ccd13abae1 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 9 Dec 2020 18:15:00 +0200 Subject: [PATCH 30/54] wip --- src/Sentry/Extensibility/DisabledHub.cs | 2 +- src/Sentry/Internal/Hub.cs | 7 +- src/Sentry/Protocol/Context/Contexts.cs | 9 + src/Sentry/Protocol/Context/Trace.cs | 76 +++++++ src/Sentry/Protocol/ISpan.cs | 33 +-- src/Sentry/Protocol/ISpanContext.cs | 36 ++++ src/Sentry/Protocol/Span.cs | 56 ++--- src/Sentry/Protocol/Transaction.cs | 275 ++++++++++++++++-------- 8 files changed, 336 insertions(+), 158 deletions(-) create mode 100644 src/Sentry/Protocol/Context/Trace.cs create mode 100644 src/Sentry/Protocol/ISpanContext.cs diff --git a/src/Sentry/Extensibility/DisabledHub.cs b/src/Sentry/Extensibility/DisabledHub.cs index 4e1492add3..03e261afc4 100644 --- a/src/Sentry/Extensibility/DisabledHub.cs +++ b/src/Sentry/Extensibility/DisabledHub.cs @@ -56,7 +56,7 @@ public void WithScope(Action scopeCallback) /// Returns a dummy transaction. /// public Transaction CreateTransaction(string name, string operation) => - new Transaction(this, name, operation); + new Transaction(this, null); /// /// Returns null. diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index 1f8a50b176..a204498f2c 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -103,7 +103,12 @@ public void WithScope(Action scopeCallback) public Transaction CreateTransaction(string name, string operation) { - var trans = new Transaction(this, name, operation); + var trans = new Transaction(this, _options) + { + Name = name, + Operation = operation + }; + ConfigureScope(scope => scope.Transaction = trans); return trans; diff --git a/src/Sentry/Protocol/Context/Contexts.cs b/src/Sentry/Protocol/Context/Contexts.cs index 6535624926..d04cafe3ac 100644 --- a/src/Sentry/Protocol/Context/Contexts.cs +++ b/src/Sentry/Protocol/Context/Contexts.cs @@ -45,6 +45,11 @@ public sealed class Contexts : ConcurrentDictionary, IJsonSerial /// public Gpu Gpu => this.GetOrCreate(Gpu.Type); + /// + /// This describes trace information. + /// + public Trace Trace => this.GetOrCreate(Trace.Type); + /// /// Initializes an instance of . /// @@ -123,6 +128,10 @@ public static Contexts FromJson(JsonElement json) { result[name] = Gpu.FromJson(value); } + else if (string.Equals(type, Trace.Type, StringComparison.OrdinalIgnoreCase)) + { + result[name] = Trace.FromJson(value); + } else { // Unknown context - parse as dictionary diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs new file mode 100644 index 0000000000..43997ab7d5 --- /dev/null +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -0,0 +1,76 @@ +using System.Text.Json; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol +{ + public class Trace : ISpanContext, IJsonSerializable + { + /// + /// Tells Sentry which type of context this is. + /// + public const string Type = "trace"; + + /// + public SentryId SpanId { get; set; } + + /// + public SentryId? ParentSpanId { get; set; } + + /// + public SentryId TraceId { get; set; } + + /// + public string Operation { get; set; } = "unknown"; + + /// + public SpanStatus? Status { get; set; } + + /// + public void WriteTo(Utf8JsonWriter writer) + { + writer.WriteStartObject(); + + writer.WriteString("span_id", SpanId.ToShortString()); + + if (ParentSpanId is {} parentSpanId) + { + writer.WriteString("parent_span_id", parentSpanId.ToShortString()); + } + + writer.WriteSerializable("trace_id", TraceId); + + if (!string.IsNullOrWhiteSpace(Operation)) + { + writer.WriteString("op", Operation); + } + + if (Status is {} status) + { + writer.WriteString("status", status.ToString().ToLowerInvariant()); + } + + writer.WriteEndObject(); + } + + /// + /// Parses trace context from JSON. + /// + public static Trace FromJson(JsonElement json) + { + var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); + var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; + var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; + var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); + + return new Trace + { + SpanId = spanId, + ParentSpanId = parentSpanId, + TraceId = traceId, + Operation = operation, + Status = status + }; + } + } +} diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index b225a69628..591d28f060 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -6,22 +6,12 @@ namespace Sentry.Protocol /// /// Span. /// - public interface ISpan + public interface ISpan : ISpanContext { /// - /// Span ID. - /// - SentryId SpanId { get; } - - /// - /// Parent ID. - /// - SentryId? ParentSpanId { get; } - - /// - /// Trace ID. + /// Description. /// - SentryId TraceId { get; } + string? Description { get; set; } /// /// Start timestamp. @@ -33,21 +23,6 @@ public interface ISpan /// DateTimeOffset? EndTimestamp { get; } - /// - /// Operation. - /// - string Operation { get; } - - /// - /// Description. - /// - string? Description { get; set; } - - /// - /// Status. - /// - SpanStatus? Status { get; } - /// /// Is sampled. /// @@ -61,7 +36,7 @@ public interface ISpan /// /// Data. /// - IReadOnlyDictionary Data { get; } + IReadOnlyDictionary Extra { get; } /// /// Starts a child span. diff --git a/src/Sentry/Protocol/ISpanContext.cs b/src/Sentry/Protocol/ISpanContext.cs new file mode 100644 index 0000000000..336b500e06 --- /dev/null +++ b/src/Sentry/Protocol/ISpanContext.cs @@ -0,0 +1,36 @@ +namespace Sentry.Protocol +{ + // Parts of transaction (which is a span) are stored in a context + // for some unknown reason. This interface defines those fields. + + /// + /// Span metadata. + /// + public interface ISpanContext + { + /// + /// Span ID. + /// + SentryId SpanId { get; } + + /// + /// Parent ID. + /// + SentryId? ParentSpanId { get; } + + /// + /// Trace ID. + /// + SentryId TraceId { get; } + + /// + /// Operation. + /// + string Operation { get; } + + /// + /// Status. + /// + SpanStatus? Status { get; } + } +} diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 386488953d..1545e56548 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -23,7 +23,7 @@ public class Span : ISpan, IJsonSerializable public SentryId TraceId { get; private set; } /// - public DateTimeOffset StartTimestamp { get; private set; } + public DateTimeOffset StartTimestamp { get; private set; } = DateTimeOffset.UtcNow; /// public DateTimeOffset? EndTimestamp { get; private set; } @@ -45,17 +45,16 @@ public class Span : ISpan, IJsonSerializable /// public IReadOnlyDictionary Tags => _tags ??= new ConcurrentDictionary(); - private ConcurrentDictionary? _data; + private ConcurrentDictionary? _data; /// - public IReadOnlyDictionary Data => _data ??= new ConcurrentDictionary(); + public IReadOnlyDictionary Extra => _data ??= new ConcurrentDictionary(); internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") { SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; TraceId = SentryId.Create(); - StartTimestamp = DateTimeOffset.Now; Operation = operation; } @@ -65,7 +64,7 @@ internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string ope /// public void Finish(SpanStatus status = SpanStatus.Ok) { - EndTimestamp = DateTimeOffset.Now; + EndTimestamp = DateTimeOffset.UtcNow; Status = status; } @@ -74,29 +73,6 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("type", "transaction"); - writer.WriteString("event_id", SentryId.Create().ToString()); - - writer.WriteString("start_timestamp", StartTimestamp); - - if (EndTimestamp is {} endTimestamp) - { - writer.WriteString("timestamp", endTimestamp); - } - - if (_tags is {} tags && tags.Any()) - { - writer.WriteDictionary("tags", tags!); - } - - if (_data is {} data && data.Any()) - { - writer.WriteDictionary("data", data!); - } - - writer.WriteStartObject("contexts"); - writer.WriteStartObject("trace"); - writer.WriteString("span_id", SpanId.ToShortString()); if (ParentSpanId is {} parentSpanId) @@ -123,8 +99,22 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteBoolean("sampled", IsSampled); - writer.WriteEndObject(); - writer.WriteEndObject(); + writer.WriteString("start_timestamp", StartTimestamp); + + if (EndTimestamp is {} endTimestamp) + { + writer.WriteString("timestamp", endTimestamp); + } + + if (_tags is {} tags && tags.Any()) + { + writer.WriteDictionary("tags", tags!); + } + + if (_data is {} data && data.Any()) + { + writer.WriteDictionary("data", data!); + } writer.WriteEndObject(); } @@ -142,9 +132,9 @@ public static Span FromJson(JsonElement json) var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); - var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; + var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); - var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); + var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); return new Span(spanId, parentSpanId, operation) { @@ -153,7 +143,7 @@ public static Span FromJson(JsonElement json) EndTimestamp = endTimestamp, Description = description, Status = status, - IsSampled = sampled, + IsSampled = isSampled, _tags = tags, _data = data }; diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 997c5caeb0..f1e314ce23 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -11,90 +11,145 @@ namespace Sentry.Protocol /// /// Sentry performance transaction. /// - public class Transaction : ISpan, IJsonSerializable + public class Transaction : ISpan, IScope, IJsonSerializable { private readonly IHub _hub; + /// + public IScopeOptions? ScopeOptions { get; } + /// /// Transaction name. /// - public string Name { get; } - - /// - public SentryId SpanId { get; } + public string Name { get; set; } = "unnamed"; /// - public SentryId? ParentSpanId { get; } + public SentryId SpanId + { + get => Contexts.Trace.SpanId; + private set => Contexts.Trace.SpanId = value; + } /// - public SentryId TraceId { get; private set; } + public SentryId TraceId + { + get => Contexts.Trace.TraceId; + private set => Contexts.Trace.TraceId = value; + } /// - public DateTimeOffset StartTimestamp { get; private set; } + public DateTimeOffset StartTimestamp { get; private set; } = DateTimeOffset.UtcNow; /// public DateTimeOffset? EndTimestamp { get; private set; } /// - public string Operation { get; } + public string Operation + { + get => Contexts.Trace.Operation; + internal set => Contexts.Trace.Operation = value; + } /// public string? Description { get; set; } /// - public SpanStatus? Status { get; private set; } + public SpanStatus? Status + { + get => Contexts.Trace.Status; + private set => Contexts.Trace.Status = value; + } /// public bool IsSampled { get; set; } - private Dictionary? _tags; + /// + public SentryLevel? Level { get; set; } + + private Request? _request; /// - public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); + public Request Request + { + get => _request ??= new Request(); + set => _request = value; + } + + private Contexts? _contexts; + + /// + public Contexts Contexts + { + get => _contexts ??= new Contexts(); + set => _contexts = value; + } + + private User? _user; + + /// + public User User + { + get => _user ??= new User(); + set => _user = value; + } + + /// + public string? Environment { get; set; } + + /// + public SdkVersion Sdk { get; internal set; } = new SdkVersion(); + + private IEnumerable? _fingerprint; + + /// + public IEnumerable Fingerprint + { + get => _fingerprint ?? Enumerable.Empty(); + set => _fingerprint = value; + } - private Dictionary? _data; + private List? _breadcrumbs; /// - public IReadOnlyDictionary Data => _data ??= new Dictionary(); + public IEnumerable Breadcrumbs => _breadcrumbs ??= new List(); + + private Dictionary? _extra; + + /// + public IReadOnlyDictionary Extra => _extra ??= new Dictionary(); + + private Dictionary? _tags; + + /// + public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); private List? _children; - /// - /// Child spans. - /// - public IReadOnlyList Children => _children ??= new List(); + // Transaction never has a parent + SentryId? ISpanContext.ParentSpanId => null; - /// - /// Initializes an instance of . - /// - internal Transaction( - IHub hub, - string name, - SentryId? spanId = null, - SentryId? parentSpanId = null, - string operation = "unknown") + string? IScope.TransactionName + { + get => Name; + set => Name = value ?? "unnamed"; + } + + internal Transaction(IHub hub, IScopeOptions? scopeOptions) { _hub = hub; - Name = name; - SpanId = spanId ?? SentryId.Create(); - ParentSpanId = parentSpanId; + ScopeOptions = scopeOptions; + + SpanId = SentryId.Create(); TraceId = SentryId.Create(); - StartTimestamp = DateTimeOffset.Now; - Operation = operation; } /// /// Initializes an instance of . /// - public Transaction(IHub hub, string name, string operation) - : this( - hub, - name, - null, - null, - operation) - { - } + /// + /// Child spans. + /// + public IReadOnlyList Children => _children ??= new List(); /// public ISpan StartChild(string operation) @@ -108,7 +163,7 @@ public ISpan StartChild(string operation) /// public void Finish(SpanStatus status = SpanStatus.Ok) { - EndTimestamp = DateTimeOffset.Now; + EndTimestamp = DateTimeOffset.UtcNow; Status = status; _hub.CaptureTransaction(this); @@ -126,20 +181,26 @@ public void Finish(SpanStatus status = SpanStatus.Ok) /// public void WriteTo(Utf8JsonWriter writer) { - // Transaction has a weird structure where some of the fields - // are apparently stored inside "contexts.trace" object for - // unknown reasons. - writer.WriteStartObject(); writer.WriteString("type", "transaction"); - writer.WriteString("event_id", SentryId.Create().ToString()); + writer.WriteSerializable("event_id", SentryId.Create()); + + if (Level is {} level) + { + writer.WriteString("level", level.ToString().ToLowerInvariant()); + } if (!string.IsNullOrWhiteSpace(Name)) { writer.WriteString("transaction", Name); } + if (!string.IsNullOrWhiteSpace(Description)) + { + writer.WriteString("description", Description); + } + writer.WriteString("start_timestamp", StartTimestamp); if (EndTimestamp is {} endTimestamp) @@ -147,59 +208,77 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("timestamp", endTimestamp); } - if (_tags is {} tags && tags.Any()) + writer.WriteBoolean("sampled", IsSampled); + + if (_request is {} request) { - writer.WriteDictionary("tags", tags!); + writer.WriteSerializable("request", request); } - if (_data is {} data && data.Any()) + if (_contexts is {} contexts) { - writer.WriteDictionary("data", data!); + writer.WriteSerializable("contexts", contexts); } - if (_children is {} children && children.Any()) + if (_user is {} user) + { + writer.WriteSerializable("user", user); + } + + if (!string.IsNullOrWhiteSpace(Environment)) + { + writer.WriteString("environment", Environment); + } + + writer.WriteSerializable("sdk", Sdk); + + if (_fingerprint is {} fingerprint && fingerprint.Any()) { - writer.WriteStartArray("spans"); + writer.WriteStartArray("fingerprint"); - foreach (var i in children) + foreach (var i in fingerprint) { - writer.WriteSerializableValue(i); + writer.WriteStringValue(i); } writer.WriteEndArray(); } - writer.WriteStartObject("contexts"); - writer.WriteStartObject("trace"); - - writer.WriteString("span_id", SpanId.ToShortString()); - - if (ParentSpanId is {} parentSpanId) + if (_breadcrumbs is {} breadcrumbs && breadcrumbs.Any()) { - writer.WriteString("parent_span_id", parentSpanId.ToShortString()); - } + writer.WriteStartArray("breadcrumbs"); - writer.WriteSerializable("trace_id", TraceId); + foreach (var i in breadcrumbs) + { + writer.WriteSerializableValue(i); + } - if (!string.IsNullOrWhiteSpace(Operation)) - { - writer.WriteString("op", Operation); + writer.WriteEndArray(); } - if (!string.IsNullOrWhiteSpace(Description)) + if (_extra is {} extra && extra.Any()) { - writer.WriteString("description", Description); + writer.WriteStartObject("extra"); + + foreach (var (key, value) in extra) + { + writer.WriteDynamic(key, value); + } + + writer.WriteEndObject(); } - if (Status is {} status) + if (_tags is {} tags && tags.Any()) { - writer.WriteString("status", status.ToString().ToLowerInvariant()); - } + writer.WriteStartObject("tags"); - writer.WriteBoolean("sampled", IsSampled); + foreach (var (key, value) in tags) + { + writer.WriteString(key, value); + } - writer.WriteEndObject(); - writer.WriteEndObject(); + writer.WriteEndObject(); + } writer.WriteEndObject(); } @@ -211,31 +290,39 @@ public static Transaction FromJson(JsonElement json) { var hub = HubAdapter.Instance; - var name = json.GetProperty("name").GetStringOrThrow(); - var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); - var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); - var endTimestamp = json.GetProperty("timestamp").GetDateTimeOffset(); - var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; + var name = json.GetProperty("transaction").GetStringOrThrow(); var description = json.GetPropertyOrNull("description")?.GetString(); - var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); - var sampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; + var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; + var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); + var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); + var level = json.GetPropertyOrNull("level")?.GetString()?.Pipe(s => s.ParseEnum()); + var request = json.GetPropertyOrNull("request")?.Pipe(Request.FromJson); + var contexts = json.GetPropertyOrNull("contexts")?.Pipe(Contexts.FromJson); + var user = json.GetPropertyOrNull("user")?.Pipe(User.FromJson); + var environment = json.GetPropertyOrNull("environment")?.GetString(); + var sdk = json.GetPropertyOrNull("sdk")?.Pipe(SdkVersion.FromJson) ?? new SdkVersion(); + var fingerprint = json.GetPropertyOrNull("fingerprint")?.EnumerateArray().Select(j => j.GetString()).ToArray(); + var breadcrumbs = json.GetPropertyOrNull("breadcrumbs")?.EnumerateArray().Select(Breadcrumb.FromJson).ToList(); + var extra = json.GetPropertyOrNull("extra")?.GetObjectDictionary()?.ToDictionary(); var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.ToDictionary(); - var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.ToDictionary(); - var children = json.GetPropertyOrNull("spans")?.EnumerateArray().Select(Span.FromJson).ToList(); - return new Transaction(hub, name, spanId, parentSpanId, operation) + return new Transaction(hub, null) { - TraceId = traceId, + Name = name, + Description = description, + IsSampled = isSampled, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, - Description = description, - Status = status, - IsSampled = sampled, - _tags = tags!, - _data = data!, - _children = children + Level = level, + _request = request, + _contexts = contexts, + _user = user, + Environment = environment, + Sdk = sdk, + _fingerprint = fingerprint!, + _breadcrumbs = breadcrumbs!, + _extra = extra!, + _tags = tags! }; } } From 6c1b41387d5c8331ce05d72068895d45d54ea70a Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 9 Dec 2020 19:23:05 +0200 Subject: [PATCH 31/54] wip --- src/Sentry/Internal/Hub.cs | 15 ++++++++++- .../Internal/MainSentryEventProcessor.cs | 25 ++++++++----------- src/Sentry/SentryClient.cs | 6 ----- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index a204498f2c..e422ccccad 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Sentry.Extensibility; @@ -109,6 +108,20 @@ public Transaction CreateTransaction(string name, string operation) Operation = operation }; + var nameAndVersion = MainSentryEventProcessor.NameAndVersion; + var protocolPackageName = MainSentryEventProcessor.ProtocolPackageName; + + if (trans.Sdk.Version == null && trans.Sdk.Name == null) + { + trans.Sdk.Name = Constants.SdkName; + trans.Sdk.Version = nameAndVersion.Version; + } + + if (nameAndVersion.Version != null) + { + trans.Sdk.AddPackage(protocolPackageName, nameAndVersion.Version); + } + ConfigureScope(scope => scope.Transaction = trans); return trans; diff --git a/src/Sentry/Internal/MainSentryEventProcessor.cs b/src/Sentry/Internal/MainSentryEventProcessor.cs index 4a13869f10..3e2ce272c9 100644 --- a/src/Sentry/Internal/MainSentryEventProcessor.cs +++ b/src/Sentry/Internal/MainSentryEventProcessor.cs @@ -31,10 +31,10 @@ internal class MainSentryEventProcessor : ISentryEventProcessor : null; }); - private static readonly SdkVersion NameAndVersion + internal static readonly SdkVersion NameAndVersion = typeof(ISentryClient).Assembly.GetNameAndVersion(); - private static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name; + internal static readonly string ProtocolPackageName = "nuget:" + NameAndVersion.Name; private readonly SentryOptions _options; internal Func SentryStackTraceFactoryAccessor { get; } @@ -96,20 +96,17 @@ public SentryEvent Process(SentryEvent @event) @event.Platform = Protocol.Constants.Platform; - if (@event.Sdk != null) + // SDK Name/Version might have be already set by an outer package + // e.g: ASP.NET Core can set itself as the SDK + if (@event.Sdk.Version == null && @event.Sdk.Name == null) { - // SDK Name/Version might have be already set by an outer package - // e.g: ASP.NET Core can set itself as the SDK - if (@event.Sdk.Version == null && @event.Sdk.Name == null) - { - @event.Sdk.Name = Constants.SdkName; - @event.Sdk.Version = NameAndVersion.Version; - } + @event.Sdk.Name = Constants.SdkName; + @event.Sdk.Version = NameAndVersion.Version; + } - if (NameAndVersion.Version != null) - { - @event.Sdk.AddPackage(ProtocolPackageName, NameAndVersion.Version); - } + if (NameAndVersion.Version != null) + { + @event.Sdk.AddPackage(ProtocolPackageName, NameAndVersion.Version); } // Report local user if opt-in PII, no user was already set to event and feature not opted-out: diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 645cf36f8a..9a5da42471 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -174,12 +174,6 @@ private SentryId DoSendEvent(SentryEvent @event, Scope? scope) scope.Evaluate(); scope.Apply(@event); - // Set transaction's span ID - if (scope.Transaction is {} transaction) - { - @event.SetTag("span_id", transaction.SpanId.ToString()); - } - if (scope.Level != null) { // Level on scope takes precedence over the one on event From 258ed5c5c64828db3146f978e182480c024fe28e Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Wed, 9 Dec 2020 19:23:46 +0200 Subject: [PATCH 32/54] wip --- src/Sentry/Internal/Hub.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index e422ccccad..2ad46c6495 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; using Sentry.Extensibility; From f2e5a8b1f3ad907ea3162e832fac33bae23abe7b Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Thu, 10 Dec 2020 17:41:37 +0200 Subject: [PATCH 33/54] wip --- src/Sentry/Protocol/Context/Trace.cs | 5 +++++ src/Sentry/Protocol/ISpan.cs | 5 ----- src/Sentry/Protocol/ISpanContext.cs | 5 +++++ src/Sentry/Protocol/Transaction.cs | 10 +++++----- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs index 43997ab7d5..1e8dd7b478 100644 --- a/src/Sentry/Protocol/Context/Trace.cs +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -25,6 +25,9 @@ public class Trace : ISpanContext, IJsonSerializable /// public SpanStatus? Status { get; set; } + /// + public bool IsSampled { get; set; } + /// public void WriteTo(Utf8JsonWriter writer) { @@ -49,6 +52,8 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("status", status.ToString().ToLowerInvariant()); } + writer.WriteBoolean("sampled", IsSampled); + writer.WriteEndObject(); } diff --git a/src/Sentry/Protocol/ISpan.cs b/src/Sentry/Protocol/ISpan.cs index 591d28f060..4708ddd3a9 100644 --- a/src/Sentry/Protocol/ISpan.cs +++ b/src/Sentry/Protocol/ISpan.cs @@ -23,11 +23,6 @@ public interface ISpan : ISpanContext /// DateTimeOffset? EndTimestamp { get; } - /// - /// Is sampled. - /// - bool IsSampled { get; set; } - /// /// Tags. /// diff --git a/src/Sentry/Protocol/ISpanContext.cs b/src/Sentry/Protocol/ISpanContext.cs index 336b500e06..0a8569bc7c 100644 --- a/src/Sentry/Protocol/ISpanContext.cs +++ b/src/Sentry/Protocol/ISpanContext.cs @@ -32,5 +32,10 @@ public interface ISpanContext /// Status. /// SpanStatus? Status { get; } + + /// + /// Is sampled. + /// + bool IsSampled { get; set; } } } diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index f1e314ce23..9b0836a89e 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -61,7 +61,11 @@ public SpanStatus? Status } /// - public bool IsSampled { get; set; } + public bool IsSampled + { + get => Contexts.Trace.IsSampled; + set => Contexts.Trace.IsSampled = value; + } /// public SentryLevel? Level { get; set; } @@ -208,8 +212,6 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteString("timestamp", endTimestamp); } - writer.WriteBoolean("sampled", IsSampled); - if (_request is {} request) { writer.WriteSerializable("request", request); @@ -292,7 +294,6 @@ public static Transaction FromJson(JsonElement json) var name = json.GetProperty("transaction").GetStringOrThrow(); var description = json.GetPropertyOrNull("description")?.GetString(); - var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); var level = json.GetPropertyOrNull("level")?.GetString()?.Pipe(s => s.ParseEnum()); @@ -310,7 +311,6 @@ public static Transaction FromJson(JsonElement json) { Name = name, Description = description, - IsSampled = isSampled, StartTimestamp = startTimestamp, EndTimestamp = endTimestamp, Level = level, From 41bcb99999a3a1d7c2e5a3de0171e1801a359bca Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Thu, 10 Dec 2020 17:47:44 +0200 Subject: [PATCH 34/54] wip --- .../Internal/Extensions/StringExtensions.cs | 10 ++++++++++ src/Sentry/Protocol/Context/Trace.cs | 4 ++-- src/Sentry/Protocol/Span.cs | 4 ++-- src/Sentry/Protocol/SpanStatus.cs | 19 ------------------- 4 files changed, 14 insertions(+), 23 deletions(-) create mode 100644 src/Sentry/Internal/Extensions/StringExtensions.cs diff --git a/src/Sentry/Internal/Extensions/StringExtensions.cs b/src/Sentry/Internal/Extensions/StringExtensions.cs new file mode 100644 index 0000000000..c526364a5f --- /dev/null +++ b/src/Sentry/Internal/Extensions/StringExtensions.cs @@ -0,0 +1,10 @@ +using System.Text.RegularExpressions; + +namespace Sentry.Internal.Extensions +{ + internal static class StringExtensions + { + public static string ToSnakeCase(this string str) => + Regex.Replace(str, @"(\p{Ll})(\p{Lu})", "$1_$2").ToLowerInvariant(); + } +} diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs index 1e8dd7b478..826c883a73 100644 --- a/src/Sentry/Protocol/Context/Trace.cs +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -49,7 +49,7 @@ public void WriteTo(Utf8JsonWriter writer) if (Status is {} status) { - writer.WriteString("status", status.ToString().ToLowerInvariant()); + writer.WriteString("status", status.ToString().ToSnakeCase()); } writer.WriteBoolean("sampled", IsSampled); @@ -66,7 +66,7 @@ public static Trace FromJson(JsonElement json) var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; - var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); + var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.Replace("_", "").ParseEnum()); return new Trace { diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 1545e56548..11028902aa 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -94,7 +94,7 @@ public void WriteTo(Utf8JsonWriter writer) if (Status is {} status) { - writer.WriteString("status", status.ToString().ToLowerInvariant()); + writer.WriteString("status", status.ToString().ToSnakeCase()); } writer.WriteBoolean("sampled", IsSampled); @@ -131,7 +131,7 @@ public static Span FromJson(JsonElement json) var endTimestamp = json.GetProperty("timestamp").GetDateTimeOffset(); var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var description = json.GetPropertyOrNull("description")?.GetString(); - var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.ParseEnum()); + var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.Replace("_", "").ParseEnum()); var isSampled = json.GetPropertyOrNull("sampled")?.GetBoolean() ?? false; var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); diff --git a/src/Sentry/Protocol/SpanStatus.cs b/src/Sentry/Protocol/SpanStatus.cs index 7c76d0d0d4..7510941b7f 100644 --- a/src/Sentry/Protocol/SpanStatus.cs +++ b/src/Sentry/Protocol/SpanStatus.cs @@ -1,5 +1,3 @@ -using System.Runtime.Serialization; - namespace Sentry.Protocol { /// @@ -8,71 +6,54 @@ namespace Sentry.Protocol public enum SpanStatus { /// The operation completed successfully. - [EnumMember(Value = "ok")] Ok, /// Deadline expired before operation could complete. - [EnumMember(Value = "deadlineExceeded")] DeadlineExceeded, /// 401 Unauthorized (actually does mean unauthenticated according to RFC 7235). - [EnumMember(Value = "unauthenticated")] Unauthenticated, /// 403 Forbidden - [EnumMember(Value = "permissionDenied")] PermissionDenied, /// 404 Not Found. Some requested entity (file or directory) was not found. - [EnumMember(Value = "notFound")] NotFound, /// 429 Too Many Requests - [EnumMember(Value = "resourceExhausted")] ResourceExhausted, /// Client specified an invalid argument. 4xx. - [EnumMember(Value = "invalidArgument")] InvalidArgument, /// 501 Not Implemented - [EnumMember(Value = "unimplemented")] Unimplemented, /// 503 Service Unavailable - [EnumMember(Value = "unavailable")] Unavailable, /// Other/generic 5xx. - [EnumMember(Value = "internalError")] InternalError, /// Unknown. Any non-standard HTTP status code. - [EnumMember(Value = "unknownError")] UnknownError, /// The operation was cancelled (typically by the user). - [EnumMember(Value = "cancelled")] Cancelled, /// Already exists (409). - [EnumMember(Value = "alreadyExists")] AlreadyExists, /// Operation was rejected because the system is not in a state required for the operation's - [EnumMember(Value = "failedPrecondition")] FailedPrecondition, /// The operation was aborted, typically due to a concurrency issue. - [EnumMember(Value = "aborted")] Aborted, /// Operation was attempted past the valid range. - [EnumMember(Value = "outOfRange")] OutOfRange, /// Unrecoverable data loss or corruption - [EnumMember(Value = "dataLoss")] DataLoss, } } From da7ee94547bf455106c87219c00670ce7a7ab221 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Thu, 10 Dec 2020 18:07:48 +0200 Subject: [PATCH 35/54] wip --- src/Sentry/Protocol/Span.cs | 18 ++++++++++--- src/Sentry/Protocol/SpanRecorder.cs | 40 +++++++++++++++++++++++++++++ src/Sentry/Protocol/Transaction.cs | 7 +++-- 3 files changed, 58 insertions(+), 7 deletions(-) create mode 100644 src/Sentry/Protocol/SpanRecorder.cs diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index 11028902aa..e663089b40 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -13,6 +13,8 @@ namespace Sentry.Protocol /// public class Span : ISpan, IJsonSerializable { + private readonly SpanRecorder _parentSpanRecorder; + /// public SentryId SpanId { get; } @@ -50,8 +52,9 @@ public class Span : ISpan, IJsonSerializable /// public IReadOnlyDictionary Extra => _data ??= new ConcurrentDictionary(); - internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") + internal Span(SpanRecorder parentSpanRecorder, SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") { + _parentSpanRecorder = parentSpanRecorder; SpanId = spanId ?? SentryId.Create(); ParentSpanId = parentSpanId; TraceId = SentryId.Create(); @@ -59,7 +62,13 @@ internal Span(SentryId? spanId = null, SentryId? parentSpanId = null, string ope } /// - public ISpan StartChild(string operation) => new Span(null, SpanId, operation); + public ISpan StartChild(string operation) + { + var span = new Span(_parentSpanRecorder, null, SpanId, operation); + _parentSpanRecorder.Add(span); + + return span; + } /// public void Finish(SpanStatus status = SpanStatus.Ok) @@ -124,6 +133,9 @@ public void WriteTo(Utf8JsonWriter writer) /// public static Span FromJson(JsonElement json) { + // TODO + var parentSpanRecorder = new SpanRecorder(); + var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; @@ -136,7 +148,7 @@ public static Span FromJson(JsonElement json) var tags = json.GetPropertyOrNull("tags")?.GetDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); var data = json.GetPropertyOrNull("data")?.GetObjectDictionary()?.Pipe(v => new ConcurrentDictionary(v!)); - return new Span(spanId, parentSpanId, operation) + return new Span(parentSpanRecorder, spanId, parentSpanId, operation) { TraceId = traceId, StartTimestamp = startTimestamp, diff --git a/src/Sentry/Protocol/SpanRecorder.cs b/src/Sentry/Protocol/SpanRecorder.cs new file mode 100644 index 0000000000..f285afa8aa --- /dev/null +++ b/src/Sentry/Protocol/SpanRecorder.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; + +namespace Sentry.Protocol +{ + /// + /// Records spans that belong to the same transaction. + /// + public class SpanRecorder + { + private const int MaxSpans = 1000; + + private readonly object _lock = new object(); + private readonly List _spans = new List(); + + /// + /// Records a span. + /// + public void Add(Span span) + { + lock (_lock) + { + if (_spans.Count < MaxSpans) + { + _spans.Add(span); + } + } + } + + /// + /// Gets all recorded spans. + /// + public IReadOnlyList GetAll() + { + lock (_lock) + { + return _spans; + } + } + } +} diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 9b0836a89e..a1a1556ebb 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -14,6 +14,7 @@ namespace Sentry.Protocol public class Transaction : ISpan, IScope, IJsonSerializable { private readonly IHub _hub; + private readonly SpanRecorder _spanRecorder = new SpanRecorder(); /// public IScopeOptions? ScopeOptions { get; } @@ -127,8 +128,6 @@ public IEnumerable Fingerprint /// public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); - private List? _children; - // Transaction never has a parent SentryId? ISpanContext.ParentSpanId => null; @@ -153,13 +152,13 @@ internal Transaction(IHub hub, IScopeOptions? scopeOptions) /// /// Child spans. /// - public IReadOnlyList Children => _children ??= new List(); + public IReadOnlyList Children => _spanRecorder.GetAll(); /// public ISpan StartChild(string operation) { var span = new Span(null, SpanId, operation); - (_children ??= new List()).Add(span); + _spanRecorder.Add(span); return span; } From 1371006c4579836f62b258289afbd74aa9eb322a Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Thu, 10 Dec 2020 18:24:09 +0200 Subject: [PATCH 36/54] wip --- src/Sentry/Protocol/Transaction.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index a1a1556ebb..82b7f5a3cf 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -146,18 +146,10 @@ internal Transaction(IHub hub, IScopeOptions? scopeOptions) TraceId = SentryId.Create(); } - /// - /// Initializes an instance of . - /// - /// - /// Child spans. - /// - public IReadOnlyList Children => _spanRecorder.GetAll(); - /// public ISpan StartChild(string operation) { - var span = new Span(null, SpanId, operation); + var span = new Span(_spanRecorder, null, SpanId, operation); _spanRecorder.Add(span); return span; From bafe82a44e00b83d97338b6de99697d9d647078a Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Thu, 10 Dec 2020 18:26:54 +0200 Subject: [PATCH 37/54] wip --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa6a27afc2..22be4d0de7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ * Ref moved SentryId from namespace Sentry.Protocol to Sentry (#643) @lucas-zimerman * Ref renamed `CacheFlushTimeout` to `InitCacheFlushTimeout` (#638) @lucas-zimerman +* Add support for performance +* Transaction (of type `string`) on Scope and Event now is called TransactionName ## 3.0.0-alpha.6 @@ -13,7 +15,6 @@ * Add net5.0 TFM to libraries #606 * Add more logging to CachingTransport #619 * Bump Microsoft.Bcl.AsyncInterfaces to 5.0.0 #618 -* Add support for performance * Bump `Microsoft.Bcl.AsyncInterfaces` to 5.0.0 #618 * `DefaultTags` moved from `SentryLoggingOptions` to `SentryOptions` (#637) @PureKrome * `Sentry.Serilog` can accept DefaultTags (#637) @PureKrome From 7abdb4f59aaa5bfd500c0cd4f51360f6447128e3 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 11 Dec 2020 17:33:23 +0200 Subject: [PATCH 38/54] wip --- .../Program.cs | 29 +++++++------ .../Properties/launchSettings.json | 4 +- .../Sentry.Samples.AspNetCore.Basic.csproj | 4 -- .../Extensions/HttpContextExtensions.cs | 43 +++++++++++++++++++ src/Sentry.AspNetCore/ScopeExtensions.cs | 40 ++++++++--------- src/Sentry.AspNetCore/SentryMiddleware.cs | 20 +++------ 6 files changed, 88 insertions(+), 52 deletions(-) create mode 100644 src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs index c86a7e7d2b..f8561ecc9b 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Program.cs +++ b/samples/Sentry.Samples.AspNetCore.Basic/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.DependencyInjection; @@ -26,20 +27,24 @@ public static IWebHost BuildWebHost(string[] args) => // .UseSentry("dsn") or .UseSentry(o => o.Dsn = ""; o.Release = "1.0"; ...) // The App: - .Configure(a => + .Configure(app => { // An example ASP.NET Core middleware that throws an // exception when serving a request to path: /throw - a.Use(async (context, next) => + app.UseRouting(); + app.UseEndpoints(endpoints => { - var log = context.RequestServices.GetService() - .CreateLogger(); + // Reported events will be grouped by route pattern + endpoints.MapGet("/throw/{message?}", context => + { + var exceptionMessage = context.GetRouteValue("message") as string; - log.LogInformation("Handling some request..."); + var log = context.RequestServices.GetRequiredService() + .CreateLogger(); - if (context.Request.Path == "/throw") - { - var hub = context.RequestServices.GetService(); + log.LogInformation("Handling some request..."); + + var hub = context.RequestServices.GetRequiredService(); hub.ConfigureScope(s => { // More data can be added to the scope like this: @@ -53,10 +58,10 @@ public static IWebHost BuildWebHost(string[] args) => // The following exception will be captured by the SDK and the event // will include the Log messages and any custom scope modifications // as exemplified above. - throw new Exception("An exception thrown from the ASP.NET Core pipeline"); - } - - await next(); + throw new Exception( + exceptionMessage ?? "An exception thrown from the ASP.NET Core pipeline" + ); + }); }); }) .Build(); diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Properties/launchSettings.json b/samples/Sentry.Samples.AspNetCore.Basic/Properties/launchSettings.json index b4f2ba4fd1..0506d1660c 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Properties/launchSettings.json +++ b/samples/Sentry.Samples.AspNetCore.Basic/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:59739/", + "applicationUrl": "http://localhost:59739", "sslPort": 0 } }, @@ -27,7 +27,7 @@ "SENTRY_RELEASE": "e386dfd", "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:59740/" + "applicationUrl": "http://localhost:59740" } } } diff --git a/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj b/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj index c24d1832df..f5e993077c 100644 --- a/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj +++ b/samples/Sentry.Samples.AspNetCore.Basic/Sentry.Samples.AspNetCore.Basic.csproj @@ -4,10 +4,6 @@ net5.0 - - - - diff --git a/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs b/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs new file mode 100644 index 0000000000..e8b27a6395 --- /dev/null +++ b/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +#if !NETSTANDARD2_0 +using Microsoft.AspNetCore.Http.Features; +#endif + +namespace Sentry.AspNetCore.Extensions +{ + internal static class HttpContextExtensions + { + public static string? TryGetRouteTemplate(this HttpContext context) + { +#if !NETSTANDARD2_0 + // Requires .UseRouting()/.UseEndpoints() + var endpoint = context.Features.Get()?.Endpoint as RouteEndpoint; + var routePattern = endpoint?.RoutePattern.RawText; + + if (!string.IsNullOrWhiteSpace(routePattern)) + { + return routePattern; + } +#endif + + // Requires legacy .UseMvc() + var routeData = context.GetRouteData(); + var controller = routeData.Values["controller"]?.ToString(); + var action = routeData.Values["action"]?.ToString(); + var area = routeData.Values["area"]?.ToString(); + + if (!string.IsNullOrWhiteSpace(action)) + { + return !string.IsNullOrWhiteSpace(area) + ? $"{controller}.{action}" + : $"{area}.{controller}.{action}"; + } + + // If the handler doesn't use routing (i.e. it checks `context.Request.Path` directly), + // then there is no way for us to extract anything that resembles a route template. + return null; + } + } +} diff --git a/src/Sentry.AspNetCore/ScopeExtensions.cs b/src/Sentry.AspNetCore/ScopeExtensions.cs index c42fcafa21..43c8579d03 100644 --- a/src/Sentry.AspNetCore/ScopeExtensions.cs +++ b/src/Sentry.AspNetCore/ScopeExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; +using Sentry.AspNetCore.Extensions; using Sentry.Extensibility; using Sentry.Protocol; @@ -68,29 +69,26 @@ public static void Populate(this Scope scope, HttpContext context, SentryAspNetC try { var routeData = context.GetRouteData(); - if (routeData != null) + var controller = routeData.Values["controller"]?.ToString(); + var action = routeData.Values["action"]?.ToString(); + var area = routeData.Values["area"]?.ToString(); + + if (controller != null) + { + scope.SetTag("route.controller", controller); + } + + if (action != null) { - var controller = routeData.Values["controller"]?.ToString(); - var action = routeData.Values["action"]?.ToString(); - var area = routeData.Values["area"]?.ToString(); - - if (controller != null) - { - scope.SetTag("route.controller", controller); - } - if (action != null) - { - scope.SetTag("route.action", action); - } - if (area != null) - { - scope.SetTag("route.area", area); - } - - scope.TransactionName = area == null - ? $"{controller}.{action}" - : $"{area}.{controller}.{action}"; + scope.SetTag("route.action", action); } + + if (area != null) + { + scope.SetTag("route.area", area); + } + + scope.TransactionName = context.TryGetRouteTemplate() ?? context.Request.Path; } catch(Exception e) { diff --git a/src/Sentry.AspNetCore/SentryMiddleware.cs b/src/Sentry.AspNetCore/SentryMiddleware.cs index cb98949542..0e1d0bd7cf 100644 --- a/src/Sentry.AspNetCore/SentryMiddleware.cs +++ b/src/Sentry.AspNetCore/SentryMiddleware.cs @@ -12,6 +12,7 @@ using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Sentry.AspNetCore.Extensions; using Sentry.Extensibility; using Sentry.Protocol; using Sentry.Reflection; @@ -96,17 +97,6 @@ public async Task InvokeAsync(HttpContext context) }); } - var routeData = context.GetRouteData(); - var controller = routeData.Values["controller"]?.ToString(); - var action = routeData.Values["action"]?.ToString(); - var area = routeData.Values["area"]?.ToString(); - - // TODO: What if it's not using controllers (i.e. endpoints)? - - var transactionName = area == null - ? $"{controller}.{action}" - : $"{area}.{controller}.{action}"; - hub.ConfigureScope(scope => { // At the point lots of stuff from the request are not yet filled @@ -119,14 +109,18 @@ public async Task InvokeAsync(HttpContext context) scope.OnEvaluating += (_, __) => PopulateScope(context, scope); }); - var transaction = hub.CreateTransaction(transactionName, "http.server"); + var transaction = hub.CreateTransaction( + // Try to get the route template or fallback to the request path + context.TryGetRouteTemplate() ?? context.Request.Path, + "http.server" + ); try { await _next(context).ConfigureAwait(false); // When an exception was handled by other component (i.e: UseExceptionHandler feature). - var exceptionFeature = context.Features.Get(); + var exceptionFeature = context.Features.Get(); if (exceptionFeature?.Error != null) { CaptureException(exceptionFeature.Error); From a1e48f87ffdebbf9c8b6b397cd65d070017a4464 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 11 Dec 2020 21:30:52 +0200 Subject: [PATCH 39/54] wip --- src/Sentry/SentryClient.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 9a5da42471..a3325067b3 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -134,6 +134,15 @@ public void CaptureTransaction(Transaction transaction) return; } + if (_options.TraceSampleRate < 1) + { + if (Random.NextDouble() > _options.TraceSampleRate) + { + _options.DiagnosticLogger?.LogDebug("Event sampled."); + return; + } + } + CaptureEnvelope(Envelope.FromTransaction(transaction)); } From fd13150c6f2eef9e93bc1925c30b1bc958e99113 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Fri, 11 Dec 2020 21:31:05 +0200 Subject: [PATCH 40/54] wip --- src/Sentry/SentryClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index a3325067b3..cfe7d63d4c 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -138,7 +138,7 @@ public void CaptureTransaction(Transaction transaction) { if (Random.NextDouble() > _options.TraceSampleRate) { - _options.DiagnosticLogger?.LogDebug("Event sampled."); + _options.DiagnosticLogger?.LogDebug("Transaction sampled."); return; } } From 134d0ecc85f5d8116501192257cff6f0458df0c8 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 18:54:37 +0200 Subject: [PATCH 41/54] wip --- src/Sentry/Protocol/Context/Trace.cs | 3 +++ src/Sentry/SentrySdk.cs | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs index 826c883a73..9db22cfbb4 100644 --- a/src/Sentry/Protocol/Context/Trace.cs +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -3,6 +3,9 @@ namespace Sentry.Protocol { + /// + /// Trace context data. + /// public class Trace : ISpanContext, IJsonSerializable { /// diff --git a/src/Sentry/SentrySdk.cs b/src/Sentry/SentrySdk.cs index dd18232356..263bcb3522 100644 --- a/src/Sentry/SentrySdk.cs +++ b/src/Sentry/SentrySdk.cs @@ -308,14 +308,23 @@ public static void CaptureUserFeedback(UserFeedback userFeedback) public static void CaptureUserFeedback(SentryId eventId, string email, string comments, string? name = null) => _hub.CaptureUserFeedback(new UserFeedback(eventId, email, comments, name)); + /// + /// Captures a transaction. + /// [DebuggerStepThrough] public static void CaptureTransaction(Transaction transaction) => _hub.CaptureTransaction(transaction); + /// + /// Creates a transaction. + /// [DebuggerStepThrough] public static Transaction CreateTransaction(string name, string operation) => _hub.CreateTransaction(name, operation); + /// + /// Gets the Sentry trace header. + /// [DebuggerStepThrough] public static SentryTraceHeader? GetTraceHeader() => _hub.GetSentryTrace(); From 6b927884b031a3a41e18e0ff658eb0c889728a96 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 19:45:24 +0200 Subject: [PATCH 42/54] wip --- test/Sentry.Tests/HubTests.cs | 2 +- .../Sentry.Tests/Protocol/TransactionTests.cs | 36 +++++++++++++++++ test/Sentry.Tests/SentryClientTests.cs | 39 +++++++++++++++++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 test/Sentry.Tests/Protocol/TransactionTests.cs diff --git a/test/Sentry.Tests/HubTests.cs b/test/Sentry.Tests/HubTests.cs index b908fe7e65..0ebb17e24f 100644 --- a/test/Sentry.Tests/HubTests.cs +++ b/test/Sentry.Tests/HubTests.cs @@ -28,7 +28,7 @@ public bool EnqueueEnvelope(Envelope envelope) return true; } - public Task FlushAsync(TimeSpan timeout) => default; + public Task FlushAsync(TimeSpan timeout) => Task.CompletedTask; } [Fact] diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs new file mode 100644 index 0000000000..5bbfcee0a1 --- /dev/null +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -0,0 +1,36 @@ +using FluentAssertions; +using Sentry.Extensibility; +using Sentry.Internal; +using Sentry.Protocol; +using Xunit; + +namespace Sentry.Tests.Protocol +{ + public class TransactionTests + { + [Fact] + public void Serialization_Roundtrip_Success() + { + // Arrange + var transaction = new Transaction(DisabledHub.Instance, null) + { + Name = "my transaction", + Operation = "some op", + Description = "description", + Request = new Request {Method = "GET", Url = "https://test.com"}, + User = new User {Email = "foo@bar.com", Username = "john"}, + Environment = "release", + Fingerprint = new[] {"foo", "bar"} + }; + + transaction.Finish(); + + // Act + var json = transaction.ToJsonString(); + var transactionRoundtrip = Transaction.FromJson(Json.Parse(json)); + + // Assert + transactionRoundtrip.Should().BeEquivalentTo(transaction); + } + } +} diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index cd2277d4a6..46fd98e6a2 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -5,6 +5,7 @@ using NSubstitute; using Sentry.Extensibility; using Sentry.Internal; +using Sentry.Protocol; using Sentry.Protocol.Envelopes; using VerifyXunit; using Xunit; @@ -365,6 +366,44 @@ public void CaptureUserFeedback_DisposedClient_ThrowsObjectDisposedException() _ = Assert.Throws(() => sut.CaptureUserFeedback(null)); } + [Fact] + public void CaptureTransaction_ValidTransaction_Sent() + { + // Arrange + var sut = _fixture.GetSut(); + + // Act + sut.CaptureTransaction(new Transaction(DisabledHub.Instance, null) + { + Name = "test name", + Operation = "test operation" + }); + + // Assert + _ = sut.Worker.Received(1).EnqueueEnvelope(Arg.Any()); + } + + [Fact] + public void CaptureTransaction_InvalidTransaction_Ignored() + { + // Arrange + var sut = _fixture.GetSut(); + + // Act + sut.CaptureTransaction(new Transaction(DisabledHub.Instance, null)); + + // Assert + _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); + } + + [Fact] + public void CaptureTransaction_DisposedClient_ThrowsObjectDisposedException() + { + var sut = _fixture.GetSut(); + sut.Dispose(); + _ = Assert.Throws(() => sut.CaptureTransaction(null)); + } + [Fact] public void Dispose_Worker_DisposeCalled() { From b6f3ee2b771b37d7468904c91a8ec78ec90a81f2 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 20:00:18 +0200 Subject: [PATCH 43/54] wip --- src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs b/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs index e8b27a6395..8b6822df3d 100644 --- a/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs +++ b/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs @@ -23,10 +23,10 @@ internal static class HttpContextExtensions #endif // Requires legacy .UseMvc() - var routeData = context.GetRouteData(); - var controller = routeData.Values["controller"]?.ToString(); - var action = routeData.Values["action"]?.ToString(); - var area = routeData.Values["area"]?.ToString(); + var routeData = context.Features.Get()?.RouteData; + var controller = routeData?.Values["controller"]?.ToString(); + var action = routeData?.Values["action"]?.ToString(); + var area = routeData?.Values["area"]?.ToString(); if (!string.IsNullOrWhiteSpace(action)) { From 0e9106632ff431464c171f33a65b27821cc89f4f Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 20:22:27 +0200 Subject: [PATCH 44/54] wip --- test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs b/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs index 0e73de0f26..08a9148ed4 100644 --- a/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs +++ b/test/Sentry.AspNetCore.Tests/SentryMiddlewareTests.cs @@ -14,6 +14,7 @@ using NSubstitute; using NSubstitute.ReturnsExtensions; using Sentry.Extensibility; +using Sentry.Protocol; using Xunit; namespace Sentry.AspNetCore.Tests @@ -35,6 +36,7 @@ public Fixture() { HubAccessor = () => Hub; _ = Hub.IsEnabled.Returns(true); + _ = Hub.CreateTransaction(default, default).ReturnsForAnyArgs(new Transaction(Hub, Options)); _ = HttpContext.Features.Returns(FeatureCollection); } From ea06cfb18347b231690c54aa5457d7a4b1bd6daa Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 21:43:41 +0200 Subject: [PATCH 45/54] wip --- test/Sentry.Tests/SentryClientTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/Sentry.Tests/SentryClientTests.cs b/test/Sentry.Tests/SentryClientTests.cs index 46fd98e6a2..7aa7ff0ed2 100644 --- a/test/Sentry.Tests/SentryClientTests.cs +++ b/test/Sentry.Tests/SentryClientTests.cs @@ -390,7 +390,11 @@ public void CaptureTransaction_InvalidTransaction_Ignored() var sut = _fixture.GetSut(); // Act - sut.CaptureTransaction(new Transaction(DisabledHub.Instance, null)); + sut.CaptureTransaction(new Transaction(DisabledHub.Instance, null) + { + Name = null!, + Operation = null! + }); // Assert _ = sut.Worker.DidNotReceive().EnqueueEnvelope(Arg.Any()); From 2cb49eff200965f6a7703d3f1e2edd452dbeb62b Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 21:45:05 +0200 Subject: [PATCH 46/54] wip --- src/Sentry/Protocol/Context/Trace.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs index 9db22cfbb4..0591029b8f 100644 --- a/src/Sentry/Protocol/Context/Trace.cs +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -36,6 +36,7 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); + writer.WriteString("type", Type); writer.WriteString("span_id", SpanId.ToShortString()); if (ParentSpanId is {} parentSpanId) From e908cee4cd7f6e78fe16471b383e16bfee7354e6 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 21:52:32 +0200 Subject: [PATCH 47/54] wip --- src/Sentry/Protocol/IScope.cs | 5 +++++ src/Sentry/Protocol/SentryEvent.cs | 7 +++++++ src/Sentry/Protocol/Transaction.cs | 7 +++++++ src/Sentry/Scope.cs | 4 +--- src/Sentry/ScopeExtensions.cs | 2 +- 5 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Sentry/Protocol/IScope.cs b/src/Sentry/Protocol/IScope.cs index eba510b9c2..c17f8a9126 100644 --- a/src/Sentry/Protocol/IScope.cs +++ b/src/Sentry/Protocol/IScope.cs @@ -69,6 +69,11 @@ public interface IScope /// string? TransactionName { get; set; } + /// + /// Transaction. + /// + Transaction? Transaction { get; set; } + /// /// SDK information. /// diff --git a/src/Sentry/Protocol/SentryEvent.cs b/src/Sentry/Protocol/SentryEvent.cs index aaa0ff3710..d8a0d2b795 100644 --- a/src/Sentry/Protocol/SentryEvent.cs +++ b/src/Sentry/Protocol/SentryEvent.cs @@ -169,6 +169,13 @@ public IEnumerable Fingerprint /// public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); + // TODO: this is a workaround, ideally Event should not inherit from IScope + Transaction? IScope.Transaction + { + get => null; + set {} + } + /// /// Creates a new instance of . /// diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index 82b7f5a3cf..c5a2cd2805 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -137,6 +137,13 @@ public IEnumerable Fingerprint set => Name = value ?? "unnamed"; } + // TODO: this is a workaround, ideally Transaction should not inherit from IScope + Transaction? IScope.Transaction + { + get => null; + set {} + } + internal Transaction(IHub hub, IScopeOptions? scopeOptions) { _hub = hub; diff --git a/src/Sentry/Scope.cs b/src/Sentry/Scope.cs index 3a51683f79..02d568d17e 100644 --- a/src/Sentry/Scope.cs +++ b/src/Sentry/Scope.cs @@ -128,9 +128,7 @@ public User User /// public IReadOnlyDictionary Tags { get; } = new ConcurrentDictionary(); - /// - /// Transaction. - /// + /// public Transaction? Transaction { get; set; } /// diff --git a/src/Sentry/ScopeExtensions.cs b/src/Sentry/ScopeExtensions.cs index c9d15fd818..73f4c00d5d 100644 --- a/src/Sentry/ScopeExtensions.cs +++ b/src/Sentry/ScopeExtensions.cs @@ -280,7 +280,7 @@ public static void UnsetTag(this IScope scope, string key) /// Conflicting keys are not overriden. /// This is a shallow copy. /// - public static void Apply(this Scope from, Scope to) + public static void Apply(this IScope from, IScope to) { // Not to throw on code that ignores nullability warnings. // ReSharper disable ConditionIsAlwaysTrueOrFalse From e8aa3cce43d8916829b08e542775cbd0db3d00f5 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 22:36:02 +0200 Subject: [PATCH 48/54] wip --- src/Sentry/ScopeExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Sentry/ScopeExtensions.cs b/src/Sentry/ScopeExtensions.cs index 73f4c00d5d..3b73450bbc 100644 --- a/src/Sentry/ScopeExtensions.cs +++ b/src/Sentry/ScopeExtensions.cs @@ -325,6 +325,7 @@ public static void Apply(this IScope from, IScope to) to.Environment ??= from.Environment; to.Transaction ??= from.Transaction; + to.TransactionName ??= from.TransactionName; to.Level ??= from.Level; if (from.Sdk is null || to.Sdk is null) From 71c87caeaea6789f05c6ad234cd1de52e356773b Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 22:37:40 +0200 Subject: [PATCH 49/54] wip --- test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs b/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs index 5066956ec2..f410e9d40a 100644 --- a/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs +++ b/test/Sentry.AspNetCore.Tests/ScopeExtensionsTests.cs @@ -280,7 +280,7 @@ public void Populate_RouteData_SetToScope() _sut.Populate(_httpContext, SentryAspNetCoreOptions); - Assert.Equal($"{controller}.{action}", _sut.Transaction?.Name); + Assert.Equal($"{controller}.{action}", _sut.TransactionName); } public static IEnumerable InvalidRequestBodies() From 0c512b194a493b8db02b567676d86a52ee88aecc Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 22:53:58 +0200 Subject: [PATCH 50/54] wip --- src/Sentry/Protocol/Context/Trace.cs | 12 ++-- src/Sentry/Protocol/ISpanContext.cs | 4 +- src/Sentry/Protocol/SentryId.cs | 15 +---- src/Sentry/Protocol/SentryTraceHeader.cs | 6 +- src/Sentry/Protocol/Span.cs | 16 ++--- src/Sentry/Protocol/SpanId.cs | 84 ++++++++++++++++++++++++ src/Sentry/Protocol/Transaction.cs | 6 +- 7 files changed, 109 insertions(+), 34 deletions(-) create mode 100644 src/Sentry/Protocol/SpanId.cs diff --git a/src/Sentry/Protocol/Context/Trace.cs b/src/Sentry/Protocol/Context/Trace.cs index 0591029b8f..df6998929a 100644 --- a/src/Sentry/Protocol/Context/Trace.cs +++ b/src/Sentry/Protocol/Context/Trace.cs @@ -14,10 +14,10 @@ public class Trace : ISpanContext, IJsonSerializable public const string Type = "trace"; /// - public SentryId SpanId { get; set; } + public SpanId SpanId { get; set; } /// - public SentryId? ParentSpanId { get; set; } + public SpanId? ParentSpanId { get; set; } /// public SentryId TraceId { get; set; } @@ -37,11 +37,11 @@ public void WriteTo(Utf8JsonWriter writer) writer.WriteStartObject(); writer.WriteString("type", Type); - writer.WriteString("span_id", SpanId.ToShortString()); + writer.WriteSerializable("span_id", SpanId); if (ParentSpanId is {} parentSpanId) { - writer.WriteString("parent_span_id", parentSpanId.ToShortString()); + writer.WriteSerializable("parent_span_id", parentSpanId); } writer.WriteSerializable("trace_id", TraceId); @@ -66,8 +66,8 @@ public void WriteTo(Utf8JsonWriter writer) /// public static Trace FromJson(JsonElement json) { - var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); + var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SpanId.FromJson) ?? SpanId.Empty; + var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SpanId.FromJson); var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var operation = json.GetPropertyOrNull("op")?.GetString() ?? "unknown"; var status = json.GetPropertyOrNull("status")?.GetString()?.Pipe(s => s.Replace("_", "").ParseEnum()); diff --git a/src/Sentry/Protocol/ISpanContext.cs b/src/Sentry/Protocol/ISpanContext.cs index 0a8569bc7c..b4ea8e07bd 100644 --- a/src/Sentry/Protocol/ISpanContext.cs +++ b/src/Sentry/Protocol/ISpanContext.cs @@ -11,12 +11,12 @@ public interface ISpanContext /// /// Span ID. /// - SentryId SpanId { get; } + SpanId SpanId { get; } /// /// Parent ID. /// - SentryId? ParentSpanId { get; } + SpanId? ParentSpanId { get; } /// /// Trace ID. diff --git a/src/Sentry/Protocol/SentryId.cs b/src/Sentry/Protocol/SentryId.cs index 3b7de9abe2..60bd96ed5f 100644 --- a/src/Sentry/Protocol/SentryId.cs +++ b/src/Sentry/Protocol/SentryId.cs @@ -31,15 +31,6 @@ namespace Sentry /// String representation of the event id. public override string ToString() => _guid.ToString("n"); - // Note: spans are sentry IDs with only 16 characters, rest being truncated. - // This is obviously a bad idea as it invalidates GUID's uniqueness properties - // (https://devblogs.microsoft.com/oldnewthing/20080627-00/?p=21823) - // but all other SDKs do it this way, so we have no choice but to comply. - /// - /// Returns a truncated ID. - /// - public string ToShortString() => ToString().Substring(0, 16); - /// public bool Equals(SentryId other) => _guid.Equals(other._guid); @@ -52,7 +43,7 @@ namespace Sentry /// /// Generates a new Sentry ID. /// - public static SentryId Create() => new SentryId(Guid.NewGuid()); + public static SentryId Create() => new(Guid.NewGuid()); /// public void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(ToString()); @@ -60,7 +51,7 @@ namespace Sentry /// /// Parses from string. /// - public static SentryId Parse(string value) => new SentryId(Guid.Parse(value)); + public static SentryId Parse(string value) => new(Guid.Parse(value)); /// /// Parses from JSON. @@ -92,6 +83,6 @@ public static SentryId FromJson(JsonElement json) /// /// A from a . /// - public static implicit operator SentryId(Guid guid) => new SentryId(guid); + public static implicit operator SentryId(Guid guid) => new(guid); } } diff --git a/src/Sentry/Protocol/SentryTraceHeader.cs b/src/Sentry/Protocol/SentryTraceHeader.cs index 9051e9c1d2..e08d4aaaec 100644 --- a/src/Sentry/Protocol/SentryTraceHeader.cs +++ b/src/Sentry/Protocol/SentryTraceHeader.cs @@ -8,13 +8,13 @@ namespace Sentry.Protocol public class SentryTraceHeader { private readonly SentryId _traceId; - private readonly SentryId _spanId; + private readonly SpanId _spanId; private readonly bool? _isSampled; /// /// Initializes an instance of . /// - public SentryTraceHeader(SentryId traceId, SentryId spanId, bool? isSampled) + public SentryTraceHeader(SentryId traceId, SpanId spanId, bool? isSampled) { _traceId = traceId; _spanId = spanId; @@ -38,7 +38,7 @@ public static SentryTraceHeader Parse(string value) } var traceId = SentryId.Parse(components[0]); - var spanId = SentryId.Parse(components[1]); + var spanId = SpanId.Parse(components[1]); var isSampled = components.Length >= 3 ? string.Equals(components[2], "1", StringComparison.OrdinalIgnoreCase) diff --git a/src/Sentry/Protocol/Span.cs b/src/Sentry/Protocol/Span.cs index e663089b40..9e568dd448 100644 --- a/src/Sentry/Protocol/Span.cs +++ b/src/Sentry/Protocol/Span.cs @@ -16,10 +16,10 @@ public class Span : ISpan, IJsonSerializable private readonly SpanRecorder _parentSpanRecorder; /// - public SentryId SpanId { get; } + public SpanId SpanId { get; } /// - public SentryId? ParentSpanId { get; } + public SpanId? ParentSpanId { get; } /// public SentryId TraceId { get; private set; } @@ -52,10 +52,10 @@ public class Span : ISpan, IJsonSerializable /// public IReadOnlyDictionary Extra => _data ??= new ConcurrentDictionary(); - internal Span(SpanRecorder parentSpanRecorder, SentryId? spanId = null, SentryId? parentSpanId = null, string operation = "unknown") + internal Span(SpanRecorder parentSpanRecorder, SpanId? spanId = null, SpanId? parentSpanId = null, string operation = "unknown") { _parentSpanRecorder = parentSpanRecorder; - SpanId = spanId ?? SentryId.Create(); + SpanId = spanId ?? SpanId.Create(); ParentSpanId = parentSpanId; TraceId = SentryId.Create(); Operation = operation; @@ -82,11 +82,11 @@ public void WriteTo(Utf8JsonWriter writer) { writer.WriteStartObject(); - writer.WriteString("span_id", SpanId.ToShortString()); + writer.WriteSerializable("span_id", SpanId); if (ParentSpanId is {} parentSpanId) { - writer.WriteString("parent_span_id", parentSpanId.ToShortString()); + writer.WriteSerializable("parent_span_id", parentSpanId); } writer.WriteSerializable("trace_id", TraceId); @@ -136,8 +136,8 @@ public static Span FromJson(JsonElement json) // TODO var parentSpanRecorder = new SpanRecorder(); - var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; - var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SentryId.FromJson); + var spanId = json.GetPropertyOrNull("span_id")?.Pipe(SpanId.FromJson) ?? SpanId.Empty; + var parentSpanId = json.GetPropertyOrNull("parent_span_id")?.Pipe(SpanId.FromJson); var traceId = json.GetPropertyOrNull("trace_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); var endTimestamp = json.GetProperty("timestamp").GetDateTimeOffset(); diff --git a/src/Sentry/Protocol/SpanId.cs b/src/Sentry/Protocol/SpanId.cs new file mode 100644 index 0000000000..e7cab657ff --- /dev/null +++ b/src/Sentry/Protocol/SpanId.cs @@ -0,0 +1,84 @@ +using System; +using System.Text.Json; + +namespace Sentry.Protocol +{ + /// + /// Sentry span ID. + /// + public readonly struct SpanId : IEquatable, IJsonSerializable + { + private readonly string _value; + + /// + /// An empty Sentry span ID. + /// + public static readonly SpanId Empty = new("0000000000000000"); + + /// + /// Creates a new instance of a Sentry span Id. + /// + public SpanId(string value) => _value = value; + + /// + public bool Equals(SpanId other) => StringComparer.Ordinal.Equals(_value, other._value); + + /// + public override bool Equals(object? obj) => obj is SpanId other && Equals(other); + + /// + public override int GetHashCode() => StringComparer.Ordinal.GetHashCode(_value); + + /// + public override string ToString() => _value; + + // Note: spans are sentry IDs with only 16 characters, rest being truncated. + // This is obviously a bad idea as it invalidates GUID's uniqueness properties + // (https://devblogs.microsoft.com/oldnewthing/20080627-00/?p=21823) + // but all other SDKs do it this way, so we have no choice but to comply. + /// + /// Generates a new Sentry ID. + /// + public static SpanId Create() => new(Guid.NewGuid().ToString("n").Substring(0, 16)); + + /// + public void WriteTo(Utf8JsonWriter writer) => writer.WriteStringValue(_value); + + /// + /// Parses from string. + /// + public static SpanId Parse(string value) => new(value); + + /// + /// Parses from JSON. + /// + public static SpanId FromJson(JsonElement json) + { + var value = json.GetString(); + + return !string.IsNullOrWhiteSpace(value) + ? new SpanId(value) + : Empty; + } + + /// + /// Equality operator. + /// + public static bool operator ==(SpanId left, SpanId right) => left.Equals(right); + + /// + /// Equality operator. + /// + public static bool operator !=(SpanId left, SpanId right) => !(left == right); + + /// + /// The from the . + /// + public static implicit operator string(SpanId id) => id._value; + + /// + /// A from a . + /// + public static implicit operator SpanId(string value) => new(value); + } +} diff --git a/src/Sentry/Protocol/Transaction.cs b/src/Sentry/Protocol/Transaction.cs index c5a2cd2805..085426864d 100644 --- a/src/Sentry/Protocol/Transaction.cs +++ b/src/Sentry/Protocol/Transaction.cs @@ -25,7 +25,7 @@ public class Transaction : ISpan, IScope, IJsonSerializable public string Name { get; set; } = "unnamed"; /// - public SentryId SpanId + public SpanId SpanId { get => Contexts.Trace.SpanId; private set => Contexts.Trace.SpanId = value; @@ -129,7 +129,7 @@ public IEnumerable Fingerprint public IReadOnlyDictionary Tags => _tags ??= new Dictionary(); // Transaction never has a parent - SentryId? ISpanContext.ParentSpanId => null; + SpanId? ISpanContext.ParentSpanId => null; string? IScope.TransactionName { @@ -149,7 +149,7 @@ internal Transaction(IHub hub, IScopeOptions? scopeOptions) _hub = hub; ScopeOptions = scopeOptions; - SpanId = SentryId.Create(); + SpanId = SpanId.Create(); TraceId = SentryId.Create(); } From fbd6f17a3c4458c013404abbfc662d3a9b0bad7c Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 22:58:55 +0200 Subject: [PATCH 51/54] wip --- src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs b/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs index 8b6822df3d..73112f8726 100644 --- a/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs +++ b/src/Sentry.AspNetCore/Extensions/HttpContextExtensions.cs @@ -31,8 +31,8 @@ internal static class HttpContextExtensions if (!string.IsNullOrWhiteSpace(action)) { return !string.IsNullOrWhiteSpace(area) - ? $"{controller}.{action}" - : $"{area}.{controller}.{action}"; + ? $"{area}.{controller}.{action}" + : $"{controller}.{action}"; } // If the handler doesn't use routing (i.e. it checks `context.Request.Path` directly), From 13b88dbd642810893ce066931a29cf648cd0f380 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 23:07:16 +0200 Subject: [PATCH 52/54] wip --- test/Sentry.Tests/Protocol/TransactionTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs index 5bbfcee0a1..1baf015e57 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -20,7 +20,8 @@ public void Serialization_Roundtrip_Success() Request = new Request {Method = "GET", Url = "https://test.com"}, User = new User {Email = "foo@bar.com", Username = "john"}, Environment = "release", - Fingerprint = new[] {"foo", "bar"} + Fingerprint = new[] {"foo", "bar"}, + Sdk = new SdkVersion {Name = "SDK", Version = "1.1.1"} }; transaction.Finish(); From eb37814c87606710640f25acd5492605da75d8be Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Mon, 14 Dec 2020 23:28:55 +0200 Subject: [PATCH 53/54] wip --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22be4d0de7..a7a53e3bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,8 @@ * Ref moved SentryId from namespace Sentry.Protocol to Sentry (#643) @lucas-zimerman * Ref renamed `CacheFlushTimeout` to `InitCacheFlushTimeout` (#638) @lucas-zimerman -* Add support for performance -* Transaction (of type `string`) on Scope and Event now is called TransactionName +* Add support for performance. ([#633](https://github.com/getsentry/sentry-dotnet/pull/633)) +* Transaction (of type `string`) on Scope and Event now is called TransactionName. ([#633](https://github.com/getsentry/sentry-dotnet/pull/633)) ## 3.0.0-alpha.6 From 61085a4e502fc2f35502a745b6811a32167901d5 Mon Sep 17 00:00:00 2001 From: Tyrrrz Date: Tue, 15 Dec 2020 16:35:03 +0200 Subject: [PATCH 54/54] wip --- src/Sentry/Internal/Extensions/StringExtensions.cs | 1 + src/Sentry/Protocol/Envelopes/Envelope.cs | 1 + test/Sentry.Tests/Protocol/TransactionTests.cs | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Sentry/Internal/Extensions/StringExtensions.cs b/src/Sentry/Internal/Extensions/StringExtensions.cs index c526364a5f..30e49f5359 100644 --- a/src/Sentry/Internal/Extensions/StringExtensions.cs +++ b/src/Sentry/Internal/Extensions/StringExtensions.cs @@ -4,6 +4,7 @@ namespace Sentry.Internal.Extensions { internal static class StringExtensions { + // Used to convert enum value into snake case, which is how Sentry represents them public static string ToSnakeCase(this string str) => Regex.Replace(str, @"(\p{Ll})(\p{Lu})", "$1_$2").ToLowerInvariant(); } diff --git a/src/Sentry/Protocol/Envelopes/Envelope.cs b/src/Sentry/Protocol/Envelopes/Envelope.cs index 08a93b457d..e66a9e4c57 100644 --- a/src/Sentry/Protocol/Envelopes/Envelope.cs +++ b/src/Sentry/Protocol/Envelopes/Envelope.cs @@ -113,6 +113,7 @@ public static Envelope FromTransaction(Transaction transaction) { var header = new Dictionary(StringComparer.Ordinal) { + // TODO: this should include transaction's event id }; var items = new[] diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs index 1baf015e57..514018eac3 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -17,8 +17,8 @@ public void Serialization_Roundtrip_Success() Name = "my transaction", Operation = "some op", Description = "description", - Request = new Request {Method = "GET", Url = "https://test.com"}, - User = new User {Email = "foo@bar.com", Username = "john"}, + Request = new Request {Method = "GET", Url = "https://example.com"}, + User = new User {Email = "test@sentry.example", Username = "john"}, Environment = "release", Fingerprint = new[] {"foo", "bar"}, Sdk = new SdkVersion {Name = "SDK", Version = "1.1.1"}