Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
245 changes: 50 additions & 195 deletions source/plugin/Assets/GoogleMobileAds/Common/GlobalExceptionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,52 @@

namespace GoogleMobileAds.Common
{
#region Exception payload definition
[Serializable]
public struct ExceptionLoggablePayload
{
public ExceptionReport unity_gma_sdk_exception_message;
}

/// <summary>
/// A data structure to hold all relevant info for a single exception event.
/// </summary>
[Serializable]
public class ExceptionReport
{
// JSPB compatibility: 64-bit integers must be sent as strings to avoid precision loss.
public string time_msec;
public bool trapped;
public string name;
public string exception_class;
public string top_exception;
public string stacktrace;
public string stacktrace_hash;

// Static metadata.
public string session_id;
public string app_id;
public string app_version_name;
public string platform;
public string unity_version;
public string os_version;
public string device_model;
public string country;
public int total_cpu;
public string total_memory_bytes;

// Dynamic metadata.
public string network_type;
public string orientation;
}
#endregion

/// <summary>
/// A persistent singleton that captures all trapped and untrapped C# exceptions.
/// It enriches them with device metadata and sends them in batches to a backend service (RCS)
/// based on either a count or time threshold.
/// </summary>
public class GlobalExceptionHandler : MonoBehaviour
public class GlobalExceptionHandler : RcsClient<ExceptionReport>
{
private static GlobalExceptionHandler _instance;
public static GlobalExceptionHandler Instance
Expand All @@ -37,74 +77,6 @@ private set
}
}

// Batching triggers are hardcoded constants. We don't need to expose them in Unity Editor.
// If any trigger fires, a batch of exceptions will get sent.
private const int CountThreshold = 50;
private const float TimeThresholdInSeconds = 30.0f;

// RCS endpoint for exception reporting. The `e=1` URL parameter defines JSPB encoding.
// The `f=1` URL parameter indicates that this client is forward compatible with unplanned
// changes to the service's response format.
private const string ProdRcsUrl = "https://pagead2.googlesyndication.com/pagead/ping?e=1&f=1";

internal static readonly Queue<ExceptionReport> _exceptionQueue =
new Queue<ExceptionReport>();
private static readonly object _queueLock = new object();
private float _timeOfNextBatch;

#region Exception payload definition
[Serializable]
public struct LoggableRemoteCaptureRequest
{
public List<LoggablePayload> payloads;
public ClientPingMetadata client_ping_metadata;
}

[Serializable]
public struct LoggablePayload
{
public ExceptionReport unity_gma_sdk_exception_message;
}

/// <summary>
/// A data structure to hold all relevant info for a single exception event.
/// </summary>
[Serializable]
public class ExceptionReport
{
// JSPB compatibility: 64-bit integers must be sent as strings to avoid precision loss.
public string time_msec;
public bool trapped;
public string name;
public string exception_class;
public string top_exception;
public string stacktrace;
public string stacktrace_hash;

// Static metadata.
public string session_id;
public string app_id;
public string app_version_name;
public string platform;
public string unity_version;
public string os_version;
public string device_model;
public string country;
public int total_cpu;
public string total_memory_bytes;

// Dynamic metadata.
public string network_type;
public string orientation;
}

[Serializable]
public struct ClientPingMetadata
{
public int binary_name;
}
#endregion

#region Unity lifecycle methods
private void Awake()
{
Expand All @@ -118,12 +90,6 @@ private void Awake()
DontDestroyOnLoad(gameObject);
}

private void Start()
{
RcsPayload.InitializeStaticMetadata();
ResetBatchTimer();
}

private void OnEnable()
{
Application.logMessageReceivedThreaded += OnLogMessageReceivedThreaded;
Expand All @@ -133,32 +99,6 @@ private void OnDisable()
{
Application.logMessageReceivedThreaded -= OnLogMessageReceivedThreaded;
}

/// <summary>
/// Runs every frame to check if either of our batching triggers has been met.
/// </summary>
private void Update()
{
int count;
lock (_queueLock)
{
count = _exceptionQueue.Count;
}
bool isCountThresholdMet = count >= CountThreshold;
bool isTimeThresholdMet = Time.time >= _timeOfNextBatch;
if (isCountThresholdMet || isTimeThresholdMet)
{
ProcessAndSendBatch();
}
}

/// <summary>
/// Sends pending exceptions before the application quits.
/// </summary>
private void OnApplicationQuit()
{
ProcessAndSendBatch();
}
#endregion

#region Public reporting method
Expand All @@ -180,16 +120,11 @@ public void ReportTrappedException(Exception e, string name = null)
stacktrace = e.StackTrace ?? "",
stacktrace_hash = Sha256Hash(e.StackTrace ?? ""),
};
lock (_queueLock)
{
_exceptionQueue.Enqueue(report);
}

Debug.Log("Trapped exception queued for batch.");
Enqueue(report);
}
#endregion

#region Private core logic
#region Core logic
/// <summary>
/// This callback handles UNTRAPPED exceptions from *any* thread.
/// It must be thread-safe and very fast.
Expand All @@ -211,12 +146,7 @@ internal void OnLogMessageReceivedThreaded(string logString, string stackTrace,
stacktrace = stackTrace ?? "",
stacktrace_hash = Sha256Hash(stackTrace ?? ""),
};
lock (_queueLock)
{
_exceptionQueue.Enqueue(report);
}

Debug.Log("Untrapped exception queued for batch.");
Enqueue(report);
}

private string Sha256Hash(string rawData)
Expand All @@ -243,24 +173,10 @@ private string Sha256Hash(string rawData)
}

/// <summary>
/// Drains the queue and passes the resulting batch to be sent.
/// Builds and sends a batch of exception reports.
/// </summary>
internal void ProcessAndSendBatch()
protected override void SendBatch(List<ExceptionReport> batch)
{
Debug.Log("Processing and sending a batch of exceptions...");

ResetBatchTimer();

List<ExceptionReport> batch = new List<ExceptionReport>();
lock (_queueLock)
{
if (_exceptionQueue.Count == 0) return;
while(_exceptionQueue.Count > 0)
{
batch.Add(_exceptionQueue.Dequeue());
}
}

var staticMetadata = RcsPayload.GetStaticMetadata();
var dynamicMetadata = RcsPayload.GetDynamicMetadata();

Expand All @@ -280,37 +196,16 @@ internal void ProcessAndSendBatch()
report.orientation = dynamicMetadata.orientation;
}

if (batch.Count > 0)
{
SendToRcs(batch);
}
}

/// <summary>
/// Resets the batch timer to the current time plus the threshold.
/// </summary>
private void ResetBatchTimer()
{
_timeOfNextBatch = Time.time + TimeThresholdInSeconds;
}

/// <summary>
/// Builds the final JSON payload (conforming to JSPB rules) and sends it to RCS.
/// </summary>
protected virtual void SendToRcs(List<ExceptionReport> batch)
{
Debug.Log(string.Format("Sending a batch of {0} exception(s)...", batch.Count));

var payloads = new List<LoggablePayload>();
var payloads = new List<ExceptionLoggablePayload>();
foreach (var report in batch)
{
payloads.Add(new LoggablePayload
payloads.Add(new ExceptionLoggablePayload
{
unity_gma_sdk_exception_message = report
});
}

var request = new LoggableRemoteCaptureRequest
var request = new LoggableRemoteCaptureRequest<ExceptionLoggablePayload>
{
payloads = payloads,
client_ping_metadata = new ClientPingMetadata
Expand All @@ -320,47 +215,7 @@ protected virtual void SendToRcs(List<ExceptionReport> batch)
};
// TODO(jochac): Use http://go/jspb-wireformat#message-layout instead.
string jsonPayload = JsonUtility.ToJson(request);
Debug.Log("RCS JSON payload: " + jsonPayload);

StartCoroutine(PostRequest(ProdRcsUrl, jsonPayload));
}

/// <summary>
/// Coroutine to send a JSON payload via HTTP POST.
/// </summary>
private IEnumerator PostRequest(string url, string jsonPayload)
{
using (UnityWebRequest uwr = new UnityWebRequest(url, "POST"))
{
byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonPayload);
uwr.uploadHandler = new UploadHandlerRaw(bodyRaw);
uwr.downloadHandler = new DownloadHandlerBuffer();
uwr.SetRequestHeader("Content-Type", "application/json");

yield return uwr.SendWebRequest();

#if UNITY_2020_2_OR_NEWER
if (uwr.result != UnityWebRequest.Result.Success)
#else
if (uwr.isHttpError || uwr.isNetworkError)
#endif
{
Debug.LogError(string.Format(
"Error sending exception batch: {0} | Response code: {1}.",
uwr.error, uwr.responseCode));
}
else
{
Debug.Log("Exception batch sent successfully.");
}
}
}

private string GetEpochMillis()
{
return ((long)DateTime.UtcNow
.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
.TotalMilliseconds).ToString();
SendToRcs(jsonPayload);
}
#endregion
}
Expand Down
Loading