From 1a5a9800b26f3e85b254848a2d216c210020dd2e Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:12:49 -0700 Subject: [PATCH 1/9] Add polyfills for HashCode.Combine --- src/Sentry/Internal/Polyfills.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/Sentry/Internal/Polyfills.cs b/src/Sentry/Internal/Polyfills.cs index a8d0fb675d..f67864fc8d 100644 --- a/src/Sentry/Internal/Polyfills.cs +++ b/src/Sentry/Internal/Polyfills.cs @@ -65,6 +65,34 @@ public static IEnumerable SkipLast(this IEnumerable source, int count) source.Reverse().Skip(count).Reverse(); } } + +namespace System +{ + internal static class HashCode + { + public static int Combine(T1 value1, T2 value2) + { + unchecked + { + var hashCode = value1 != null ? value1.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ (value2 != null ? value2.GetHashCode() : 0); + return hashCode; + } + } + + public static int Combine(T1 value1, T2 value2, T3 value3) + { + unchecked + { + var hashCode = value1 != null ? value1.GetHashCode() : 0; + hashCode = (hashCode * 397) ^ (value2 != null ? value2.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (value3 != null ? value3.GetHashCode() : 0); + return hashCode; + } + } + } +} + #endif #if NET461 From 0e716740f7396d79ac74b5988aa56afe264a7640 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:13:31 -0700 Subject: [PATCH 2/9] Add more JsonExtensions --- .../Internal/Extensions/JsonExtensions.cs | 100 +++++++++++++++--- 1 file changed, 88 insertions(+), 12 deletions(-) diff --git a/src/Sentry/Internal/Extensions/JsonExtensions.cs b/src/Sentry/Internal/Extensions/JsonExtensions.cs index b3e811f933..26e32545c3 100644 --- a/src/Sentry/Internal/Extensions/JsonExtensions.cs +++ b/src/Sentry/Internal/Extensions/JsonExtensions.cs @@ -52,6 +52,26 @@ public static void Deconstruct(this JsonProperty jsonProperty, out string name, return result; } + public static Dictionary? GetDictionaryOrNull( + this JsonElement json, + Func factory) + where TValue : IJsonSerializable? + { + if (json.ValueKind != JsonValueKind.Object) + { + return null; + } + + var result = new Dictionary(); + + foreach (var (name, value) in json.EnumerateObject()) + { + result[name] = factory(value); + } + + return result; + } + public static Dictionary? GetStringDictionaryOrNull(this JsonElement json) { if (json.ValueKind != JsonValueKind.Object) @@ -189,6 +209,37 @@ public static void WriteDictionaryValue( } } + public static void WriteDictionaryValue( + this Utf8JsonWriter writer, + IEnumerable>? dic, + IDiagnosticLogger? logger, + bool includeNullValues = true) + where TValue : IJsonSerializable? + { + if (dic is not null) + { + writer.WriteStartObject(); + + foreach (var (key, value) in dic) + { + if (value is not null) + { + writer.WriteSerializable(key, value, logger); + } + else if (includeNullValues) + { + writer.WriteNull(key); + } + } + + writer.WriteEndObject(); + } + else + { + writer.WriteNullValue(); + } + } + public static void WriteStringDictionaryValue( this Utf8JsonWriter writer, IEnumerable>? dic) @@ -220,6 +271,17 @@ public static void WriteDictionary( writer.WriteDictionaryValue(dic, logger); } + public static void WriteDictionary( + this Utf8JsonWriter writer, + string propertyName, + IEnumerable>? dic, + IDiagnosticLogger? logger) + where TValue : IJsonSerializable? + { + writer.WritePropertyName(propertyName); + writer.WriteDictionaryValue(dic, logger); + } + public static void WriteStringDictionary( this Utf8JsonWriter writer, string propertyName, @@ -552,10 +614,24 @@ public static void WriteDictionaryIfNotEmpty( IEnumerable>? dic, IDiagnosticLogger? logger) { - var asDictionary = dic as IReadOnlyDictionary ?? dic?.ToDictionary(); - if (asDictionary is not null && asDictionary.Count > 0) + var dictionary = dic as IReadOnlyDictionary ?? dic?.ToDictionary(); + if (dictionary is not null && dictionary.Count > 0) + { + writer.WriteDictionary(propertyName, dictionary, logger); + } + } + + public static void WriteDictionaryIfNotEmpty( + this Utf8JsonWriter writer, + string propertyName, + IEnumerable>? dic, + IDiagnosticLogger? logger) + where TValue : IJsonSerializable? + { + var dictionary = dic as IReadOnlyDictionary ?? dic?.ToDictionary(); + if (dictionary is not null && dictionary.Count > 0) { - writer.WriteDictionary(propertyName, asDictionary, logger); + writer.WriteDictionary(propertyName, dictionary, logger); } } @@ -564,10 +640,10 @@ public static void WriteStringDictionaryIfNotEmpty( string propertyName, IEnumerable>? dic) { - var asDictionary = dic as IReadOnlyDictionary ?? dic?.ToDictionary(); - if (asDictionary is not null && asDictionary.Count > 0) + var dictionary = dic as IReadOnlyDictionary ?? dic?.ToDictionary(); + if (dictionary is not null && dictionary.Count > 0) { - writer.WriteStringDictionary(propertyName, asDictionary); + writer.WriteStringDictionary(propertyName, dictionary); } } @@ -577,10 +653,10 @@ public static void WriteArrayIfNotEmpty( IEnumerable? arr, IDiagnosticLogger? logger) { - var asList = arr as IReadOnlyList ?? arr?.ToArray(); - if (asList is not null && asList.Count > 0) + var list = arr as IReadOnlyList ?? arr?.ToArray(); + if (list is not null && list.Count > 0) { - writer.WriteArray(propertyName, asList, logger); + writer.WriteArray(propertyName, list, logger); } } @@ -589,10 +665,10 @@ public static void WriteStringArrayIfNotEmpty( string propertyName, IEnumerable? arr) { - var asList = arr as IReadOnlyList ?? arr?.ToArray(); - if (asList is not null && asList.Count > 0) + var list = arr as IReadOnlyList ?? arr?.ToArray(); + if (list is not null && list.Count > 0) { - writer.WriteStringArray(propertyName, asList); + writer.WriteStringArray(propertyName, list); } } From 44230e379e2fb150b699e283d1f8e41444ed5ed9 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:14:16 -0700 Subject: [PATCH 3/9] Add Measurement And MeasurementUnit --- src/Sentry/MeasurementUnit.Duration.cs | 57 ++++++++++++++ src/Sentry/MeasurementUnit.Fraction.cs | 31 ++++++++ src/Sentry/MeasurementUnit.Information.cs | 90 ++++++++++++++++++++++ src/Sentry/MeasurementUnit.cs | 94 +++++++++++++++++++++++ src/Sentry/Protocol/Measurement.cs | 88 +++++++++++++++++++++ 5 files changed, 360 insertions(+) create mode 100644 src/Sentry/MeasurementUnit.Duration.cs create mode 100644 src/Sentry/MeasurementUnit.Fraction.cs create mode 100644 src/Sentry/MeasurementUnit.Information.cs create mode 100644 src/Sentry/MeasurementUnit.cs create mode 100644 src/Sentry/Protocol/Measurement.cs diff --git a/src/Sentry/MeasurementUnit.Duration.cs b/src/Sentry/MeasurementUnit.Duration.cs new file mode 100644 index 0000000000..8eed1831cc --- /dev/null +++ b/src/Sentry/MeasurementUnit.Duration.cs @@ -0,0 +1,57 @@ +namespace Sentry +{ + public readonly partial struct MeasurementUnit + { + /// + /// A time duration unit + /// + /// + public enum Duration + { + /// + /// Nanosecond unit (10^-9 seconds) + /// + Nanosecond, + + /// + /// Microsecond unit (10^-6 seconds) + /// + Microsecond, + + /// + /// Millisecond unit (10^-3 seconds) + /// + Millisecond, + + /// + /// Second unit + /// + Second, + + /// + /// Minute unit (60 seconds) + /// + Minute, + + /// + /// Hour unit (3,600 seconds) + /// + Hour, + + /// + /// Day unit (86,400 seconds) + /// + Day, + + /// + /// Week unit (604,800 seconds) + /// + Week + } + + /// + /// Implicitly casts a to a . + /// + public static implicit operator MeasurementUnit(Duration unit) => new(unit); + } +} diff --git a/src/Sentry/MeasurementUnit.Fraction.cs b/src/Sentry/MeasurementUnit.Fraction.cs new file mode 100644 index 0000000000..e689e15085 --- /dev/null +++ b/src/Sentry/MeasurementUnit.Fraction.cs @@ -0,0 +1,31 @@ +using System.ComponentModel; + +namespace Sentry +{ + public readonly partial struct MeasurementUnit + { + /// + /// A fraction unit + /// + /// + public enum Fraction + { + /// + /// Floating point fraction of 1. + /// A ratio of 1.0 equals 100%. + /// + Ratio, + + /// + /// Ratio expressed as a fraction of 100. + /// 100% equals a ratio of 1.0. + /// + Percent + } + + /// + /// Implicitly casts a to a . + /// + public static implicit operator MeasurementUnit(Fraction unit) => new(unit); + } +} diff --git a/src/Sentry/MeasurementUnit.Information.cs b/src/Sentry/MeasurementUnit.Information.cs new file mode 100644 index 0000000000..ca2fc448c0 --- /dev/null +++ b/src/Sentry/MeasurementUnit.Information.cs @@ -0,0 +1,90 @@ +namespace Sentry +{ + public readonly partial struct MeasurementUnit + { + /// + /// An information size unit + /// + /// + public enum Information + { + /// + /// Bit unit (1/8 of byte) + /// + /// + /// Some computer systems may have a different number of bits per byte. + /// + Bit, + + /// + /// Byte unit + /// + Byte, + + /// + /// Kilobyte unit (10^3 bytes) + /// + Kilobyte, + + /// + /// Kibibyte unit (2^10 bytes) + /// + Kibibyte, + + /// + /// Megabyte unit (10^6 bytes) + /// + Megabyte, + + /// + /// Mebibyte unit (2^20 bytes) + /// + Mebibyte, + + /// + /// Gigabyte unit (10^9 bytes) + /// + Gigabyte, + + /// + /// Gibibyte unit (2^30 bytes) + /// + Gibibyte, + + /// + /// Terabyte unit (10^12 bytes) + /// + Terabyte, + + /// + /// Tebibyte unit (2^40 bytes) + /// + Tebibyte, + + /// + /// Petabyte unit (10^15 bytes) + /// + Petabyte, + + /// + /// Pebibyte unit (2^50 bytes) + /// + Pebibyte, + + /// + /// Exabyte unit (10^18 bytes) + /// + Exabyte, + + /// + /// Exbibyte unit (2^60 bytes) + /// + Exbibyte + } + + /// + /// Implicitly casts a to a . + /// + public static implicit operator MeasurementUnit(Information unit) => new(unit); + } +} diff --git a/src/Sentry/MeasurementUnit.cs b/src/Sentry/MeasurementUnit.cs new file mode 100644 index 0000000000..364009d5f4 --- /dev/null +++ b/src/Sentry/MeasurementUnit.cs @@ -0,0 +1,94 @@ +using System; + +namespace Sentry +{ + /// + /// The unit of measurement of a metric value. + /// + /// + public readonly partial struct MeasurementUnit : IEquatable + { + private readonly Enum? _unit; + private readonly string? _name; + + private MeasurementUnit(Enum unit) + { + _unit = unit; + _name = null; + } + + private MeasurementUnit(string name) + { + _unit = null; + _name = name; + } + + /// + /// Represents an untyped measurement unit, used for measurements that have no natural unit. + /// + public static MeasurementUnit None = new(); + + /// + /// Creates a custom measurement unit. + /// + /// The name of the custom measurement unit. It will be converted to lower case. + /// The custom measurement unit. + public static MeasurementUnit Custom(string name) => new(name.ToLowerInvariant()); + + internal static MeasurementUnit Parse(string? name) + { + if (name == null) + { + return None; + } + + name = name.Trim(); + + if (name.Length == 0) + { + return None; + } + + if (Enum.TryParse(name, ignoreCase: true, out var duration)) + { + return duration; + } + + if (Enum.TryParse(name, ignoreCase: true, out var information)) + { + return information; + } + + if (Enum.TryParse(name, ignoreCase: true, out var fraction)) + { + return fraction; + } + + return Custom(name); + } + + /// + /// Returns the string representation of the measurement unit, as it will be sent to Sentry. + /// + public override string ToString() => _unit?.ToString().ToLowerInvariant() ?? _name ?? ""; + + /// + public bool Equals(MeasurementUnit other) => Equals(_unit, other._unit) && _name == other._name; + + /// + public override bool Equals(object? obj) => obj is MeasurementUnit other && Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(_unit, _name, _unit?.GetType()); + + /// + /// Returns true if the operands are equal. + /// + public static bool operator ==(MeasurementUnit left, MeasurementUnit right) => left.Equals(right); + + /// + /// Returns true if the operands are not equal. + /// + public static bool operator !=(MeasurementUnit left, MeasurementUnit right) => !left.Equals(right); + } +} diff --git a/src/Sentry/Protocol/Measurement.cs b/src/Sentry/Protocol/Measurement.cs new file mode 100644 index 0000000000..6982f9af1c --- /dev/null +++ b/src/Sentry/Protocol/Measurement.cs @@ -0,0 +1,88 @@ +using System.Text.Json; +using Sentry.Extensibility; +using Sentry.Internal.Extensions; + +namespace Sentry.Protocol +{ + /// + /// A measurement, containing a numeric value and a unit. + /// + public sealed class Measurement : IJsonSerializable + { + /// + /// The numeric value of the measurement. + /// + public object Value { get; } + + /// + /// The unit of measurement. + /// + public MeasurementUnit Unit { get; } + + private Measurement(object value, MeasurementUnit unit) + { + Value = value; + Unit = unit; + } + + internal Measurement(int value, MeasurementUnit unit = default) + { + Value = value; + Unit = unit; + } + + internal Measurement(long value, MeasurementUnit unit = default) + { + Value = value; + Unit = unit; + } + + internal Measurement(ulong value, MeasurementUnit unit = default) + { + Value = value; + Unit = unit; + } + + internal Measurement(double value, MeasurementUnit unit = default) + { + Value = value; + Unit = unit; + } + + /// + public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) + { + writer.WriteStartObject(); + + switch (Value) + { + case int number: + writer.WriteNumber("value", number); + break; + case long number: + writer.WriteNumber("value", number); + break; + case ulong number: + writer.WriteNumber("value", number); + break; + case double number: + writer.WriteNumber("value", number); + break; + } + + writer.WriteStringIfNotWhiteSpace("unit", Unit.ToString()); + + writer.WriteEndObject(); + } + + /// + /// Parses from JSON. + /// + public static Measurement FromJson(JsonElement json) + { + var value = json.GetProperty("value").GetDynamicOrNull()!; + var unit = json.GetPropertyOrNull("unit")?.GetString(); + return new Measurement(value, MeasurementUnit.Parse(unit)); + } + } +} From 7860f592b6329e8548ebd3065184409b58e84778 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:14:33 -0700 Subject: [PATCH 4/9] Add measurements to transactions --- src/Sentry/IHasMeasurements.cs | 69 +++++++++++++++++++++++++++++++++ src/Sentry/Transaction.cs | 60 ++++++++++++++++++---------- src/Sentry/TransactionTracer.cs | 14 ++++++- 3 files changed, 121 insertions(+), 22 deletions(-) create mode 100644 src/Sentry/IHasMeasurements.cs diff --git a/src/Sentry/IHasMeasurements.cs b/src/Sentry/IHasMeasurements.cs new file mode 100644 index 0000000000..b9f4cf4e3a --- /dev/null +++ b/src/Sentry/IHasMeasurements.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Sentry.Protocol; + +namespace Sentry +{ + /// + /// Interface for transactions that can keep track of measurements. + /// + /// + /// Ideally, this would just be implemented as part of . + /// However, adding a property to a public interface is a breaking change. We can do that in a future major version. + /// + internal interface IHasMeasurements + { + /// + /// The measurements that have been set on the transaction. + /// + IReadOnlyDictionary Measurements { get; } + + /// + /// Sets a measurement on the transaction. + /// + /// The name of the measurement. + /// The measurement. + [EditorBrowsable(EditorBrowsableState.Never)] + void SetMeasurement(string name, Measurement measurement); + } + + /// + /// Extensions for + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public static class MeasurementExtensions + { + /// + /// Sets a measurement on the transaction. + /// + /// The transaction. + /// The name of the measurement. + /// The value of the measurement. + /// + /// The optional unit of the measurement. Defaults to . + /// + public static void SetMeasurement(this ITransactionData transaction, string name, int value, + MeasurementUnit unit = default) => + (transaction as IHasMeasurements)?.SetMeasurement(name, new Measurement(value, unit)); + + /// + public static void SetMeasurement(this ITransactionData transaction, string name, long value, + MeasurementUnit unit = default) => + (transaction as IHasMeasurements)?.SetMeasurement(name, new Measurement(value, unit)); + + /// +#if !__MOBILE__ + // ulong parameter is not CLS compliant + [CLSCompliant(false)] +#endif + public static void SetMeasurement(this ITransactionData transaction, string name, ulong value, + MeasurementUnit unit = default) => + (transaction as IHasMeasurements)?.SetMeasurement(name, new Measurement(value, unit)); + + /// + public static void SetMeasurement(this ITransactionData transaction, string name, double value, + MeasurementUnit unit = default) => + (transaction as IHasMeasurements)?.SetMeasurement(name, new Measurement(value, unit)); + } +} diff --git a/src/Sentry/Transaction.cs b/src/Sentry/Transaction.cs index 5e571b0df6..3f151bf559 100644 --- a/src/Sentry/Transaction.cs +++ b/src/Sentry/Transaction.cs @@ -1,10 +1,12 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Text.Json; using Sentry.Extensibility; using Sentry.Internal; using Sentry.Internal.Extensions; +using Sentry.Protocol; namespace Sentry { @@ -12,7 +14,7 @@ namespace Sentry /// /// Sentry performance transaction. /// - public class Transaction : ITransactionData, IJsonSerializable, IHasDistribution, IHasTransactionNameSource + public class Transaction : ITransactionData, IJsonSerializable, IHasDistribution, IHasTransactionNameSource, IHasMeasurements { /// /// Transaction's event ID. @@ -184,6 +186,12 @@ public IReadOnlyList Fingerprint /// public IReadOnlyCollection Spans => _spans; + // Not readonly because of deserialization + private Dictionary _measurements = new(); + + /// + public IReadOnlyDictionary Measurements => _measurements; + /// public bool IsFinished => EndTimestamp is not null; @@ -261,6 +269,7 @@ public Transaction(ITransaction tracer) { SampleRate = transactionTracer.SampleRate; DynamicSamplingContext = transactionTracer.DynamicSamplingContext; + _measurements = transactionTracer.Measurements.ToDictionary(); } } @@ -280,6 +289,11 @@ public void SetTag(string key, string value) => public void UnsetTag(string key) => _tags.Remove(key); + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetMeasurement(string name, Measurement measurement) => + _measurements[name] = measurement; + /// public SentryTraceHeader GetTraceHeader() => new( TraceId, @@ -317,6 +331,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger) writer.WriteDictionaryIfNotEmpty("extra", _extra, logger); writer.WriteStringDictionaryIfNotEmpty("tags", _tags!); writer.WriteArrayIfNotEmpty("spans", _spans, logger); + writer.WriteDictionaryIfNotEmpty("measurements", _measurements, logger); writer.WriteEndObject(); } @@ -328,8 +343,8 @@ public static Transaction FromJson(JsonElement json) { var eventId = json.GetPropertyOrNull("event_id")?.Pipe(SentryId.FromJson) ?? SentryId.Empty; var name = json.GetProperty("transaction").GetStringOrThrow(); - var nameSourceValue = json.GetPropertyOrNull("transaction_info")?.GetPropertyOrNull("source")?.GetString(); - var nameSource = nameSourceValue?.ParseEnum() ?? TransactionNameSource.Custom; + var nameSource = json.GetPropertyOrNull("transaction_info")?.GetPropertyOrNull("source")? + .GetString()?.ParseEnum() ?? TransactionNameSource.Custom; var startTimestamp = json.GetProperty("start_timestamp").GetDateTimeOffset(); var endTimestamp = json.GetPropertyOrNull("timestamp")?.GetDateTimeOffset(); var level = json.GetPropertyOrNull("level")?.GetString()?.ParseEnum(); @@ -337,18 +352,22 @@ public static Transaction FromJson(JsonElement json) var release = json.GetPropertyOrNull("release")?.GetString(); var distribution = json.GetPropertyOrNull("dist")?.GetString(); var request = json.GetPropertyOrNull("request")?.Pipe(Request.FromJson); - var contexts = json.GetPropertyOrNull("contexts")?.Pipe(Contexts.FromJson); + var contexts = json.GetPropertyOrNull("contexts")?.Pipe(Contexts.FromJson) ?? new(); 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) - .Pipe(v => new List(v)); - var extra = json.GetPropertyOrNull("extra")?.GetDictionaryOrNull() - ?.ToDictionary(); - var tags = json.GetPropertyOrNull("tags")?.GetStringDictionaryOrNull() - ?.ToDictionary(); + var fingerprint = json.GetPropertyOrNull("fingerprint")? + .EnumerateArray().Select(j => j.GetString()!).ToArray(); + var breadcrumbs = json.GetPropertyOrNull("breadcrumbs")? + .EnumerateArray().Select(Breadcrumb.FromJson).ToList() ?? new(); + var extra = json.GetPropertyOrNull("extra")? + .GetDictionaryOrNull() ?? new(); + var tags = json.GetPropertyOrNull("tags")? + .GetStringDictionaryOrNull()?.WhereNotNullValue().ToDictionary() ?? new(); + var measurements = json.GetPropertyOrNull("measurements")? + .GetDictionaryOrNull(Measurement.FromJson) ?? new(); + var spans = json.GetPropertyOrNull("spans")? + .EnumerateArray().Select(Span.FromJson).ToArray() ?? Array.Empty(); return new Transaction(name, nameSource) { @@ -360,20 +379,19 @@ public static Transaction FromJson(JsonElement json) Release = release, Distribution = distribution, _request = request, - Contexts = contexts ?? new(), + Contexts = contexts, _user = user, Environment = environment, Sdk = sdk, _fingerprint = fingerprint, - _breadcrumbs = breadcrumbs ?? new(), - _extra = extra ?? new(), - _tags = (tags ?? new())!, - _spans = json - .GetPropertyOrNull("spans")? - .EnumerateArray() - .Select(Span.FromJson) - .ToArray() ?? Array.Empty() + _breadcrumbs = breadcrumbs, + _extra = extra, + _tags = tags, + _measurements = measurements, + _spans = spans }; } } } + + diff --git a/src/Sentry/TransactionTracer.cs b/src/Sentry/TransactionTracer.cs index b1a9549a93..abc5b2499b 100644 --- a/src/Sentry/TransactionTracer.cs +++ b/src/Sentry/TransactionTracer.cs @@ -1,15 +1,17 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using Sentry.Internal; +using Sentry.Protocol; namespace Sentry { /// /// Transaction tracer. /// - public class TransactionTracer : ITransaction, IHasDistribution, IHasTransactionNameSource + public class TransactionTracer : ITransaction, IHasDistribution, IHasTransactionNameSource, IHasMeasurements { private readonly IHub _hub; private readonly SentryStopwatch _stopwatch = SentryStopwatch.StartNew(); @@ -165,6 +167,11 @@ public IReadOnlyList Fingerprint /// public IReadOnlyCollection Spans => _spans; + private readonly ConcurrentDictionary _measurements = new(); + + /// + public IReadOnlyDictionary Measurements => _measurements; + /// public bool IsFinished => EndTimestamp is not null; @@ -224,6 +231,11 @@ public void SetTag(string key, string value) => public void UnsetTag(string key) => _tags.TryRemove(key, out _); + /// + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetMeasurement(string name, Measurement measurement) => + _measurements[name] = measurement; + internal ISpan StartChild(SpanId parentSpanId, string operation) { // Limit spans to 1000 From f68cb7aa0c9d93dc5c3523c6b17ba33aea6d3fd7 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:14:53 -0700 Subject: [PATCH 5/9] Add unit tests --- .../Helpers/JsonSerializableExtensions.cs | 4 +- test/Sentry.Tests/MeasurementUnitTests.cs | 110 ++++++ ...ction_Serializes_Measurements.verified.txt | 56 +++ .../Sentry.Tests/Protocol/MeasurementTests.cs | 347 ++++++++++++++++++ .../Sentry.Tests/Protocol/TransactionTests.cs | 3 + 5 files changed, 518 insertions(+), 2 deletions(-) create mode 100644 test/Sentry.Tests/MeasurementUnitTests.cs create mode 100644 test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt create mode 100644 test/Sentry.Tests/Protocol/MeasurementTests.cs diff --git a/test/Sentry.Tests/Helpers/JsonSerializableExtensions.cs b/test/Sentry.Tests/Helpers/JsonSerializableExtensions.cs index 728f40f650..4d46c7c6d7 100644 --- a/test/Sentry.Tests/Helpers/JsonSerializableExtensions.cs +++ b/test/Sentry.Tests/Helpers/JsonSerializableExtensions.cs @@ -3,10 +3,10 @@ internal static class JsonSerializableExtensions { - public static string ToJsonString(this IJsonSerializable serializable, IDiagnosticLogger logger) => + public static string ToJsonString(this IJsonSerializable serializable, IDiagnosticLogger logger = null) => WriteToJsonString(writer => writer.WriteSerializableValue(serializable, logger)); - public static string ToJsonString(this object @object, IDiagnosticLogger logger) => + public static string ToJsonString(this object @object, IDiagnosticLogger logger = null) => WriteToJsonString(writer => writer.WriteDynamicValue(@object, logger)); private static string WriteToJsonString(Action writeAction) diff --git a/test/Sentry.Tests/MeasurementUnitTests.cs b/test/Sentry.Tests/MeasurementUnitTests.cs new file mode 100644 index 0000000000..fb523fb085 --- /dev/null +++ b/test/Sentry.Tests/MeasurementUnitTests.cs @@ -0,0 +1,110 @@ +namespace Sentry.Tests; + +public class MeasurementUnitTests +{ + [Fact] + public void DefaultNone() + { + MeasurementUnit m = new(); + Assert.Equal(MeasurementUnit.None, m); + Assert.Equal("", m.ToString()); + } + + [Fact] + public void CanUseDurationUnits() + { + MeasurementUnit m = MeasurementUnit.Duration.Second; + Assert.Equal("second", m.ToString()); + } + + [Fact] + public void CanUseInformationUnits() + { + MeasurementUnit m = MeasurementUnit.Information.Byte; + Assert.Equal("byte", m.ToString()); + } + + [Fact] + public void CanUseFractionUnits() + { + MeasurementUnit m = MeasurementUnit.Fraction.Percent; + Assert.Equal("percent", m.ToString()); + } + + [Fact] + public void CanUseCustomUnits() + { + var m = MeasurementUnit.Custom("foo"); + Assert.Equal("foo", m.ToString()); + } + + [Fact] + public void ZeroInequality() + { + MeasurementUnit m1 = (MeasurementUnit.Duration)0; + MeasurementUnit m2 = (MeasurementUnit.Information)0; + Assert.NotEqual(m1, m2); + } + + [Fact] + public void ZeroDifferentHashCodes() + { + MeasurementUnit m1 = (MeasurementUnit.Duration)0; + MeasurementUnit m2 = (MeasurementUnit.Information)0; + Assert.NotEqual(m1.GetHashCode(), m2.GetHashCode()); + } + + [Fact] + public void SimpleEquality() + { + MeasurementUnit m1 = MeasurementUnit.Duration.Second; + MeasurementUnit m2 = MeasurementUnit.Duration.Second; + Assert.Equal(m1, m2); + + // we overload the == operator, so check that as well + Assert.True(m1 == m2); + } + + [Fact] + public void SimpleInequality() + { + MeasurementUnit m1 = MeasurementUnit.Duration.Second; + MeasurementUnit m2 = MeasurementUnit.Duration.Millisecond; + Assert.NotEqual(m1, m2); + + // we overload the != operator, so check that as well + Assert.True(m1 != m2); + } + + [Fact] + public void MixedInequality() + { + MeasurementUnit m1 = MeasurementUnit.Duration.Nanosecond; + MeasurementUnit m2 = MeasurementUnit.Information.Bit; + Assert.NotEqual(m1, m2); + } + + [Fact] + public void CustomEquality() + { + var m1 = MeasurementUnit.Custom("foo"); + var m2 = MeasurementUnit.Custom("foo"); + Assert.Equal(m1, m2); + } + + [Fact] + public void CustomInequality() + { + var m1 = MeasurementUnit.Custom("foo"); + var m2 = MeasurementUnit.Custom("bar"); + Assert.NotEqual(m1, m2); + } + + [Fact] + public void MixedInequalityWithCustom() + { + var m1 = MeasurementUnit.Custom("second"); + var m2 = MeasurementUnit.Duration.Second; + Assert.NotEqual(m1, m2); + } +} diff --git a/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt b/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt new file mode 100644 index 0000000000..1653c6e930 --- /dev/null +++ b/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt @@ -0,0 +1,56 @@ +{ + contexts: { + trace: { + op: operation, + trace_id: Guid_1, + type: trace + } + }, + event_id: Guid_2, + measurements: { + a: { + value: 2147483647 + }, + b: { + unit: second, + value: 2147483647 + }, + c: { + value: 9223372036854775807 + }, + d: { + unit: kilobyte, + value: 9223372036854775807 + }, + e: { + value: 18446744073709551615 + }, + f: { + unit: exbibyte, + value: 18446744073709551615 + }, + g: { + value: 1.7976931348623157E+308 + }, + h: { + unit: foo, + value: 1.7976931348623157E+308 + }, + i: { + unit: ratio, + value: 0.5 + }, + j: { + unit: percent, + value: 88.25 + } + }, + platform: csharp, + sdk: {}, + start_timestamp: DateTime_1, + transaction: name, + transaction_info: { + source: custom + }, + type: transaction +} \ No newline at end of file diff --git a/test/Sentry.Tests/Protocol/MeasurementTests.cs b/test/Sentry.Tests/Protocol/MeasurementTests.cs new file mode 100644 index 0000000000..2b3795fe1d --- /dev/null +++ b/test/Sentry.Tests/Protocol/MeasurementTests.cs @@ -0,0 +1,347 @@ +namespace Sentry.Tests.Protocol; + +[UsesVerify] +public class MeasurementTests +{ + [Fact] + public void Constructor_IntValue() + { + var m = new Measurement(int.MaxValue); + Assert.Equal(int.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.None, m.Unit); + } + + [Fact] + public void Constructor_LongValue() + { + var m = new Measurement(long.MaxValue); + Assert.Equal(long.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.None, m.Unit); + } + + [Fact] + public void Constructor_ULongValue() + { + var m = new Measurement(ulong.MaxValue); + Assert.Equal(ulong.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.None, m.Unit); + } + + [Fact] + public void Constructor_DoubleValue() + { + var m = new Measurement(double.MaxValue); + Assert.Equal(double.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.None, m.Unit); + } + + [Fact] + public void Constructor_IntValue_WithUnit() + { + var m = new Measurement(int.MaxValue, MeasurementUnit.Duration.Second); + Assert.Equal(int.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.Duration.Second, m.Unit); + } + + [Fact] + public void Constructor_LongValue_WithUnit() + { + var m = new Measurement(long.MaxValue, MeasurementUnit.Duration.Second); + Assert.Equal(long.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.Duration.Second, m.Unit); + } + + [Fact] + public void Constructor_ULongValue_WithUnit() + { + var m = new Measurement(ulong.MaxValue, MeasurementUnit.Duration.Second); + Assert.Equal(ulong.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.Duration.Second, m.Unit); + } + + [Fact] + public void Constructor_DoubleValue_WithUnit() + { + var m = new Measurement(double.MaxValue, MeasurementUnit.Duration.Second); + Assert.Equal(double.MaxValue, m.Value); + Assert.Equal(MeasurementUnit.Duration.Second, m.Unit); + } + + [Fact] + public void Json_IntValue() + { + var m = new Measurement(int.MaxValue); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":2147483647}", json); + } + + [Fact] + public void Json_LongValue() + { + var m = new Measurement(long.MaxValue); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":9223372036854775807}", json); + } + + [Fact] + public void Json_ULongValue() + { + var m = new Measurement(ulong.MaxValue); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":18446744073709551615}", json); + } + + [Fact] + public void Json_DoubleValue() + { + var m = new Measurement(double.MaxValue); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":1.7976931348623157E+308}", json); + } + + [Fact] + public void Json_IntValue_WithUnit() + { + var m = new Measurement(int.MaxValue, MeasurementUnit.Duration.Second); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":2147483647,\"unit\":\"second\"}", json); + } + + [Fact] + public void Json_LongValue_WithUnit() + { + var m = new Measurement(long.MaxValue, MeasurementUnit.Duration.Second); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":9223372036854775807,\"unit\":\"second\"}", json); + } + + [Fact] + public void Json_ULongValue_WithUnit() + { + var m = new Measurement(ulong.MaxValue, MeasurementUnit.Duration.Second); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":18446744073709551615,\"unit\":\"second\"}", json); + } + + [Fact] + public void Json_DoubleValue_WithUnit() + { + var m = new Measurement(double.MaxValue, MeasurementUnit.Duration.Second); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":1.7976931348623157E+308,\"unit\":\"second\"}", json); + } + + [Fact] + public void Transaction_SetMeasurement_IntValue() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", int.MaxValue); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(int.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + } + + [Fact] + public void Transaction_SetMeasurement_LongValue() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", long.MaxValue); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(long.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + } + + [Fact] + public void Transaction_SetMeasurement_ULongValue() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", ulong.MaxValue); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(ulong.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + } + + [Fact] + public void Transaction_SetMeasurement_DoubleValue() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", double.MaxValue); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(double.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + } + + [Fact] + public void Transaction_SetMeasurement_IntValue_WithUnit() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", int.MaxValue, MeasurementUnit.Duration.Nanosecond); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(int.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.Duration.Nanosecond))); + } + + [Fact] + public void Transaction_SetMeasurement_LongValue_WithUnit() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", long.MaxValue, MeasurementUnit.Duration.Nanosecond); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(long.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.Duration.Nanosecond))); + } + + [Fact] + public void Transaction_SetMeasurement_ULongValue_WithUnit() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", ulong.MaxValue, MeasurementUnit.Duration.Nanosecond); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(ulong.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.Duration.Nanosecond))); + } + + [Fact] + public void Transaction_SetMeasurement_DoubleValue_WithUnit() + { + // Arrange + var client = Substitute.For(); + var options = new SentryOptions + { + Dsn = ValidDsn, + TracesSampleRate = 1.0 + }; + var hub = new Hub(options, client); + var transaction = hub.StartTransaction("name", "operation"); + + // Act + transaction.SetMeasurement("foo", double.MaxValue, MeasurementUnit.Duration.Nanosecond); + transaction.Finish(); + + // Assert + client.Received(1).CaptureTransaction(Arg.Is(t => + t.Measurements.Count == 1 && + t.Measurements["foo"].Value.Equals(double.MaxValue) && + t.Measurements["foo"].Unit.Equals(MeasurementUnit.Duration.Nanosecond))); + } + + [Fact] + [Trait("Category", "Verify")] + public Task Transaction_Serializes_Measurements() + { + var transaction = new Transaction("name", "operation"); + transaction.Contexts.Trace.SpanId = SpanId.Empty; + + transaction.SetMeasurement("a", int.MaxValue); + transaction.SetMeasurement("b", int.MaxValue, MeasurementUnit.Duration.Second); + transaction.SetMeasurement("c", long.MaxValue); + transaction.SetMeasurement("d", long.MaxValue, MeasurementUnit.Information.Kilobyte); + transaction.SetMeasurement("e", ulong.MaxValue); + transaction.SetMeasurement("f", ulong.MaxValue, MeasurementUnit.Information.Exbibyte); + transaction.SetMeasurement("g", double.MaxValue); + transaction.SetMeasurement("h", double.MaxValue, MeasurementUnit.Custom("foo")); + transaction.SetMeasurement("i", 0.5, MeasurementUnit.Fraction.Ratio); + transaction.SetMeasurement("j", 88.25, MeasurementUnit.Fraction.Percent); + + var json = transaction.ToJsonString(); + return VerifyJson(json); + } +} diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs index 8881a6f641..1bb2fe0fef 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -66,6 +66,9 @@ public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() transaction.SetExtra("extra_key", "extra_value"); transaction.Fingerprint = new[] { "fingerprint" }; transaction.SetTag("tag_key", "tag_value"); + transaction.SetMeasurement("measurement_1", 111); + transaction.SetMeasurement("measurement_2", 2.34, MeasurementUnit.Custom("things")); + transaction.SetMeasurement("measurement_3", 333, MeasurementUnit.Information.Terabyte); var child1 = transaction.StartChild("child_op123", "child_desc123"); child1.Status = SpanStatus.Unimplemented; From 2135ca314b6155cd07cdf6733594c53f6630f904 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:15:26 -0700 Subject: [PATCH 6/9] Update public API verifications --- .../ApiApprovalTests.Run.Core3_1.verified.txt | 66 +++++++++++++++++++ ...piApprovalTests.Run.DotNet4_8.verified.txt | 66 +++++++++++++++++++ ...piApprovalTests.Run.DotNet6_0.verified.txt | 66 +++++++++++++++++++ 3 files changed, 198 insertions(+) diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt index 7a5eb85da0..0c39d6ab18 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt @@ -276,6 +276,61 @@ namespace Sentry string Name { get; } } public interface ITransactionData : Sentry.IEventLike, Sentry.IHasBreadcrumbs, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanContext, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { } + public static class MeasurementExtensions + { + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, double value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, int value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, long value, Sentry.MeasurementUnit unit = default) { } + [System.CLSCompliant(false)] + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, ulong value, Sentry.MeasurementUnit unit = default) { } + } + public readonly struct MeasurementUnit : System.IEquatable + { + public static Sentry.MeasurementUnit None; + public bool Equals(Sentry.MeasurementUnit other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public static Sentry.MeasurementUnit Custom(string name) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Duration unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Fraction unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Information unit) { } + public static bool operator !=(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public static bool operator ==(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public enum Duration + { + Nanosecond = 0, + Microsecond = 1, + Millisecond = 2, + Second = 3, + Minute = 4, + Hour = 5, + Day = 6, + Week = 7, + } + public enum Fraction + { + Ratio = 0, + Percent = 1, + } + public enum Information + { + Bit = 0, + Byte = 1, + Kilobyte = 2, + Kibibyte = 3, + Megabyte = 4, + Mebibyte = 5, + Gigabyte = 6, + Gibibyte = 7, + Terabyte = 8, + Tebibyte = 9, + Petabyte = 10, + Pebibyte = 11, + Exabyte = 12, + Exbibyte = 13, + } + } public sealed class Package : Sentry.IJsonSerializable { public Package(string name, string version) { } @@ -841,6 +896,7 @@ namespace Sentry public bool? IsParentSampled { get; set; } public bool? IsSampled { get; } public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } public string Name { get; } public Sentry.TransactionNameSource NameSource { get; } public string Operation { get; } @@ -860,6 +916,7 @@ namespace Sentry public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } public Sentry.SentryTraceHeader GetTraceHeader() { } public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } public void SetTag(string key, string value) { } public void UnsetTag(string key) { } public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } @@ -911,6 +968,7 @@ namespace Sentry public bool? IsParentSampled { get; set; } public bool? IsSampled { get; } public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } public string Name { get; set; } public Sentry.TransactionNameSource NameSource { get; set; } public string Operation { get; set; } @@ -935,6 +993,7 @@ namespace Sentry public Sentry.ISpan? GetLastActiveSpan() { } public Sentry.SentryTraceHeader GetTraceHeader() { } public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } public void SetTag(string key, string value) { } public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } @@ -1355,6 +1414,13 @@ namespace Sentry.Protocol Sentry.SpanStatus? Status { get; } Sentry.SentryId TraceId { get; } } + public sealed class Measurement : Sentry.IJsonSerializable + { + public Sentry.MeasurementUnit Unit { get; } + public object Value { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Measurement FromJson(System.Text.Json.JsonElement json) { } + } public sealed class Mechanism : Sentry.IJsonSerializable { public static readonly string HandledKey; diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt index 2376f3aec1..1e9ac606a6 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet4_8.verified.txt @@ -275,6 +275,61 @@ namespace Sentry string Name { get; } } public interface ITransactionData : Sentry.IEventLike, Sentry.IHasBreadcrumbs, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanContext, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { } + public static class MeasurementExtensions + { + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, double value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, int value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, long value, Sentry.MeasurementUnit unit = default) { } + [System.CLSCompliant(false)] + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, ulong value, Sentry.MeasurementUnit unit = default) { } + } + public readonly struct MeasurementUnit : System.IEquatable + { + public static Sentry.MeasurementUnit None; + public bool Equals(Sentry.MeasurementUnit other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public static Sentry.MeasurementUnit Custom(string name) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Duration unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Fraction unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Information unit) { } + public static bool operator !=(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public static bool operator ==(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public enum Duration + { + Nanosecond = 0, + Microsecond = 1, + Millisecond = 2, + Second = 3, + Minute = 4, + Hour = 5, + Day = 6, + Week = 7, + } + public enum Fraction + { + Ratio = 0, + Percent = 1, + } + public enum Information + { + Bit = 0, + Byte = 1, + Kilobyte = 2, + Kibibyte = 3, + Megabyte = 4, + Mebibyte = 5, + Gigabyte = 6, + Gibibyte = 7, + Terabyte = 8, + Tebibyte = 9, + Petabyte = 10, + Pebibyte = 11, + Exabyte = 12, + Exbibyte = 13, + } + } public sealed class Package : Sentry.IJsonSerializable { public Package(string name, string version) { } @@ -840,6 +895,7 @@ namespace Sentry public bool? IsParentSampled { get; set; } public bool? IsSampled { get; } public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } public string Name { get; } public Sentry.TransactionNameSource NameSource { get; } public string Operation { get; } @@ -859,6 +915,7 @@ namespace Sentry public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } public Sentry.SentryTraceHeader GetTraceHeader() { } public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } public void SetTag(string key, string value) { } public void UnsetTag(string key) { } public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } @@ -910,6 +967,7 @@ namespace Sentry public bool? IsParentSampled { get; set; } public bool? IsSampled { get; } public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } public string Name { get; set; } public Sentry.TransactionNameSource NameSource { get; set; } public string Operation { get; set; } @@ -934,6 +992,7 @@ namespace Sentry public Sentry.ISpan? GetLastActiveSpan() { } public Sentry.SentryTraceHeader GetTraceHeader() { } public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } public void SetTag(string key, string value) { } public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } @@ -1355,6 +1414,13 @@ namespace Sentry.Protocol Sentry.SpanStatus? Status { get; } Sentry.SentryId TraceId { get; } } + public sealed class Measurement : Sentry.IJsonSerializable + { + public Sentry.MeasurementUnit Unit { get; } + public object Value { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Measurement FromJson(System.Text.Json.JsonElement json) { } + } public sealed class Mechanism : Sentry.IJsonSerializable { public static readonly string HandledKey; diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 7a5eb85da0..0c39d6ab18 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -276,6 +276,61 @@ namespace Sentry string Name { get; } } public interface ITransactionData : Sentry.IEventLike, Sentry.IHasBreadcrumbs, Sentry.IHasExtra, Sentry.IHasTags, Sentry.ISpanContext, Sentry.ISpanData, Sentry.ITransactionContext, Sentry.Protocol.ITraceContext { } + public static class MeasurementExtensions + { + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, double value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, int value, Sentry.MeasurementUnit unit = default) { } + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, long value, Sentry.MeasurementUnit unit = default) { } + [System.CLSCompliant(false)] + public static void SetMeasurement(this Sentry.ITransactionData transaction, string name, ulong value, Sentry.MeasurementUnit unit = default) { } + } + public readonly struct MeasurementUnit : System.IEquatable + { + public static Sentry.MeasurementUnit None; + public bool Equals(Sentry.MeasurementUnit other) { } + public override bool Equals(object? obj) { } + public override int GetHashCode() { } + public override string ToString() { } + public static Sentry.MeasurementUnit Custom(string name) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Duration unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Fraction unit) { } + public static Sentry.MeasurementUnit op_Implicit(Sentry.MeasurementUnit.Information unit) { } + public static bool operator !=(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public static bool operator ==(Sentry.MeasurementUnit left, Sentry.MeasurementUnit right) { } + public enum Duration + { + Nanosecond = 0, + Microsecond = 1, + Millisecond = 2, + Second = 3, + Minute = 4, + Hour = 5, + Day = 6, + Week = 7, + } + public enum Fraction + { + Ratio = 0, + Percent = 1, + } + public enum Information + { + Bit = 0, + Byte = 1, + Kilobyte = 2, + Kibibyte = 3, + Megabyte = 4, + Mebibyte = 5, + Gigabyte = 6, + Gibibyte = 7, + Terabyte = 8, + Tebibyte = 9, + Petabyte = 10, + Pebibyte = 11, + Exabyte = 12, + Exbibyte = 13, + } + } public sealed class Package : Sentry.IJsonSerializable { public Package(string name, string version) { } @@ -841,6 +896,7 @@ namespace Sentry public bool? IsParentSampled { get; set; } public bool? IsSampled { get; } public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } public string Name { get; } public Sentry.TransactionNameSource NameSource { get; } public string Operation { get; } @@ -860,6 +916,7 @@ namespace Sentry public void AddBreadcrumb(Sentry.Breadcrumb breadcrumb) { } public Sentry.SentryTraceHeader GetTraceHeader() { } public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } public void SetTag(string key, string value) { } public void UnsetTag(string key) { } public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } @@ -911,6 +968,7 @@ namespace Sentry public bool? IsParentSampled { get; set; } public bool? IsSampled { get; } public Sentry.SentryLevel? Level { get; set; } + public System.Collections.Generic.IReadOnlyDictionary Measurements { get; } public string Name { get; set; } public Sentry.TransactionNameSource NameSource { get; set; } public string Operation { get; set; } @@ -935,6 +993,7 @@ namespace Sentry public Sentry.ISpan? GetLastActiveSpan() { } public Sentry.SentryTraceHeader GetTraceHeader() { } public void SetExtra(string key, object? value) { } + public void SetMeasurement(string name, Sentry.Protocol.Measurement measurement) { } public void SetTag(string key, string value) { } public Sentry.ISpan StartChild(string operation) { } public void UnsetTag(string key) { } @@ -1355,6 +1414,13 @@ namespace Sentry.Protocol Sentry.SpanStatus? Status { get; } Sentry.SentryId TraceId { get; } } + public sealed class Measurement : Sentry.IJsonSerializable + { + public Sentry.MeasurementUnit Unit { get; } + public object Value { get; } + public void WriteTo(System.Text.Json.Utf8JsonWriter writer, Sentry.Extensibility.IDiagnosticLogger? logger) { } + public static Sentry.Protocol.Measurement FromJson(System.Text.Json.JsonElement json) { } + } public sealed class Mechanism : Sentry.IJsonSerializable { public static readonly string HandledKey; From 16f3500469367758e7581848cc9349ed6b5e0b15 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Tue, 25 Oct 2022 22:17:25 -0700 Subject: [PATCH 7/9] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 838a780377..ad573b929b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Support DI for custom transaction processors ([#1993](https://github.com/getsentry/sentry-dotnet/pull/1993)) - Mark Transaction as aborted when unhandled exception occurs ([#1996](https://github.com/getsentry/sentry-dotnet/pull/1996)) - Build Windows and Tizen targets for `Sentry.Maui` ([#2005](https://github.com/getsentry/sentry-dotnet/pull/2005)) +- Add Custom Measurements API ([#2013](https://github.com/getsentry/sentry-dotnet/pull/2013)) ### Fixes From 209cf46d9d44a21c82cdfd6f98fe52d0557099ad Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Wed, 26 Oct 2022 07:45:34 -0700 Subject: [PATCH 8/9] Add json tests with None --- .../Sentry.Tests/Protocol/MeasurementTests.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/test/Sentry.Tests/Protocol/MeasurementTests.cs b/test/Sentry.Tests/Protocol/MeasurementTests.cs index 2b3795fe1d..6696ba10bf 100644 --- a/test/Sentry.Tests/Protocol/MeasurementTests.cs +++ b/test/Sentry.Tests/Protocol/MeasurementTests.cs @@ -99,6 +99,38 @@ public void Json_DoubleValue() Assert.Equal("{\"value\":1.7976931348623157E+308}", json); } + [Fact] + public void Json_IntValue_WithNone() + { + var m = new Measurement(int.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":2147483647}", json); + } + + [Fact] + public void Json_LongValue_WithNone() + { + var m = new Measurement(long.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":9223372036854775807}", json); + } + + [Fact] + public void Json_ULongValue_WithNone() + { + var m = new Measurement(ulong.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":18446744073709551615}", json); + } + + [Fact] + public void Json_DoubleValue_WithNone() + { + var m = new Measurement(double.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":1.7976931348623157E+308}", json); + } + [Fact] public void Json_IntValue_WithUnit() { From 126ca7667d1fc5678f8b54be01ab3d7d66bb6b88 Mon Sep 17 00:00:00 2001 From: Matt Johnson-Pint Date: Thu, 27 Oct 2022 00:50:47 -0700 Subject: [PATCH 9/9] Separate "none" from "" --- src/Sentry/IHasMeasurements.cs | 4 +-- src/Sentry/MeasurementUnit.cs | 11 +++++--- test/Sentry.Tests/MeasurementUnitTests.cs | 17 ++++++++++-- ...ction_Serializes_Measurements.verified.txt | 4 +++ .../Sentry.Tests/Protocol/MeasurementTests.cs | 27 ++++++++++--------- .../Sentry.Tests/Protocol/TransactionTests.cs | 1 + 6 files changed, 44 insertions(+), 20 deletions(-) diff --git a/src/Sentry/IHasMeasurements.cs b/src/Sentry/IHasMeasurements.cs index b9f4cf4e3a..ec5fab8003 100644 --- a/src/Sentry/IHasMeasurements.cs +++ b/src/Sentry/IHasMeasurements.cs @@ -40,9 +40,7 @@ public static class MeasurementExtensions /// The transaction. /// The name of the measurement. /// The value of the measurement. - /// - /// The optional unit of the measurement. Defaults to . - /// + /// The optional unit of the measurement. public static void SetMeasurement(this ITransactionData transaction, string name, int value, MeasurementUnit unit = default) => (transaction as IHasMeasurements)?.SetMeasurement(name, new Measurement(value, unit)); diff --git a/src/Sentry/MeasurementUnit.cs b/src/Sentry/MeasurementUnit.cs index 364009d5f4..47f5beb8aa 100644 --- a/src/Sentry/MeasurementUnit.cs +++ b/src/Sentry/MeasurementUnit.cs @@ -24,9 +24,9 @@ private MeasurementUnit(string name) } /// - /// Represents an untyped measurement unit, used for measurements that have no natural unit. + /// A special measurement unit that is used for measurements that have no natural unit. /// - public static MeasurementUnit None = new(); + public static MeasurementUnit None = new("none"); /// /// Creates a custom measurement unit. @@ -39,12 +39,17 @@ internal static MeasurementUnit Parse(string? name) { if (name == null) { - return None; + return new MeasurementUnit(); } name = name.Trim(); if (name.Length == 0) + { + return new MeasurementUnit(); + } + + if (name.Equals("none", StringComparison.OrdinalIgnoreCase)) { return None; } diff --git a/test/Sentry.Tests/MeasurementUnitTests.cs b/test/Sentry.Tests/MeasurementUnitTests.cs index fb523fb085..fad3b2ea1f 100644 --- a/test/Sentry.Tests/MeasurementUnitTests.cs +++ b/test/Sentry.Tests/MeasurementUnitTests.cs @@ -3,13 +3,26 @@ namespace Sentry.Tests; public class MeasurementUnitTests { [Fact] - public void DefaultNone() + public void DefaultEmpty() { MeasurementUnit m = new(); - Assert.Equal(MeasurementUnit.None, m); Assert.Equal("", m.ToString()); } + [Fact] + public void NoneDiffersFromEmpty() + { + MeasurementUnit m = new(); + Assert.NotEqual(MeasurementUnit.None, m); + } + + [Fact] + public void CanUseNoneUnit() + { + var m = MeasurementUnit.None; + Assert.Equal("none", m.ToString()); + } + [Fact] public void CanUseDurationUnits() { diff --git a/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt b/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt index 1653c6e930..70eb7ae216 100644 --- a/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt +++ b/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt @@ -43,6 +43,10 @@ j: { unit: percent, value: 88.25 + }, + _: { + unit: none, + value: 0 } }, platform: csharp, diff --git a/test/Sentry.Tests/Protocol/MeasurementTests.cs b/test/Sentry.Tests/Protocol/MeasurementTests.cs index 6696ba10bf..d9e469d8db 100644 --- a/test/Sentry.Tests/Protocol/MeasurementTests.cs +++ b/test/Sentry.Tests/Protocol/MeasurementTests.cs @@ -3,12 +3,14 @@ namespace Sentry.Tests.Protocol; [UsesVerify] public class MeasurementTests { + private static readonly MeasurementUnit EmptyUnit = new(); + [Fact] public void Constructor_IntValue() { var m = new Measurement(int.MaxValue); Assert.Equal(int.MaxValue, m.Value); - Assert.Equal(MeasurementUnit.None, m.Unit); + Assert.Equal(EmptyUnit, m.Unit); } [Fact] @@ -16,7 +18,7 @@ public void Constructor_LongValue() { var m = new Measurement(long.MaxValue); Assert.Equal(long.MaxValue, m.Value); - Assert.Equal(MeasurementUnit.None, m.Unit); + Assert.Equal(EmptyUnit, m.Unit); } [Fact] @@ -24,7 +26,7 @@ public void Constructor_ULongValue() { var m = new Measurement(ulong.MaxValue); Assert.Equal(ulong.MaxValue, m.Value); - Assert.Equal(MeasurementUnit.None, m.Unit); + Assert.Equal(EmptyUnit, m.Unit); } [Fact] @@ -32,7 +34,7 @@ public void Constructor_DoubleValue() { var m = new Measurement(double.MaxValue); Assert.Equal(double.MaxValue, m.Value); - Assert.Equal(MeasurementUnit.None, m.Unit); + Assert.Equal(EmptyUnit, m.Unit); } [Fact] @@ -104,7 +106,7 @@ public void Json_IntValue_WithNone() { var m = new Measurement(int.MaxValue, MeasurementUnit.None); var json = m.ToJsonString(); - Assert.Equal("{\"value\":2147483647}", json); + Assert.Equal("{\"value\":2147483647,\"unit\":\"none\"}", json); } [Fact] @@ -112,7 +114,7 @@ public void Json_LongValue_WithNone() { var m = new Measurement(long.MaxValue, MeasurementUnit.None); var json = m.ToJsonString(); - Assert.Equal("{\"value\":9223372036854775807}", json); + Assert.Equal("{\"value\":9223372036854775807,\"unit\":\"none\"}", json); } [Fact] @@ -120,7 +122,7 @@ public void Json_ULongValue_WithNone() { var m = new Measurement(ulong.MaxValue, MeasurementUnit.None); var json = m.ToJsonString(); - Assert.Equal("{\"value\":18446744073709551615}", json); + Assert.Equal("{\"value\":18446744073709551615,\"unit\":\"none\"}", json); } [Fact] @@ -128,7 +130,7 @@ public void Json_DoubleValue_WithNone() { var m = new Measurement(double.MaxValue, MeasurementUnit.None); var json = m.ToJsonString(); - Assert.Equal("{\"value\":1.7976931348623157E+308}", json); + Assert.Equal("{\"value\":1.7976931348623157E+308,\"unit\":\"none\"}", json); } [Fact] @@ -184,7 +186,7 @@ public void Transaction_SetMeasurement_IntValue() client.Received(1).CaptureTransaction(Arg.Is(t => t.Measurements.Count == 1 && t.Measurements["foo"].Value.Equals(int.MaxValue) && - t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + t.Measurements["foo"].Unit.Equals(EmptyUnit))); } [Fact] @@ -208,7 +210,7 @@ public void Transaction_SetMeasurement_LongValue() client.Received(1).CaptureTransaction(Arg.Is(t => t.Measurements.Count == 1 && t.Measurements["foo"].Value.Equals(long.MaxValue) && - t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + t.Measurements["foo"].Unit.Equals(EmptyUnit))); } [Fact] @@ -232,7 +234,7 @@ public void Transaction_SetMeasurement_ULongValue() client.Received(1).CaptureTransaction(Arg.Is(t => t.Measurements.Count == 1 && t.Measurements["foo"].Value.Equals(ulong.MaxValue) && - t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + t.Measurements["foo"].Unit.Equals(EmptyUnit))); } [Fact] @@ -256,7 +258,7 @@ public void Transaction_SetMeasurement_DoubleValue() client.Received(1).CaptureTransaction(Arg.Is(t => t.Measurements.Count == 1 && t.Measurements["foo"].Value.Equals(double.MaxValue) && - t.Measurements["foo"].Unit.Equals(MeasurementUnit.None))); + t.Measurements["foo"].Unit.Equals(EmptyUnit))); } [Fact] @@ -362,6 +364,7 @@ public Task Transaction_Serializes_Measurements() var transaction = new Transaction("name", "operation"); transaction.Contexts.Trace.SpanId = SpanId.Empty; + transaction.SetMeasurement("_", 0, MeasurementUnit.None); transaction.SetMeasurement("a", int.MaxValue); transaction.SetMeasurement("b", int.MaxValue, MeasurementUnit.Duration.Second); transaction.SetMeasurement("c", long.MaxValue); diff --git a/test/Sentry.Tests/Protocol/TransactionTests.cs b/test/Sentry.Tests/Protocol/TransactionTests.cs index 2ca3158ca0..48d2fc7ea7 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -69,6 +69,7 @@ public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject() transaction.SetMeasurement("measurement_1", 111); transaction.SetMeasurement("measurement_2", 2.34, MeasurementUnit.Custom("things")); transaction.SetMeasurement("measurement_3", 333, MeasurementUnit.Information.Terabyte); + transaction.SetMeasurement("measurement_4", 0, MeasurementUnit.None); var child1 = transaction.StartChild("child_op123", "child_desc123"); child1.Status = SpanStatus.Unimplemented;