From 4f2696990d98652d59d45ce16e7ab4eb9b63828b Mon Sep 17 00:00:00 2001 From: Ahmed Ahmed Date: Tue, 18 Mar 2025 11:36:38 +0000 Subject: [PATCH 1/6] Replace deilvery with HttpClient implementation --- src/Bugsnag/Bugsnag.csproj | 2 +- src/Bugsnag/Client.cs | 2 +- src/Bugsnag/DefaultDelivery.cs | 126 +++++++++++++ src/Bugsnag/SessionsStore.cs | 2 +- src/Bugsnag/ThreadQueueDelivery.cs | 173 ------------------ src/Bugsnag/UnhandledException.cs | 2 +- src/Bugsnag/WebRequest.cs | 153 ---------------- ...liveryTests.cs => DefaultDeliveryTests.cs} | 9 +- tests/Bugsnag.Tests/WebRequestTests.cs | 31 ---- 9 files changed, 135 insertions(+), 365 deletions(-) create mode 100644 src/Bugsnag/DefaultDelivery.cs delete mode 100644 src/Bugsnag/ThreadQueueDelivery.cs delete mode 100644 src/Bugsnag/WebRequest.cs rename tests/Bugsnag.Tests/{ThreadQueueDeliveryTests.cs => DefaultDeliveryTests.cs} (81%) delete mode 100644 tests/Bugsnag.Tests/WebRequestTests.cs diff --git a/src/Bugsnag/Bugsnag.csproj b/src/Bugsnag/Bugsnag.csproj index f61590b1..3d311bda 100644 --- a/src/Bugsnag/Bugsnag.csproj +++ b/src/Bugsnag/Bugsnag.csproj @@ -11,12 +11,12 @@ + - diff --git a/src/Bugsnag/Client.cs b/src/Bugsnag/Client.cs index f17f3995..c634415c 100644 --- a/src/Bugsnag/Client.cs +++ b/src/Bugsnag/Client.cs @@ -47,7 +47,7 @@ public Client(string apiKey) : this(new Configuration(apiKey)) /// Constructs a client with the default storage and delivery classes. /// /// - public Client(IConfiguration configuration) : this(configuration, ThreadQueueDelivery.Instance, new Breadcrumbs(configuration), new SessionTracker(configuration)) + public Client(IConfiguration configuration) : this(configuration, DefaultDelivery.Instance, new Breadcrumbs(configuration), new SessionTracker(configuration)) { } diff --git a/src/Bugsnag/DefaultDelivery.cs b/src/Bugsnag/DefaultDelivery.cs new file mode 100644 index 00000000..2851e4e4 --- /dev/null +++ b/src/Bugsnag/DefaultDelivery.cs @@ -0,0 +1,126 @@ +using Bugsnag.Payload; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Bugsnag +{ + public class DefaultDelivery : IDelivery + { + private static DefaultDelivery instance = null; + private static readonly object instanceLock = new object(); + + private readonly HttpClient _httpClient; + private readonly CancellationTokenSource _cancellationTokenSource; + private readonly Task _processingTask; + private readonly BlockingCollection _queue; + + private DefaultDelivery() + { + _httpClient = new HttpClient(); + _queue = new BlockingCollection(new ConcurrentQueue()); + _cancellationTokenSource = new CancellationTokenSource(); + _processingTask = Task.Run(() => ProcessQueueAsync(_cancellationTokenSource.Token)); + } + + public static DefaultDelivery Instance + { + get + { + lock (instanceLock) + { + if (instance == null) + { + instance = new DefaultDelivery(); + } + + return instance; + } + } + } + + public void Send(IPayload payload) + { + _queue.Add(payload); + } + + internal void Stop() + { + Task.WaitAll(new[] { _processingTask }, TimeSpan.FromSeconds(5)); + _cancellationTokenSource.Cancel(); + _httpClient.Dispose(); + _cancellationTokenSource.Dispose(); + _queue.Dispose(); + } + + private async Task ProcessQueueAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + IPayload payload = null; + + try + { + // Take will block until an item is available or cancellation is requested + payload = _queue.Take(cancellationToken); + } + catch (OperationCanceledException) + { + // Exit gracefully when cancellation is requested + break; + } + + if (payload != null) + { + await SendPayloadAsync(payload); + } + } + } + + private async Task SendPayloadAsync(IPayload payload) + { + try + { + byte[] serializedPayload = payload.Serialize(); + if (serializedPayload == null) + return; + + using (var request = new HttpRequestMessage(HttpMethod.Post, payload.Endpoint)) + { + request.Content = new ByteArrayContent(serializedPayload); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + + // Add headers from payload + if (payload.Headers != null) + { + foreach (var header in payload.Headers) + { + request.Headers.TryAddWithoutValidation(header.Key, header.Value); + } + } + + // Add the Bugsnag-Sent-At header + request.Headers.Add("Bugsnag-Sent-At", DateTime.UtcNow.ToString("o", System.Globalization.CultureInfo.InvariantCulture)); + + // Send the request and log any failures + var response = await _httpClient.SendAsync(request); + + if (!response.IsSuccessStatusCode) { + Trace.WriteLine($"Failed to send payload to Bugsnag - received status code {response.StatusCode}"); + } + } + } + catch (System.Exception ex) + { + Trace.WriteLine($"Error sending payload to Bugsnag: {ex}"); + } + } + } +} diff --git a/src/Bugsnag/SessionsStore.cs b/src/Bugsnag/SessionsStore.cs index b47afa1a..c9718e9f 100644 --- a/src/Bugsnag/SessionsStore.cs +++ b/src/Bugsnag/SessionsStore.cs @@ -39,7 +39,7 @@ private void SendSessions(object state) foreach (var item in sessionData) { var payload = new BatchedSessions(item.Key, item.Value); - ThreadQueueDelivery.Instance.Send(payload); + DefaultDelivery.Instance.Send(payload); } } diff --git a/src/Bugsnag/ThreadQueueDelivery.cs b/src/Bugsnag/ThreadQueueDelivery.cs deleted file mode 100644 index ab9c5e6f..00000000 --- a/src/Bugsnag/ThreadQueueDelivery.cs +++ /dev/null @@ -1,173 +0,0 @@ -using Bugsnag.Payload; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; - -namespace Bugsnag -{ - public class Countdown - { - private readonly object _lock = new object(); - private int _counter; - private readonly ManualResetEvent _resetEvent; - - public Countdown(int initialCount) - { - _counter = initialCount; - _resetEvent = new ManualResetEvent(initialCount == 0); - } - - internal void AddCount() - { - lock (_lock) - { - _counter++; - if (_counter > 0) - { - _resetEvent.Reset(); - } - } - } - - internal void Signal() - { - lock (_lock) - { - _counter--; - if (_counter <= 0) - { - _resetEvent.Set(); - } - } - } - - internal void Wait(TimeSpan timeout) - { - _resetEvent.WaitOne(timeout); - } - } - - public class ThreadQueueDelivery : IDelivery - { - private static ThreadQueueDelivery instance = null; - private static readonly object instanceLock = new object(); - - private readonly BlockingQueue _queue; - - private readonly Thread _worker; - - private ThreadQueueDelivery() - { - _queue = new BlockingQueue(); - _worker = new Thread(new ThreadStart(ProcessQueue)) { - Name = "Bugsnag Queue", - IsBackground = true - }; - _worker.Start(); - } - - internal void Stop() - { - _queue.Wait(TimeSpan.FromSeconds(5)); - } - - public static ThreadQueueDelivery Instance - { - get - { - lock (instanceLock) - { - if (instance == null) - { - instance = new ThreadQueueDelivery(); - } - - return instance; - } - } - } - - private void ProcessQueue() - { - while (true) - { - var payload = _queue.Dequeue(); - try - { - var serializedPayload = payload.Serialize(); - if (serializedPayload != null) - { - var request = new WebRequest(); - request.BeginSend(payload.Endpoint, payload.Proxy, payload.Headers, serializedPayload, ReportCallback, request); - } - } - catch (System.Exception exception) - { - Trace.WriteLine(exception); - } - } - } - - private void ReportCallback(IAsyncResult asyncResult) - { - if (asyncResult.AsyncState is WebRequest request) - { - var response = request.EndSend(asyncResult); - _queue.Signal(); - } - } - - public void Send(IPayload payload) - { - _queue.Enqueue(payload); - } - - private class BlockingQueue - { - private readonly Countdown _countdown; - private readonly Queue _queue; - private readonly object _queueLock; - - public BlockingQueue() - { - _countdown = new Countdown(0); - _queueLock = new object(); - _queue = new Queue(); - } - - public void Enqueue(T item) - { - lock (_queueLock) - { - _countdown.AddCount(); - _queue.Enqueue(item); - Monitor.Pulse(_queueLock); - } - } - - public T Dequeue() - { - lock (_queueLock) - { - while (_queue.Count == 0) - { - Monitor.Wait(_queueLock); - } - - return _queue.Dequeue(); - } - } - - public void Signal() - { - _countdown.Signal(); - } - - public void Wait(TimeSpan timeout) - { - _countdown.Wait(timeout); - } - } - } -} diff --git a/src/Bugsnag/UnhandledException.cs b/src/Bugsnag/UnhandledException.cs index ae356972..d082c45c 100644 --- a/src/Bugsnag/UnhandledException.cs +++ b/src/Bugsnag/UnhandledException.cs @@ -104,7 +104,7 @@ private void HandleEvent(Exception exception, bool runtimeEnding) if (runtimeEnding) { SessionsStore.Instance.Stop(); - ThreadQueueDelivery.Instance.Stop(); + DefaultDelivery.Instance.Stop(); } } } diff --git a/src/Bugsnag/WebRequest.cs b/src/Bugsnag/WebRequest.cs deleted file mode 100644 index f86aa470..00000000 --- a/src/Bugsnag/WebRequest.cs +++ /dev/null @@ -1,153 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Net; -using System.Threading; - -namespace Bugsnag -{ - public class WebRequest - { - private class WebRequestState - { - public AsyncCallback Callback { get; } - - public object OriginalState { get; } - - public Uri Endpoint { get; } - - public byte[] Report { get; } - - public System.Net.WebRequest Request { get; } - - public HttpWebResponse Response { get; set; } - - public WebException Exception { get; set; } - - public WebRequestState(AsyncCallback callback, object state, Uri endpoint, byte[] report, System.Net.WebRequest request) - { - Callback = callback; - OriginalState = state; - Endpoint = endpoint; - Report = report; - Request = request; - } - } - - private class WebRequestAsyncResult : IAsyncResult - { - public bool IsCompleted => InnerAsyncResult.IsCompleted; - - public WaitHandle AsyncWaitHandle => InnerAsyncResult.AsyncWaitHandle; - - public object AsyncState => WebRequestState.OriginalState; - - public bool CompletedSynchronously => InnerAsyncResult.CompletedSynchronously; - - public WebRequestState WebRequestState { get; } - - private IAsyncResult InnerAsyncResult { get; } - - public WebRequestAsyncResult(IAsyncResult innerAsyncResult, WebRequestState webRequestState) - { - InnerAsyncResult = innerAsyncResult; - WebRequestState = webRequestState; - } - } - - public IAsyncResult BeginSend(Uri endpoint, IWebProxy proxy, KeyValuePair[] headers, byte[] report, AsyncCallback callback, object state) - { - var request = (HttpWebRequest)System.Net.WebRequest.Create(endpoint); - request.KeepAlive = false; - request.Method = "POST"; - request.ContentType = "application/json"; - if (proxy != null) - { - request.Proxy = proxy; - } - if (headers != null) - { - foreach (var header in headers) - { - request.Headers[header.Key] = header.Value; - } - } - request.Headers["Bugsnag-Sent-At"] = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture); - var internalState = new WebRequestState(callback, state, endpoint, report, request); - var asyncResult = request.BeginGetRequestStream(new AsyncCallback(WriteCallback), internalState); - return new WebRequestAsyncResult(asyncResult, internalState); - } - - public WebResponse EndSend(IAsyncResult asyncResult) - { - if (asyncResult is WebRequestAsyncResult result) - { - if (result.WebRequestState.Response != null) - { - return new WebResponse(result.WebRequestState.Response.StatusCode); - } - - if (result.WebRequestState.Exception != null) - { - if (result.WebRequestState.Exception.Response is HttpWebResponse response) - { - return new WebResponse(response.StatusCode); - } - } - } - - return null; - } - - private void ReadCallback(IAsyncResult asynchronousResult) - { - var state = (WebRequestState)asynchronousResult.AsyncState; - try - { - var response = (HttpWebResponse)state.Request.EndGetResponse(asynchronousResult); - using (var stream = response.GetResponseStream()) - { - // we don't care about the content of the http response, only the status code - } - state.Response = response; - } - catch (WebException exception) - { - state.Exception = exception; - } - - state.Callback(new WebRequestAsyncResult(asynchronousResult, state)); - } - - private void WriteCallback(IAsyncResult asynchronousResult) - { - var state = (WebRequestState)asynchronousResult.AsyncState; - try - { - using (var stream = state.Request.EndGetRequestStream(asynchronousResult)) - { - stream.Write(state.Report, 0, state.Report.Length); - } - } - catch (WebException exception) - { - state.Exception = exception; - // call the original callback as we cannot continue sending the report - state.Callback(new WebRequestAsyncResult(asynchronousResult, state)); - return; - } - - state.Request.BeginGetResponse(new AsyncCallback(ReadCallback), state); - } - } - - public class WebResponse - { - public HttpStatusCode HttpStatusCode { get; } - - public WebResponse(HttpStatusCode httpStatusCode) - { - HttpStatusCode = httpStatusCode; - } - } -} diff --git a/tests/Bugsnag.Tests/ThreadQueueDeliveryTests.cs b/tests/Bugsnag.Tests/DefaultDeliveryTests.cs similarity index 81% rename from tests/Bugsnag.Tests/ThreadQueueDeliveryTests.cs rename to tests/Bugsnag.Tests/DefaultDeliveryTests.cs index ee966be1..aa7281c2 100644 --- a/tests/Bugsnag.Tests/ThreadQueueDeliveryTests.cs +++ b/tests/Bugsnag.Tests/DefaultDeliveryTests.cs @@ -2,15 +2,16 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Text; using System.Threading.Tasks; using Xunit; namespace Bugsnag.Tests { - public class ThreadQueueDeliveryTests + public class DefaultDeliveryTests { [Fact] - public async Task Test() + public async Task Send_EnqueuesAndSendsPayloads() { var numberOfRequests = 500; @@ -21,7 +22,7 @@ public async Task Test() for (int i = 0; i < numberOfRequests; i++) { var payload = new SamplePayload(i, server.Endpoint); - ThreadQueueDelivery.Instance.Send(payload); + DefaultDelivery.Instance.Send(payload); } var requests = await server.Requests(numberOfRequests); @@ -47,7 +48,7 @@ public SamplePayload(int count, Uri endpoint) public byte[] Serialize() { - return System.Text.Encoding.UTF8.GetBytes($"payload {Count}"); + return Encoding.UTF8.GetBytes($"payload {Count}"); } } } diff --git a/tests/Bugsnag.Tests/WebRequestTests.cs b/tests/Bugsnag.Tests/WebRequestTests.cs deleted file mode 100644 index 66fcfd14..00000000 --- a/tests/Bugsnag.Tests/WebRequestTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using Xunit; - -namespace Bugsnag.Tests -{ - public class WebRequestTests - { - [Fact] - public async Task Test() - { - var numerOfRequests = 1; - var server = new TestServer(); - server.Start(); - - var webRequest = new WebRequest(); - - var headers = new KeyValuePair[] { new KeyValuePair("Test-Header", "wow!") }; - - var rawPayload = System.Text.Encoding.UTF8.GetBytes($"{{ \"count\": {numerOfRequests} }}"); - var response = await Task.Factory.FromAsync((callback, state) => webRequest.BeginSend(server.Endpoint, null, headers, rawPayload, callback, state), webRequest.EndSend, null); - Assert.Equal(HttpStatusCode.OK, response.HttpStatusCode); - - var requests = await server.Requests(numerOfRequests); - - Assert.Equal(numerOfRequests, requests.Count()); - } - } -} From 75c5c54f69397f117bd8d680ceaa2c3fa7835a2c Mon Sep 17 00:00:00 2001 From: Ahmed Ahmed Date: Tue, 18 Mar 2025 14:26:09 +0000 Subject: [PATCH 2/6] Use proxy from client configuration and remove proxy property from payloads --- src/Bugsnag/Client.cs | 2 +- src/Bugsnag/DefaultDelivery.cs | 11 ++++++++++- src/Bugsnag/IDelivery.cs | 2 -- src/Bugsnag/Payload/BatchedSessions.cs | 5 ++--- src/Bugsnag/Payload/Report.cs | 4 +--- 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Bugsnag/Client.cs b/src/Bugsnag/Client.cs index c634415c..bdedb72a 100644 --- a/src/Bugsnag/Client.cs +++ b/src/Bugsnag/Client.cs @@ -49,7 +49,7 @@ public Client(string apiKey) : this(new Configuration(apiKey)) /// public Client(IConfiguration configuration) : this(configuration, DefaultDelivery.Instance, new Breadcrumbs(configuration), new SessionTracker(configuration)) { - + DefaultDelivery.Instance.Configure(configuration); } /// diff --git a/src/Bugsnag/DefaultDelivery.cs b/src/Bugsnag/DefaultDelivery.cs index 2851e4e4..de09cbbc 100644 --- a/src/Bugsnag/DefaultDelivery.cs +++ b/src/Bugsnag/DefaultDelivery.cs @@ -17,7 +17,7 @@ public class DefaultDelivery : IDelivery private static DefaultDelivery instance = null; private static readonly object instanceLock = new object(); - private readonly HttpClient _httpClient; + private HttpClient _httpClient; private readonly CancellationTokenSource _cancellationTokenSource; private readonly Task _processingTask; private readonly BlockingCollection _queue; @@ -46,6 +46,15 @@ public static DefaultDelivery Instance } } + public void Configure(IConfiguration configuration) + { + if (configuration.Proxy != null) + { + _httpClient.Dispose(); + _httpClient = new HttpClient(new HttpClientHandler { Proxy = configuration.Proxy }); + } + } + public void Send(IPayload payload) { _queue.Add(payload); diff --git a/src/Bugsnag/IDelivery.cs b/src/Bugsnag/IDelivery.cs index 0a794e48..08830bd1 100644 --- a/src/Bugsnag/IDelivery.cs +++ b/src/Bugsnag/IDelivery.cs @@ -17,8 +17,6 @@ public interface IPayload { Uri Endpoint { get; } - IWebProxy Proxy { get; } - KeyValuePair[] Headers { get; } byte[] Serialize(); diff --git a/src/Bugsnag/Payload/BatchedSessions.cs b/src/Bugsnag/Payload/BatchedSessions.cs index 087d00d8..21454418 100644 --- a/src/Bugsnag/Payload/BatchedSessions.cs +++ b/src/Bugsnag/Payload/BatchedSessions.cs @@ -19,11 +19,12 @@ public BatchedSessions(IConfiguration configuration, NotifierInfo notifier, App { _configuration = configuration; Endpoint = configuration.SessionEndpoint; - Proxy = configuration.Proxy; + _headers = new KeyValuePair[] { new KeyValuePair(Payload.Headers.ApiKeyHeader, configuration.ApiKey), new KeyValuePair(Payload.Headers.PayloadVersionHeader, "1.0") }; + this.AddToPayload("notifier", notifier); this.AddToPayload("device", device); this.AddToPayload("app", app); @@ -32,8 +33,6 @@ public BatchedSessions(IConfiguration configuration, NotifierInfo notifier, App public Uri Endpoint { get; set; } - public IWebProxy Proxy { get; set; } - public KeyValuePair[] Headers => _headers; public byte[] Serialize() diff --git a/src/Bugsnag/Payload/Report.cs b/src/Bugsnag/Payload/Report.cs index 120fe1f4..1df0c974 100644 --- a/src/Bugsnag/Payload/Report.cs +++ b/src/Bugsnag/Payload/Report.cs @@ -39,7 +39,7 @@ public Report(IConfiguration configuration, System.Exception exception, HandledS { _ignored = false; Endpoint = configuration.Endpoint; - Proxy = configuration.Proxy; + _headers = new KeyValuePair[] { new KeyValuePair(Payload.Headers.ApiKeyHeader, configuration.ApiKey), new KeyValuePair(Payload.Headers.PayloadVersionHeader, _payloadVersion), @@ -87,8 +87,6 @@ public void Ignore() /// public Uri Endpoint { get; set; } - public IWebProxy Proxy { get; set; } - public KeyValuePair[] Headers { get { return _headers; } } public byte[] Serialize() From faa93eab9748626a822f57aa8cf770441d9b5853 Mon Sep 17 00:00:00 2001 From: Ahmed Ahmed Date: Tue, 18 Mar 2025 15:09:05 +0000 Subject: [PATCH 3/6] Add unit tests with real payloads and test headers are set on requests --- .../UseExceptionHandlerTests.cs | 2 +- tests/Bugsnag.AspNet.Tests/ClientTests.cs | 2 +- .../WebHostTests.cs | 2 +- tests/Bugsnag.Tests.Server/TestServer.cs | 41 +++++++++----- tests/Bugsnag.Tests/ClientTests.cs | 2 +- tests/Bugsnag.Tests/DefaultDeliveryTests.cs | 56 +++++++++++++++++++ 6 files changed, 88 insertions(+), 17 deletions(-) diff --git a/tests/Bugsnag.AspNet.Core.Tests/UseExceptionHandlerTests.cs b/tests/Bugsnag.AspNet.Core.Tests/UseExceptionHandlerTests.cs index 15fdfd52..697c9e37 100644 --- a/tests/Bugsnag.AspNet.Core.Tests/UseExceptionHandlerTests.cs +++ b/tests/Bugsnag.AspNet.Core.Tests/UseExceptionHandlerTests.cs @@ -49,7 +49,7 @@ public async Task InitializeAsync() var bugsnags = await bugsnag.Requests(1); - BugsnagPayload = bugsnags.First(); + BugsnagPayload = bugsnags.First().Body; } /// diff --git a/tests/Bugsnag.AspNet.Tests/ClientTests.cs b/tests/Bugsnag.AspNet.Tests/ClientTests.cs index 4cf4b790..fb179081 100644 --- a/tests/Bugsnag.AspNet.Tests/ClientTests.cs +++ b/tests/Bugsnag.AspNet.Tests/ClientTests.cs @@ -39,7 +39,7 @@ public async Task InitializeAsync() var requests = await server.Requests(1); - _request = requests.Single(); + _request = requests.Single().Body; } [Fact] diff --git a/tests/Bugsnag.AspNet.WebApi.Tests/WebHostTests.cs b/tests/Bugsnag.AspNet.WebApi.Tests/WebHostTests.cs index eb852c56..e763bf0b 100644 --- a/tests/Bugsnag.AspNet.WebApi.Tests/WebHostTests.cs +++ b/tests/Bugsnag.AspNet.WebApi.Tests/WebHostTests.cs @@ -41,7 +41,7 @@ public async Task Test() var responses = await bugsnagServer.Requests(1); Assert.Single(responses); - Assert.Contains("Bugsnag is great!", responses.Single()); + Assert.Contains("Bugsnag is great!", responses.Single().Body); } } } diff --git a/tests/Bugsnag.Tests.Server/TestServer.cs b/tests/Bugsnag.Tests.Server/TestServer.cs index 7d46459b..99506900 100644 --- a/tests/Bugsnag.Tests.Server/TestServer.cs +++ b/tests/Bugsnag.Tests.Server/TestServer.cs @@ -4,7 +4,9 @@ using Microsoft.Extensions.DependencyInjection; using System; using System.Collections.Generic; +using System.Dynamic; using System.IO; +using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -14,15 +16,15 @@ namespace Bugsnag.Tests public class TestServer { private readonly IWebHost _webHost; - private readonly RequestCollection _requests; + private readonly RequestCollection _requests; private readonly int _port; public TestServer() { _port = PortAllocations.Instance.NextFreePort(); - _requests = new RequestCollection(); + _requests = new RequestCollection(); _webHost = new WebHostBuilder() - .ConfigureServices(services => services.AddSingleton(typeof(RequestCollection), _requests)) + .ConfigureServices(services => services.AddSingleton(typeof(RequestCollection), _requests)) .UseStartup() .UseKestrel(options => { options.Listen(IPAddress.Loopback, _port); @@ -37,13 +39,13 @@ public void Start() _webHost.Start(); } - public async Task> Requests(int numberOfRequests) + public async Task> Requests(int numberOfRequests) { var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); return await Requests(numberOfRequests, cts.Token); } - public async Task> Requests(int numberOfRequests, CancellationToken token) + public async Task> Requests(int numberOfRequests, CancellationToken token) { var items = await _requests.Items(numberOfRequests, token); await _webHost.StopAsync(); @@ -71,9 +73,9 @@ public int NextFreePort() class Startup { - private readonly RequestCollection _requests; + private readonly RequestCollection _requests; - public Startup(RequestCollection requests) + public Startup(RequestCollection requests) { _requests = requests; } @@ -81,12 +83,8 @@ public Startup(RequestCollection requests) public void Configure(IApplicationBuilder app) { app.Run(async context => { - var stream = context.Request.Body; - using (var reader = new StreamReader(stream)) - { - var request = await reader.ReadToEndAsync(); - _requests.Add(request); - } + var request = await CapturedRequest.Create(context.Request); + _requests.Add(request); await context.Response.WriteAsync("OK"); }); } @@ -133,4 +131,21 @@ public Task> Items(int numberOfRequests, CancellationToken token) } } } + + public class CapturedRequest + { + public Dictionary Headers { get; set; } + public string Body { get; set; } + + public static async Task Create(HttpRequest request) + { + var testRequest = new CapturedRequest(); + testRequest.Headers = request.Headers.ToDictionary(h => h.Key, h => h.Value.ToString()); + using (var reader = new StreamReader(request.Body)) + { + testRequest.Body = await reader.ReadToEndAsync(); + } + return testRequest; + } + } } diff --git a/tests/Bugsnag.Tests/ClientTests.cs b/tests/Bugsnag.Tests/ClientTests.cs index 7384ed94..a26f4e71 100644 --- a/tests/Bugsnag.Tests/ClientTests.cs +++ b/tests/Bugsnag.Tests/ClientTests.cs @@ -71,7 +71,7 @@ public async Task InitializeAsync() var requests = await server.Requests(1); - BugsnagPayload = requests.Single(); + BugsnagPayload = requests.Single().Body; } /// diff --git a/tests/Bugsnag.Tests/DefaultDeliveryTests.cs b/tests/Bugsnag.Tests/DefaultDeliveryTests.cs index aa7281c2..648091a8 100644 --- a/tests/Bugsnag.Tests/DefaultDeliveryTests.cs +++ b/tests/Bugsnag.Tests/DefaultDeliveryTests.cs @@ -1,3 +1,4 @@ +using Bugsnag.Payload; using System; using System.Collections.Generic; using System.Linq; @@ -10,6 +11,61 @@ namespace Bugsnag.Tests { public class DefaultDeliveryTests { + private const string API_KEY = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"; + + [Fact] + public async Task Send_ReportPayload_SendsCorrectly() + { + var server = new TestServer(); + server.Start(); + + var configuration = new Configuration(API_KEY); + configuration.Endpoint = server.Endpoint; + + var report = new Report(configuration, new System.Exception("Test exception"), HandledState.ForHandledException(), new List(), new Session()); + + DefaultDelivery.Instance.Send(report); + + var requests = await server.Requests(1); + var request = requests.First(); + + Assert.Contains(request.Headers, h => h.Key == "Bugsnag-Api-Key" && h.Value == API_KEY); + Assert.Contains(request.Headers, h => h.Key == "Bugsnag-Payload-Version" && h.Value == "4"); + Assert.Contains(request.Headers, h => h.Key == "Bugsnag-Sent-At"); + + Assert.Contains($"\"apiKey\":\"{API_KEY}\"", request.Body); + Assert.Contains("\"events\":[", request.Body); + Assert.Contains("\"exceptions\":[", request.Body); + Assert.Contains("\"message\":\"Test exception\"", request.Body); + } + + [Fact] + public async Task Send_BatchedSessionsPayload_SendsCorrectly() + { + var server = new TestServer(); + server.Start(); + + var configuration = new Configuration(API_KEY); + configuration.SessionEndpoint = server.Endpoint; + + var session = new Session(); + var sessionData = new List> { new KeyValuePair(session.SessionKey, 1) }; + var batchedSessions = new BatchedSessions(configuration, sessionData); + + DefaultDelivery.Instance.Send(batchedSessions); + + var requests = await server.Requests(1); + var request = requests.First(); + + Assert.Contains(request.Headers, h => h.Key == "Bugsnag-Api-Key" && h.Value == API_KEY); + Assert.Contains(request.Headers, h => h.Key == "Bugsnag-Payload-Version" && h.Value == "1.0"); + Assert.Contains(request.Headers, h => h.Key == "Bugsnag-Sent-At"); + + Assert.Contains("\"sessionCounts\":[", request.Body); + Assert.Contains($"\"startedAt\":\"{session.SessionKey}\"", request.Body); + Assert.Contains("\"sessionsStarted\":1", request.Body); + } + [Fact] public async Task Send_EnqueuesAndSendsPayloads() { From 609a03d2a1b123c25c6e69b8333d762ac2e47cc0 Mon Sep 17 00:00:00 2001 From: Ahmed Ahmed Date: Tue, 18 Mar 2025 11:37:01 +0000 Subject: [PATCH 4/6] Fix target frameworks for AspNet.Core tests --- .../Bugsnag.AspNet.Core.Tests/Bugsnag.AspNet.Core.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Bugsnag.AspNet.Core.Tests/Bugsnag.AspNet.Core.Tests.csproj b/tests/Bugsnag.AspNet.Core.Tests/Bugsnag.AspNet.Core.Tests.csproj index acd98b67..724e6a3b 100644 --- a/tests/Bugsnag.AspNet.Core.Tests/Bugsnag.AspNet.Core.Tests.csproj +++ b/tests/Bugsnag.AspNet.Core.Tests/Bugsnag.AspNet.Core.Tests.csproj @@ -1,6 +1,6 @@ - net462;net6.0;net8.0 + net6.0;net8.0 win10-x64 false 7.1 From 66ea2251032325dd2c8233450d39caa43fbe78e8 Mon Sep 17 00:00:00 2001 From: Ahmed Ahmed Date: Tue, 18 Mar 2025 15:18:05 +0000 Subject: [PATCH 5/6] restore TreatWarningsAsErrors configuration for non-test projects --- src/Directory.build.props | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Directory.build.props b/src/Directory.build.props index b0f02bf7..09daafc8 100644 --- a/src/Directory.build.props +++ b/src/Directory.build.props @@ -9,7 +9,6 @@ bugsnag.png true true - NU1901;NU1902;NU1903;NU1904 7.1 1591 $(MSBuildThisFileDirectory)..\Bugsnag.snk From 744b37c0cba16d9061ee79438405272918fcb4a6 Mon Sep 17 00:00:00 2001 From: Yousif <74918474+yousif-bugsnag@users.noreply.github.com> Date: Wed, 19 Mar 2025 15:25:19 +0000 Subject: [PATCH 6/6] Fix formatting Co-authored-by: Jason --- src/Bugsnag/DefaultDelivery.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Bugsnag/DefaultDelivery.cs b/src/Bugsnag/DefaultDelivery.cs index de09cbbc..2425b947 100644 --- a/src/Bugsnag/DefaultDelivery.cs +++ b/src/Bugsnag/DefaultDelivery.cs @@ -99,7 +99,9 @@ private async Task SendPayloadAsync(IPayload payload) { byte[] serializedPayload = payload.Serialize(); if (serializedPayload == null) + { return; + } using (var request = new HttpRequestMessage(HttpMethod.Post, payload.Endpoint)) { @@ -121,7 +123,8 @@ private async Task SendPayloadAsync(IPayload payload) // Send the request and log any failures var response = await _httpClient.SendAsync(request); - if (!response.IsSuccessStatusCode) { + if (!response.IsSuccessStatusCode) + { Trace.WriteLine($"Failed to send payload to Bugsnag - received status code {response.StatusCode}"); } }