diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e202b4e15..99c8d7c897 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)) - Add `ISpan.GetTransaction` convenience method ([#2014](https://github.com/getsentry/sentry-dotnet/pull/2014)) ### Fixes diff --git a/src/Sentry/IHasMeasurements.cs b/src/Sentry/IHasMeasurements.cs new file mode 100644 index 0000000000..ec5fab8003 --- /dev/null +++ b/src/Sentry/IHasMeasurements.cs @@ -0,0 +1,67 @@ +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. + 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/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); } } 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 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..47f5beb8aa --- /dev/null +++ b/src/Sentry/MeasurementUnit.cs @@ -0,0 +1,99 @@ +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; + } + + /// + /// A special measurement unit that is used for measurements that have no natural unit. + /// + public static MeasurementUnit None = new("none"); + + /// + /// 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 new MeasurementUnit(); + } + + name = name.Trim(); + + if (name.Length == 0) + { + return new MeasurementUnit(); + } + + if (name.Equals("none", StringComparison.OrdinalIgnoreCase)) + { + 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)); + } + } +} 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 diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Core3_1.verified.txt index 6cb2f37fff..f72013fa90 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) { } @@ -842,6 +897,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; } @@ -861,6 +917,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) { } @@ -912,6 +969,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; } @@ -936,6 +994,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) { } @@ -1356,6 +1415,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 a8a67cf7e0..b67daed0ba 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) { } @@ -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) { } @@ -1356,6 +1415,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 6cb2f37fff..f72013fa90 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) { } @@ -842,6 +897,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; } @@ -861,6 +917,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) { } @@ -912,6 +969,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; } @@ -936,6 +994,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) { } @@ -1356,6 +1415,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/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..fad3b2ea1f --- /dev/null +++ b/test/Sentry.Tests/MeasurementUnitTests.cs @@ -0,0 +1,123 @@ +namespace Sentry.Tests; + +public class MeasurementUnitTests +{ + [Fact] + public void DefaultEmpty() + { + MeasurementUnit m = new(); + 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() + { + 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..70eb7ae216 --- /dev/null +++ b/test/Sentry.Tests/Protocol/MeasurementTests.Transaction_Serializes_Measurements.verified.txt @@ -0,0 +1,60 @@ +{ + 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 + }, + _: { + unit: none, + value: 0 + } + }, + 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..d9e469d8db --- /dev/null +++ b/test/Sentry.Tests/Protocol/MeasurementTests.cs @@ -0,0 +1,382 @@ +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(EmptyUnit, m.Unit); + } + + [Fact] + public void Constructor_LongValue() + { + var m = new Measurement(long.MaxValue); + Assert.Equal(long.MaxValue, m.Value); + Assert.Equal(EmptyUnit, m.Unit); + } + + [Fact] + public void Constructor_ULongValue() + { + var m = new Measurement(ulong.MaxValue); + Assert.Equal(ulong.MaxValue, m.Value); + Assert.Equal(EmptyUnit, m.Unit); + } + + [Fact] + public void Constructor_DoubleValue() + { + var m = new Measurement(double.MaxValue); + Assert.Equal(double.MaxValue, m.Value); + Assert.Equal(EmptyUnit, 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_WithNone() + { + var m = new Measurement(int.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":2147483647,\"unit\":\"none\"}", json); + } + + [Fact] + public void Json_LongValue_WithNone() + { + var m = new Measurement(long.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":9223372036854775807,\"unit\":\"none\"}", json); + } + + [Fact] + public void Json_ULongValue_WithNone() + { + var m = new Measurement(ulong.MaxValue, MeasurementUnit.None); + var json = m.ToJsonString(); + Assert.Equal("{\"value\":18446744073709551615,\"unit\":\"none\"}", 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,\"unit\":\"none\"}", 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(EmptyUnit))); + } + + [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(EmptyUnit))); + } + + [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(EmptyUnit))); + } + + [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(EmptyUnit))); + } + + [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("_", 0, MeasurementUnit.None); + 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 462fd10915..48d2fc7ea7 100644 --- a/test/Sentry.Tests/Protocol/TransactionTests.cs +++ b/test/Sentry.Tests/Protocol/TransactionTests.cs @@ -66,6 +66,10 @@ 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); + transaction.SetMeasurement("measurement_4", 0, MeasurementUnit.None); var child1 = transaction.StartChild("child_op123", "child_desc123"); child1.Status = SpanStatus.Unimplemented;