Skip to content

Commit 101c49e

Browse files
authored
task(persistence): bring persistence up to spec (#518)
1 parent 5ca2ee6 commit 101c49e

File tree

18 files changed

+328
-345
lines changed

18 files changed

+328
-345
lines changed

features/desktop/persistence.feature

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,11 @@ Feature: Unity Persistence
3838
And I wait for 5 seconds
3939
And I run the game in the "PersistEventReport" state
4040
And I wait to receive 2 errors
41-
And the event "context" equals "First Error"
42-
And the exception "message" equals "First Event"
43-
And I discard the oldest error
4441
And the event "context" equals "Second Error"
4542
And the exception "message" equals "Second Event"
43+
And I discard the oldest error
44+
And the event "context" equals "First Error"
45+
And the exception "message" equals "First Event"
4646

4747

4848
Scenario: Receive a persisted event with on send callback
@@ -52,6 +52,7 @@ Feature: Unity Persistence
5252
And I wait for 5 seconds
5353
And I run the game in the "PersistEventReportCallback" state
5454
And I wait to receive 2 errors
55+
And I discard the oldest error
5556
And the event "context" equals "First Error"
5657
And the exception "message" equals "First Event"
5758

@@ -87,4 +88,20 @@ Feature: Unity Persistence
8788
And the error payload field "events.0.device.id" equals the stored value "device_id"
8889
And the error payload field "events.0.user.id" equals the stored value "device_id"
8990

91+
Scenario: Max Persisted Events
92+
When I run the game in the "ClearBugsnagCache" state
93+
And I wait for 5 seconds
94+
And I run the game in the "MaxPersistEvents" state
95+
And I wait for 5 seconds
96+
And I run the game in the "(noop)" state
97+
And I wait to receive 4 errors
98+
And the exception "message" equals "Event 1"
99+
And I discard the oldest error
100+
And the exception "message" equals "Event 2"
101+
And I discard the oldest error
102+
And the exception "message" equals "Event 3"
103+
And I discard the oldest error
104+
And the exception "message" equals "Event 4"
105+
106+
90107

features/fixtures/maze_runner/Assets/Scripts/Main.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ void PrepareConfigForScenario(Configuration config, string scenario)
138138
{
139139
switch (scenario)
140140
{
141+
case "MaxPersistEvents":
142+
config.MaximumBreadcrumbs = 0;
143+
config.MaxPersistedEvents = 4;
144+
config.AutoDetectErrors = true;
145+
config.AutoTrackSessions = false;
146+
config.Endpoints = new EndpointConfiguration("https://notify.def-not-bugsnag.com", "https://notify.def-not-bugsnag.com");
147+
break;
141148
case "PersistEvent":
142149
config.AutoDetectErrors = true;
143150
config.Endpoints = new EndpointConfiguration("https://notify.def-not-bugsnag.com", "https://notify.def-not-bugsnag.com");
@@ -424,6 +431,9 @@ void RunScenario(string scenario)
424431
{
425432
switch (scenario)
426433
{
434+
case "MaxPersistEvents":
435+
StartCoroutine(NotifyPersistedEvents());
436+
break;
427437
case "PersistDeviceId":
428438
throw new Exception("PersistDeviceId");
429439
case "FeatureFlagsAfterInitClearAll":
@@ -663,6 +673,15 @@ void RunScenario(string scenario)
663673
}
664674
}
665675

676+
private IEnumerator NotifyPersistedEvents()
677+
{
678+
for (int i = 0; i < 5; i++)
679+
{
680+
Bugsnag.Notify(new Exception("Event " + i));
681+
yield return new WaitForSeconds(0.75f);
682+
}
683+
}
684+
666685
private void ClearBugsnagCache()
667686
{
668687
var path = Application.persistentDataPath + "/Bugsnag";

src/BugsnagUnity/Client.cs

Lines changed: 6 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ internal class Client : IClient
2828

2929
private MaximumLogTypeCounter _logTypeCounter;
3030

31-
protected IDelivery Delivery => NativeClient.Delivery;
31+
private Delivery _delivery;
3232

3333
object CallbackLock { get; } = new object();
3434

@@ -52,6 +52,7 @@ internal class Client : IClient
5252
public Client(INativeClient nativeClient)
5353
{
5454
NativeClient = nativeClient;
55+
_delivery = new Delivery(nativeClient.Configuration);
5556
FileManager.InitFileManager(nativeClient.Configuration);
5657
MainThread = Thread.CurrentThread;
5758
SessionTracking = new SessionTracker(this);
@@ -66,7 +67,7 @@ public Client(INativeClient nativeClient)
6667
InitInitialSessionCheck();
6768
CheckForMisconfiguredEndpointsWarning();
6869
AddBugsnagLoadedBreadcrumb();
69-
Delivery.TrySendingCachedPayloads();
70+
_delivery.StartDeliveringCachedPayloads();
7071
}
7172

7273
private void InitFeatureFlags()
@@ -202,7 +203,7 @@ public void Send(IPayload payload)
202203
{
203204
return;
204205
}
205-
Delivery.Send(payload);
206+
_delivery.Deliver(payload);
206207
}
207208

208209
void MultiThreadedNotify(string condition, string stackTrace, LogType logType)
@@ -428,32 +429,10 @@ private void NotifyOnMainThread(Error[] exceptions, HandledState handledState, F
428429
// If the callback causes an exception, ignore it and execute the next one
429430
}
430431

431-
432-
lock (CallbackLock)
433-
{
434-
foreach (var onSendErrorCallback in Configuration.GetOnSendErrorCallbacks())
435-
{
436-
try
437-
{
438-
if (!onSendErrorCallback.Invoke(@event))
439-
{
440-
return;
441-
}
442-
}
443-
catch
444-
{
445-
// If the callback causes an exception, ignore it and execute the next one
446-
}
447-
}
448-
}
449-
450-
@event.RedactMetadata(Configuration);
451-
452432
var report = new Report(Configuration, @event);
453-
454433
if (!report.Ignored)
455434
{
456-
FileManager.AddPendingEvent(report);
435+
FileManager.AddPendingPayload(report);
457436
Send(report);
458437
if (Configuration.IsBreadcrumbTypeEnabled(BreadcrumbType.Error))
459438
{
@@ -497,7 +476,6 @@ public void SetApplicationState(bool inFocus)
497476
if (IsUsingFallback())
498477
{
499478
SessionTracking.StartSession();
500-
Delivery.TrySendingCachedPayloads();
501479
}
502480
else
503481
{
@@ -510,6 +488,7 @@ public void SetApplicationState(bool inFocus)
510488
}
511489
_backgroundStopwatch.Reset();
512490
}
491+
_delivery.StartDeliveringCachedPayloads();
513492
}
514493
else
515494
{

src/BugsnagUnity/Delivery.cs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
using System.Globalization;
5+
using System.IO;
6+
using System.Text;
7+
using BugsnagUnity.Payload;
8+
using UnityEngine;
9+
using UnityEngine.Networking;
10+
11+
namespace BugsnagUnity
12+
{
13+
class Delivery
14+
{
15+
16+
private Configuration _configuration;
17+
private object _callbackLock { get; } = new object();
18+
19+
private static List<string> _finishedCacheDeliveries = new List<string>();
20+
21+
private bool _cacheDeliveryInProcess;
22+
23+
24+
internal Delivery(Configuration configuration)
25+
{
26+
_configuration = configuration;
27+
}
28+
29+
// Run any on send error callbacks if it's an event, serialise the payload and add it to the sending queue
30+
public void Deliver(IPayload payload)
31+
{
32+
if (payload.PayloadType == PayloadType.Event)
33+
{
34+
var report = (Report)payload;
35+
if (_configuration.GetOnSendErrorCallbacks().Count > 0)
36+
{
37+
lock (_callbackLock)
38+
{
39+
foreach (var onSendErrorCallback in _configuration.GetOnSendErrorCallbacks())
40+
{
41+
try
42+
{
43+
if (!onSendErrorCallback.Invoke(report.Event))
44+
{
45+
return;
46+
}
47+
}
48+
catch
49+
{
50+
// If the callback causes an exception, ignore it and execute the next one
51+
}
52+
}
53+
}
54+
}
55+
report.Event.RedactMetadata(_configuration);
56+
report.ApplyEventPayload();
57+
}
58+
try
59+
{
60+
// not avaliable in unit tests
61+
MainThreadDispatchBehaviour.Instance().Enqueue(PushToServer(payload, SerializePayload(payload)));
62+
}
63+
catch { }
64+
}
65+
66+
byte[] SerializePayload(IPayload payload)
67+
{
68+
using (var stream = new MemoryStream())
69+
using (var reader = new StreamReader(stream))
70+
using (var writer = new StreamWriter(stream, new UTF8Encoding(false)) { AutoFlush = false })
71+
{
72+
SimpleJson.SerializeObject(payload, writer);
73+
writer.Flush();
74+
stream.Position = 0;
75+
return Encoding.UTF8.GetBytes(reader.ReadToEnd());
76+
}
77+
}
78+
79+
// Push to the server and handle the result
80+
IEnumerator PushToServer(IPayload payload, byte[] body)
81+
{
82+
using (var req = new UnityWebRequest(payload.Endpoint.ToString()))
83+
{
84+
req.SetRequestHeader("Content-Type", "application/json");
85+
req.SetRequestHeader("Bugsnag-Sent-At", DateTimeOffset.Now.ToString("o", CultureInfo.InvariantCulture));
86+
foreach (var header in payload.Headers)
87+
{
88+
req.SetRequestHeader(header.Key, header.Value);
89+
}
90+
req.uploadHandler = new UploadHandlerRaw(body);
91+
req.downloadHandler = new DownloadHandlerBuffer();
92+
req.method = UnityWebRequest.kHttpVerbPOST;
93+
yield return req.SendWebRequest();
94+
95+
while (!req.isDone)
96+
{
97+
yield return new WaitForEndOfFrame();
98+
}
99+
var code = req.responseCode;
100+
if (code == 200 || code == 202)
101+
{
102+
// success!
103+
FileManager.PayloadSendSuccess(payload);
104+
}
105+
else if (req.isNetworkError || code == 0 || code == 408 || code == 429 || code >= 500)
106+
{
107+
// sending failed with no network or retryable error, cache payload to disk
108+
FileManager.SendPayloadFailed(payload);
109+
}
110+
_finishedCacheDeliveries.Add(payload.Id);
111+
}
112+
}
113+
114+
public void StartDeliveringCachedPayloads()
115+
{
116+
if (_cacheDeliveryInProcess)
117+
{
118+
return;
119+
}
120+
_cacheDeliveryInProcess = true;
121+
try
122+
{
123+
_finishedCacheDeliveries.Clear();
124+
MainThreadDispatchBehaviour.Instance().Enqueue(DeliverCachedPayloads());
125+
}
126+
catch
127+
{
128+
// Not possible in unit tests
129+
}
130+
}
131+
132+
private IEnumerator DeliverCachedPayloads()
133+
{
134+
foreach (var cachedPayloadPath in FileManager.GetCachedPayloadPaths())
135+
{
136+
var payload = FileManager.GetPayloadFromCachePath(cachedPayloadPath);
137+
if (payload != null)
138+
{
139+
Deliver(payload);
140+
yield return new WaitUntil(() => CachedPayloadProcessed(payload.Id));
141+
}
142+
}
143+
_cacheDeliveryInProcess = false;
144+
}
145+
146+
private bool CachedPayloadProcessed(string id)
147+
{
148+
foreach (var processedCachedPayloadIds in _finishedCacheDeliveries)
149+
{
150+
if (id == processedCachedPayloadIds)
151+
{
152+
return true;
153+
}
154+
}
155+
return false;
156+
}
157+
158+
}
159+
}

0 commit comments

Comments
 (0)