diff --git a/CHANGELOG.md b/CHANGELOG.md index 6522855e4b..7c20446038 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Feature + +- Support for [Spotlight](https://spotlightjs.com/). Debug tool for local development. ([#2961](https://github.com/getsentry/sentry-dotnet/pull/2961)) + - Enable it with option `EnableSpotlight` + - Optionally configure the URL to connect via `SpotlightUrl`. Defaults to `http://localhost:8969/stream`. + ### Fixes - Stop Sentry for MacCatalyst from creating `default.profraw` in the app bundle using xcodebuild archive to build sentry-cocoa ([#2960](https://github.com/getsentry/sentry-dotnet/pull/2960)) diff --git a/src/Sentry/BindableSentryOptions.cs b/src/Sentry/BindableSentryOptions.cs index 92360933ae..0964b5ebad 100644 --- a/src/Sentry/BindableSentryOptions.cs +++ b/src/Sentry/BindableSentryOptions.cs @@ -47,6 +47,8 @@ internal partial class BindableSentryOptions public bool? AutoSessionTracking { get; set; } public bool? UseAsyncFileIO { get; set; } public bool? JsonPreserveReferences { get; set; } + public bool? EnableSpotlight { get; set; } + public string? SpotlightUrl { get; set; } public void ApplyTo(SentryOptions options) { @@ -90,6 +92,8 @@ public void ApplyTo(SentryOptions options) options.AutoSessionTracking = AutoSessionTracking ?? options.AutoSessionTracking; options.UseAsyncFileIO = UseAsyncFileIO ?? options.UseAsyncFileIO; options.JsonPreserveReferences = JsonPreserveReferences ?? options.JsonPreserveReferences; + options.EnableSpotlight = EnableSpotlight ?? options.EnableSpotlight; + options.SpotlightUrl = SpotlightUrl ?? options.SpotlightUrl; #if ANDROID Android.ApplyTo(options.Android); diff --git a/src/Sentry/Extensibility/DiagnosticLoggerExtensions.cs b/src/Sentry/Extensibility/DiagnosticLoggerExtensions.cs index 579364db9f..a0862dc122 100644 --- a/src/Sentry/Extensibility/DiagnosticLoggerExtensions.cs +++ b/src/Sentry/Extensibility/DiagnosticLoggerExtensions.cs @@ -58,6 +58,17 @@ internal static void LogDebug( TArg2 arg2) => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Debug, null, message, arg, arg2); + /// + /// Log a debug message. + /// + internal static void LogDebug( + this SentryOptions options, + string message, + TArg arg, + TArg2 arg2, + TArg3 arg3) + => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Debug, null, message, arg, arg2, arg3); + /// /// Log a debug message. /// @@ -206,6 +217,17 @@ internal static void LogWarning( TArg2 arg2) => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Warning, null, message, arg, arg2); + /// + /// Log a warning message. + /// + internal static void LogWarning( + this SentryOptions options, + string message, + TArg arg, + TArg2 arg2, + TArg3 arg3) + => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Warning, null, message, arg, arg2, arg3); + /// /// Log a error message. /// @@ -300,6 +322,29 @@ internal static void LogError(this SentryOptions opti TArg4 arg4) => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Error, exception, message, arg, arg2, arg3, arg4); + /// + /// Log a error message. + /// + internal static void LogError(this SentryOptions options, + string message, + TArg arg, + TArg2 arg2, + TArg3 arg3, + TArg4 arg4, + TArg5 arg5) + => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Error, null, message, arg, arg2, arg3, arg4, arg5); + + /// + /// Log a error message. + /// + internal static void LogError(this SentryOptions options, + string message, + TArg arg, + TArg2 arg2, + TArg3 arg3, + TArg4 arg4) + => options.DiagnosticLogger?.LogIfEnabled(SentryLevel.Error, null, message, arg, arg2, arg3, arg4); + /// /// Log an error message. /// @@ -504,6 +549,23 @@ internal static void LogIfEnabled( } } + internal static void LogIfEnabled( + this IDiagnosticLogger logger, + SentryLevel level, + Exception? exception, + string message, + TArg arg, + TArg2 arg2, + TArg3 arg3, + TArg4 arg4, + TArg5 arg5) + { + if (logger.IsEnabled(level)) + { + logger.Log(level, message, exception, arg, arg2, arg3, arg4, arg5); + } + } + internal static void LogIfEnabled( this SentryOptions options, SentryLevel level, diff --git a/src/Sentry/Http/HttpTransportBase.cs b/src/Sentry/Http/HttpTransportBase.cs index 2eb646b8bd..9b8e491805 100644 --- a/src/Sentry/Http/HttpTransportBase.cs +++ b/src/Sentry/Http/HttpTransportBase.cs @@ -24,6 +24,8 @@ public abstract class HttpTransportBase // Using string instead of SentryId here so that we can use Interlocked.Exchange(...). private string? _lastDiscardedSessionInitId; + private string _typeName; + /// /// Constructor for this class. /// @@ -37,6 +39,7 @@ protected HttpTransportBase(SentryOptions options, _options = options; _clock = clock ?? SystemClock.Clock; _getEnvironmentVariable = getEnvironmentVariable ?? options.SettingLocator.GetEnvironmentVariable; + _typeName = GetType().Name; } // Keep track of rate limits and their expiry dates. @@ -66,20 +69,22 @@ protected internal Envelope ProcessEnvelope(Envelope envelope) if (clientReport != null) { envelopeItems.Add(EnvelopeItem.FromClientReport(clientReport)); - _options.LogDebug("Attached client report to envelope {0}.", eventId); + _options.LogDebug("{0}: Attached client report to envelope {1}.", _typeName, eventId); } if (envelopeItems.Count == 0) { if (_options.SendClientReports) { - _options.LogInfo("Envelope {0} was discarded because all contained items are rate-limited " + + _options.LogInfo("{0}: Envelope '{1}' was discarded because all contained items are rate-limited " + "and there are no client reports to send.", + _typeName, eventId); } else { - _options.LogInfo("Envelope {0} was discarded because all contained items are rate-limited.", + _options.LogInfo("{0}: Envelope '{1}' was discarded because all contained items are rate-limited.", + _typeName, eventId); } } @@ -99,7 +104,8 @@ private void ProcessEnvelopeItem(DateTimeOffset now, EnvelopeItem item, ListThe envelope. /// An HTTP request message, with the proper headers and body set. /// Throws if the DSN is not set in the options. - protected internal HttpRequestMessage CreateRequest(Envelope envelope) + protected internal virtual HttpRequestMessage CreateRequest(Envelope envelope) { if (string.IsNullOrWhiteSpace(_options.Dsn)) { @@ -302,22 +311,22 @@ private void LogEnvelopeSent(Envelope envelope, string? payload = null) { if (eventId == null) { - _options.LogInfo("Envelope successfully sent.", eventId); + _options.LogInfo("{0}: Envelope successfully sent.", _typeName); } else { - _options.LogInfo("Envelope '{0}' successfully sent.", eventId); + _options.LogInfo("{0}: Envelope '{1}' successfully sent.", _typeName, eventId); } } else { if (eventId == null) { - _options.LogDebug("Envelope successfully sent. Content: {1}", eventId, payload); + _options.LogDebug("{0}: Envelope successfully sent. Content: {1}", _typeName, payload); } else { - _options.LogDebug("Envelope '{0}' successfully sent. Content: {1}", eventId, payload); + _options.LogDebug("{0}: Envelope '{1}' successfully sent. Content: {2}", _typeName, eventId, payload); } } } @@ -347,17 +356,17 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope) if (_options.DiagnosticLogger?.IsEnabled(SentryLevel.Debug) is true) { var payload = envelope.SerializeToString(_options.DiagnosticLogger, _clock); - _options.LogDebug("Failed envelope '{0}' has payload:\n{1}\n", eventId, payload); + _options.LogDebug("{0}: Failed envelope '{1}' has payload:\n{2}\n", _typeName, eventId, payload); // SDK is in debug mode, and envelope was too large. To help troubleshoot: const string persistLargeEnvelopePathEnvVar = "SENTRY_KEEP_LARGE_ENVELOPE_PATH"; if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge && _getEnvironmentVariable(persistLargeEnvelopePathEnvVar) is { } destinationDirectory) { - _options.DiagnosticLogger? - .LogDebug("Environment variable '{0}' set. Writing envelope to {1}", - persistLargeEnvelopePathEnvVar, - destinationDirectory); + _options.LogDebug("{0}: Environment variable '{1}' set. Writing envelope to {2}", + _typeName, + persistLargeEnvelopePathEnvVar, + destinationDirectory); var destination = Path.Combine(destinationDirectory, "envelope_too_large", (eventId ?? SentryId.Create()).ToString()); @@ -370,8 +379,8 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope) { envelope.Serialize(envelopeFile, _options.DiagnosticLogger); envelopeFile.Flush(); - _options.LogInfo("Envelope's {0} bytes written to: {1}", - envelopeFile.Length, destination); + _options.LogInfo("{0}: Envelope's {1} bytes written to: {2}", + _typeName, envelopeFile.Length, destination); } } } @@ -403,17 +412,17 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env { var payload = await envelope .SerializeToStringAsync(_options.DiagnosticLogger, _clock, cancellationToken).ConfigureAwait(false); - _options.LogDebug("Failed envelope '{0}' has payload:\n{1}\n", eventId, payload); + _options.LogDebug("{0}: Failed envelope '{1}' has payload:\n{2}\n", _typeName, eventId, payload); // SDK is in debug mode, and envelope was too large. To help troubleshoot: const string persistLargeEnvelopePathEnvVar = "SENTRY_KEEP_LARGE_ENVELOPE_PATH"; if (response.StatusCode == HttpStatusCode.RequestEntityTooLarge && _getEnvironmentVariable(persistLargeEnvelopePathEnvVar) is { } destinationDirectory) { - _options.DiagnosticLogger? - .LogDebug("Environment variable '{0}' set. Writing envelope to {1}", - persistLargeEnvelopePathEnvVar, - destinationDirectory); + _options.LogDebug("{0}: Environment variable '{1}' set. Writing envelope to {2}", + _typeName, + persistLargeEnvelopePathEnvVar, + destinationDirectory); var destination = Path.Combine(destinationDirectory, "envelope_too_large", (eventId ?? SentryId.Create()).ToString()); @@ -431,8 +440,8 @@ await envelope .SerializeAsync(envelopeFile, _options.DiagnosticLogger, cancellationToken) .ConfigureAwait(false); await envelopeFile.FlushAsync(cancellationToken).ConfigureAwait(false); - _options.LogInfo("Envelope's {0} bytes written to: {1}", - envelopeFile.Length, destination); + _options.LogInfo("{0}: Envelope's {1} bytes written to: {2}", + _typeName, envelopeFile.Length, destination); } } } @@ -460,9 +469,8 @@ private void IncrementDiscardsForHttpFailure(HttpStatusCode responseStatusCode, private void LogFailure(string responseString, HttpStatusCode responseStatusCode, SentryId? eventId) { - _options.Log(SentryLevel.Error, - "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}.", - null, + _options.LogError("{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}.", + _typeName, eventId, responseStatusCode, responseString); @@ -478,9 +486,8 @@ private void LogFailure(JsonElement responseJson, HttpStatusCode responseStatusC responseJson.GetPropertyOrNull("causes")?.EnumerateArray().Select(j => j.GetString()).ToArray() ?? Array.Empty(); - _options.Log(SentryLevel.Error, - "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}. Error causes: {3}.", - null, + _options.LogError("{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}. Error causes: {4}.", + _typeName, eventId, responseStatusCode, errorMessage, diff --git a/src/Sentry/Http/SpotlightHttpTransport.cs b/src/Sentry/Http/SpotlightHttpTransport.cs new file mode 100644 index 0000000000..097847531f --- /dev/null +++ b/src/Sentry/Http/SpotlightHttpTransport.cs @@ -0,0 +1,60 @@ +using Sentry.Extensibility; +using Sentry.Infrastructure; +using Sentry.Internal.Http; +using Sentry.Protocol.Envelopes; + +namespace Sentry.Http; + +internal class SpotlightHttpTransport : HttpTransport +{ + private readonly ITransport _inner; + private readonly SentryOptions _options; + private readonly HttpClient _httpClient; + private readonly Uri _spotlightUrl; + private readonly ISystemClock _clock; + + public SpotlightHttpTransport(ITransport inner, SentryOptions options, HttpClient httpClient, Uri spotlightUrl, ISystemClock clock) + : base(options, httpClient) + { + _options = options; + _httpClient = httpClient; + _spotlightUrl = spotlightUrl; + _inner = inner; + _clock = clock; + } + + protected internal override HttpRequestMessage CreateRequest(Envelope envelope) + { + return new HttpRequestMessage + { + RequestUri = _spotlightUrl, + Method = HttpMethod.Post, + Content = new EnvelopeHttpContent(envelope, _options.DiagnosticLogger, _clock) + { Headers = { ContentType = MediaTypeHeaderValue.Parse("application/x-sentry-envelope") } } + }; + } + + public override async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default) + { + var sentryTask = _inner.SendEnvelopeAsync(envelope, cancellationToken); + + try + { + // Send to spotlight + using var processedEnvelope = ProcessEnvelope(envelope); + if (processedEnvelope.Items.Count > 0) + { + using var request = CreateRequest(processedEnvelope); + using var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); + await HandleResponseAsync(response, processedEnvelope, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception e) + { + _options.LogError(e, "Failed sending envelope to Spotlight."); + } + + // await the Sentry request before returning + await sentryTask.ConfigureAwait(false); + } +} diff --git a/src/Sentry/Internal/Http/DefaultSentryHttpClientFactory.cs b/src/Sentry/Internal/Http/DefaultSentryHttpClientFactory.cs index ab1f9a795d..e6746283a6 100644 --- a/src/Sentry/Internal/Http/DefaultSentryHttpClientFactory.cs +++ b/src/Sentry/Internal/Http/DefaultSentryHttpClientFactory.cs @@ -20,7 +20,7 @@ public HttpClient Create(SentryOptions options) throw new ArgumentNullException(nameof(options)); } - HttpMessageHandler handler = options.CreateHttpMessageHandler?.Invoke() ?? new HttpClientHandler(); + var handler = options.CreateHttpMessageHandler?.Invoke() ?? new HttpClientHandler(); if (handler is HttpClientHandler httpClientHandler) { if (options.HttpProxy is not null) diff --git a/src/Sentry/Internal/Http/HttpTransport.cs b/src/Sentry/Internal/Http/HttpTransport.cs index 495d732dfb..7f8b7ce91d 100644 --- a/src/Sentry/Internal/Http/HttpTransport.cs +++ b/src/Sentry/Internal/Http/HttpTransport.cs @@ -33,7 +33,7 @@ internal HttpTransport(SentryOptions options, HttpClient httpClient, /// such that they can be shared with higher-level SDKs (such as Unity) that may implement their own method /// for performing HTTP transport. /// - public async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default) + public virtual async Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default) { using var processedEnvelope = ProcessEnvelope(envelope); if (processedEnvelope.Items.Count > 0) diff --git a/src/Sentry/Internal/Http/LazyHttpTransport.cs b/src/Sentry/Internal/Http/LazyHttpTransport.cs index cff2f85e85..8a81042694 100644 --- a/src/Sentry/Internal/Http/LazyHttpTransport.cs +++ b/src/Sentry/Internal/Http/LazyHttpTransport.cs @@ -9,11 +9,7 @@ internal class LazyHttpTransport : ITransport public LazyHttpTransport(SentryOptions options) { - _httpTransport = new Lazy(() => - { - var factory = (options.SentryHttpClientFactory ?? new DefaultSentryHttpClientFactory()).Create(options); - return new HttpTransport(options, factory); - }); + _httpTransport = new Lazy(() => new HttpTransport(options, options.GetHttpClient())); } public Task SendEnvelopeAsync(Envelope envelope, CancellationToken cancellationToken = default) diff --git a/src/Sentry/Internal/Hub.cs b/src/Sentry/Internal/Hub.cs index c8c2e1da86..1ce56c8429 100644 --- a/src/Sentry/Internal/Hub.cs +++ b/src/Sentry/Internal/Hub.cs @@ -1,7 +1,5 @@ using Sentry.Extensibility; using Sentry.Infrastructure; -using Sentry.Integrations; -using Sentry.Internal.ScopeStack; namespace Sentry.Internal; diff --git a/src/Sentry/Internal/SdkComposer.cs b/src/Sentry/Internal/SdkComposer.cs index f813a61805..66d158d3f5 100644 --- a/src/Sentry/Internal/SdkComposer.cs +++ b/src/Sentry/Internal/SdkComposer.cs @@ -1,4 +1,6 @@ using Sentry.Extensibility; +using Sentry.Http; +using Sentry.Infrastructure; using Sentry.Internal.Http; namespace Sentry.Internal; @@ -27,6 +29,30 @@ private ITransport CreateTransport() transport = CachingTransport.Create(transport, _options); } + // Wrap the transport with the Spotlight one that double sends the envelope: Sentry + Spotlight + if (_options.EnableSpotlight) + { + var environment = _options.SettingLocator.GetEnvironment(true); + if (string.Equals(environment, Constants.ProductionEnvironmentSetting, StringComparison.OrdinalIgnoreCase)) + { + _options.LogWarning(""" + [Spotlight] It seems you're not in dev mode because environment is set to 'production'. + Do you really want to have Spotlight enabled? + You can set a different environment via SENTRY_ENVIRONMENT env var or programatically during Init. + Docs on Environment: https://docs.sentry.io/platforms/dotnet/configuration/environments/ + """); + } + else + { + _options.LogInfo("Connecting to Spotlight at {0}", _options.SpotlightUrl); + } + if (!Uri.TryCreate(_options.SpotlightUrl, UriKind.Absolute, out var spotlightUrl)) + { + throw new InvalidOperationException("Invalid option for SpotlightUrl: " + _options.SpotlightUrl); + } + transport = new SpotlightHttpTransport(transport, _options, _options.GetHttpClient(), spotlightUrl, SystemClock.Clock); + } + // Always persist the transport on the options, so other places can pick it up where necessary. _options.Transport = transport; @@ -35,7 +61,7 @@ private ITransport CreateTransport() private LazyHttpTransport CreateHttpTransport() { - if (_options.SentryHttpClientFactory is { }) + if (_options.SentryHttpClientFactory is not null) { _options.LogDebug( "Using ISentryHttpClientFactory set through options: {0}.", diff --git a/src/Sentry/SentryClient.cs b/src/Sentry/SentryClient.cs index 3f39a34f40..520e1954a0 100644 --- a/src/Sentry/SentryClient.cs +++ b/src/Sentry/SentryClient.cs @@ -49,11 +49,14 @@ internal SentryClient( options.SetupLogging(); // Only relevant if this client wasn't created as a result of calling Init - if (AotHelper.IsNativeAot) { + if (AotHelper.IsNativeAot) +#pragma warning disable CS0162 // Unreachable code detected + { #pragma warning disable 0162 // Unreachable code on old .NET frameworks options.LogDebug("This looks like a NativeAOT application build."); #pragma warning restore 0162 } else { +#pragma warning restore CS0162 // Unreachable code detected options.LogDebug("This looks like a standard JIT/AOT application build."); } diff --git a/src/Sentry/SentryOptions.cs b/src/Sentry/SentryOptions.cs index 19810f2939..198c1a569e 100644 --- a/src/Sentry/SentryOptions.cs +++ b/src/Sentry/SentryOptions.cs @@ -214,6 +214,12 @@ internal IEnumerable Integrations internal ISentryHttpClientFactory? SentryHttpClientFactory { get; set; } + internal HttpClient GetHttpClient() + { + var factory = SentryHttpClientFactory ?? new DefaultSentryHttpClientFactory(); + return factory.Create(this); + } + /// /// Scope state processor. /// @@ -1064,6 +1070,23 @@ public bool JsonPreserveReferences [EditorBrowsable(EditorBrowsableState.Never)] public Func? AssemblyReader { get; set; } + /// + /// The Spotlight URL. Defaults to http://localhost:8969/stream + /// + /// + /// + public string SpotlightUrl { get; set; } = "http://localhost:8969/stream"; + + /// + /// Whether to enable Spotlight for local development. + /// + /// + /// Only set this option to `true` while developing, not in production! + /// + /// + /// + public bool EnableSpotlight { get; set; } + internal SettingLocator SettingLocator { get; set; } /// diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt index 60d2cee9db..df13a013dd 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet6_0.verified.txt @@ -618,6 +618,7 @@ namespace Sentry public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } + public bool EnableSpotlight { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } @@ -644,6 +645,7 @@ namespace Sentry public Sentry.ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } public string? ServerName { get; set; } public System.TimeSpan ShutdownTimeout { get; set; } + public string SpotlightUrl { get; set; } public Sentry.StackTraceMode StackTraceMode { get; set; } public System.Collections.Generic.ICollection TagFilters { get; set; } public System.Collections.Generic.IList TracePropagationTargets { get; set; } @@ -1336,7 +1338,7 @@ namespace Sentry.Http public abstract class HttpTransportBase { protected HttpTransportBase(Sentry.SentryOptions options, System.Func? getEnvironmentVariable = null, Sentry.Infrastructure.ISystemClock? clock = null) { } - protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected virtual System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } protected void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope) { } protected System.Threading.Tasks.Task HandleResponseAsync(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken) { } protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt index 60d2cee9db..df13a013dd 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet7_0.verified.txt @@ -618,6 +618,7 @@ namespace Sentry public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } + public bool EnableSpotlight { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } @@ -644,6 +645,7 @@ namespace Sentry public Sentry.ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } public string? ServerName { get; set; } public System.TimeSpan ShutdownTimeout { get; set; } + public string SpotlightUrl { get; set; } public Sentry.StackTraceMode StackTraceMode { get; set; } public System.Collections.Generic.ICollection TagFilters { get; set; } public System.Collections.Generic.IList TracePropagationTargets { get; set; } @@ -1336,7 +1338,7 @@ namespace Sentry.Http public abstract class HttpTransportBase { protected HttpTransportBase(Sentry.SentryOptions options, System.Func? getEnvironmentVariable = null, Sentry.Infrastructure.ISystemClock? clock = null) { } - protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected virtual System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } protected void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope) { } protected System.Threading.Tasks.Task HandleResponseAsync(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken) { } protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt index 4ed6442010..c12dbe0dbe 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.DotNet8_0.verified.txt @@ -619,6 +619,7 @@ namespace Sentry public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } + public bool EnableSpotlight { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } @@ -645,6 +646,7 @@ namespace Sentry public Sentry.ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } public string? ServerName { get; set; } public System.TimeSpan ShutdownTimeout { get; set; } + public string SpotlightUrl { get; set; } public Sentry.StackTraceMode StackTraceMode { get; set; } public System.Collections.Generic.ICollection TagFilters { get; set; } public System.Collections.Generic.IList TracePropagationTargets { get; set; } @@ -1337,7 +1339,7 @@ namespace Sentry.Http public abstract class HttpTransportBase { protected HttpTransportBase(Sentry.SentryOptions options, System.Func? getEnvironmentVariable = null, Sentry.Infrastructure.ISystemClock? clock = null) { } - protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected virtual System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } protected void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope) { } protected System.Threading.Tasks.Task HandleResponseAsync(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken) { } protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } diff --git a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt index c2ad004e64..298e3ff6ee 100644 --- a/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt +++ b/test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt @@ -616,6 +616,7 @@ namespace Sentry public string? Distribution { get; set; } public string? Dsn { get; set; } public bool EnableScopeSync { get; set; } + public bool EnableSpotlight { get; set; } public bool? EnableTracing { get; set; } public string? Environment { get; set; } public System.Collections.Generic.IList FailedRequestStatusCodes { get; set; } @@ -642,6 +643,7 @@ namespace Sentry public Sentry.ISentryScopeStateProcessor SentryScopeStateProcessor { get; set; } public string? ServerName { get; set; } public System.TimeSpan ShutdownTimeout { get; set; } + public string SpotlightUrl { get; set; } public Sentry.StackTraceMode StackTraceMode { get; set; } public System.Collections.Generic.ICollection TagFilters { get; set; } public System.Collections.Generic.IList TracePropagationTargets { get; set; } @@ -1333,7 +1335,7 @@ namespace Sentry.Http public abstract class HttpTransportBase { protected HttpTransportBase(Sentry.SentryOptions options, System.Func? getEnvironmentVariable = null, Sentry.Infrastructure.ISystemClock? clock = null) { } - protected System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } + protected virtual System.Net.Http.HttpRequestMessage CreateRequest(Sentry.Protocol.Envelopes.Envelope envelope) { } protected void HandleResponse(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope) { } protected System.Threading.Tasks.Task HandleResponseAsync(System.Net.Http.HttpResponseMessage response, Sentry.Protocol.Envelopes.Envelope envelope, System.Threading.CancellationToken cancellationToken) { } protected Sentry.Protocol.Envelopes.Envelope ProcessEnvelope(Sentry.Protocol.Envelopes.Envelope envelope) { } diff --git a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs index e00b8a3f55..7e2eb80825 100644 --- a/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs +++ b/test/Sentry.Tests/Internals/Http/HttpTransportTests.cs @@ -82,12 +82,13 @@ public async Task SendEnvelopeAsync_ResponseNotOkWithJsonMessage_LogsError() // Assert logger.Entries.Any(e => e.Level == SentryLevel.Error && - e.Message == "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}. Error causes: {3}." && + e.Message == "{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}. Error causes: {4}." && e.Exception == null && - e.Args[0].ToString() == envelope.TryGetEventId().ToString() && - e.Args[1].ToString() == expectedCode.ToString() && - e.Args[2].ToString() == expectedMessage && - e.Args[3].ToString() == expectedCausesFormatted + e.Args[0].ToString() == "HttpTransport" && + e.Args[1].ToString() == envelope.TryGetEventId().ToString() && + e.Args[2].ToString() == expectedCode.ToString() && + e.Args[3].ToString() == expectedMessage && + e.Args[4].ToString() == expectedCausesFormatted ).Should().BeTrue(); } @@ -125,28 +126,29 @@ public async Task SendEnvelopeAsync_ResponseRequestEntityTooLargeWithPathDefined // Assert logger.Entries.Any(e => e.Level == SentryLevel.Debug && - e.Message == "Environment variable '{0}' set. Writing envelope to {1}" && + e.Message == "{0}: Environment variable '{1}' set. Writing envelope to {2}" && e.Exception == null && - e.Args[0].ToString() == expectedEnvVar && - e.Args[1].ToString() == path) + e.Args[0].ToString() == "HttpTransport" && + e.Args[1].ToString() == expectedEnvVar && + e.Args[2].ToString() == path) .Should() .BeTrue(); var fileStoredLogEntry = logger.Entries.FirstOrDefault(e => e.Level == SentryLevel.Info && - e.Message == "Envelope's {0} bytes written to: {1}"); + e.Message == "{0}: Envelope's {1} bytes written to: {2}"); Assert.NotNull(fileStoredLogEntry); - var expectedFile = new FileInfo(fileStoredLogEntry.Args[1].ToString()!); + var expectedFile = new FileInfo(fileStoredLogEntry.Args[2].ToString()!); Assert.True(expectedFile.Exists); try { Assert.Null(fileStoredLogEntry.Exception); // // Path is based on the provided path: - Assert.Contains(path, fileStoredLogEntry.Args[1] as string); + Assert.Contains(path, fileStoredLogEntry.Args[2] as string); // // Path contains the envelope id in its name: - Assert.Contains(envelope.TryGetEventId().ToString(), fileStoredLogEntry.Args[1] as string); - Assert.Equal(expectedFile.Length, (long)fileStoredLogEntry.Args[0]); + Assert.Contains(envelope.TryGetEventId().ToString(), fileStoredLogEntry.Args[2] as string); + Assert.Equal(expectedFile.Length, (long)fileStoredLogEntry.Args[1]); } finally { @@ -224,11 +226,12 @@ public async Task SendEnvelopeAsync_ResponseNotOkWithStringMessage_LogsError() // Assert _ = logger.Entries.Any(e => e.Level == SentryLevel.Error && - e.Message == "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}." && + e.Message == "{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}." && e.Exception == null && - e.Args[0].ToString() == envelope.TryGetEventId().ToString() && - e.Args[1].ToString() == expectedCode.ToString() && - e.Args[2].ToString() == expectedMessage + e.Args[0].ToString() == "HttpTransport" && + e.Args[1].ToString() == envelope.TryGetEventId().ToString() && + e.Args[2].ToString() == expectedCode.ToString() && + e.Args[3].ToString() == expectedMessage ).Should().BeTrue(); } @@ -262,12 +265,14 @@ public async Task SendEnvelopeAsync_ResponseNotOkNoMessage_LogsError() // Assert logger.Entries.Any(e => e.Level == SentryLevel.Error && - e.Message == "Sentry rejected the envelope {0}. Status code: {1}. Error detail: {2}. Error causes: {3}." && + e.Message == "{0}: Sentry rejected the envelope '{1}'. Status code: {2}. Error detail: {3}. Error causes: {4}." && e.Exception == null && - e.Args[0].ToString() == envelope.TryGetEventId().ToString() && - e.Args[1].ToString() == expectedCode.ToString() && - e.Args[2].ToString() == HttpTransportBase.DefaultErrorMessage && - e.Args[3].ToString() == string.Empty + e.Args[0].ToString() == "HttpTransport" && + e.Args[1].ToString() == envelope.TryGetEventId().ToString() && + e.Args[2].ToString() == expectedCode.ToString() && + e.Args[3].ToString() == HttpTransportBase.DefaultErrorMessage && + e.Args[4].ToString() == string.Empty + ).Should().BeTrue(); } @@ -588,7 +593,7 @@ public async Task SendEnvelopeAsync_AttachmentTooLarge_DropsItem() // (the envelope should have only one item) logger.Entries.Should().Contain(e => - string.Format(e.Message, e.Args) == "Attachment 'test2.txt' dropped because it's too large (5 bytes)."); + string.Format(e.Message, e.Args) == "HttpTransport: Attachment 'test2.txt' dropped because it's too large (5 bytes)."); actualEnvelopeSerialized.Should().NotContain("test2.txt"); } diff --git a/test/Sentry.Tests/Internals/Http/SpotlightTransportTests.cs b/test/Sentry.Tests/Internals/Http/SpotlightTransportTests.cs new file mode 100644 index 0000000000..0669280b4b --- /dev/null +++ b/test/Sentry.Tests/Internals/Http/SpotlightTransportTests.cs @@ -0,0 +1,54 @@ +using Sentry.Http; +using Sentry.Tests.Helpers; + +namespace Sentry.Tests.Internals.Http; + +public class SpotlightTransportTests +{ + // Makes sure it'll call both inner transport and spotlight, even if spotlight's request fails. + // Inner transport error actually bubbles up instead of Spotlights' + [Fact] + public async Task SendEnvelopeAsync_SpotlightRequestFailed_InnerTransportFailureBubblesUp() + { + // Arrange + var httpHandler = Substitute.For(); + var expectedSpotlightTransportException = new Exception("Spotlight request fails"); + httpHandler.WhenForAnyArgs(h => h.VerifiableSendAsync(null, CancellationToken.None)) + .Throw(expectedSpotlightTransportException); + + var innerTransport = Substitute.For(); + var logger = new InMemoryDiagnosticLogger(); + + var sut = new SpotlightHttpTransport( + innerTransport, + new SentryOptions + { + Dsn = ValidDsn, + Debug = true, + DiagnosticLogger = logger + }, + new HttpClient(httpHandler), + new Uri("http://localhost:8969/stream"), + Substitute.For()); + + var envelope = Envelope.FromEvent(new SentryEvent()); + var expectedInnerTransportException = new Exception("expected inner transport exception"); + var tcs = new TaskCompletionSource(); + tcs.SetException(expectedInnerTransportException); + innerTransport.SendEnvelopeAsync(envelope).Returns(tcs.Task); + + // Act + var actualException = await Assert.ThrowsAsync(() => sut.SendEnvelopeAsync(envelope)); + + // Assert + // Inner transport Exception bubbles out + Assert.Same(expectedInnerTransportException, actualException); + + // Spotlight request failure logged out to diagnostic logger + logger.Entries.Any(e => + e.Level == SentryLevel.Error && + e.Message == "Failed sending envelope to Spotlight." && + ReferenceEquals(expectedSpotlightTransportException, e.Exception) + ).Should().BeTrue(); + } +}