From f4211751da7c233c4f151eff2d502292775a1ce6 Mon Sep 17 00:00:00 2001 From: Mobile Ads Developer Relations Date: Wed, 5 Nov 2025 14:58:59 -0800 Subject: [PATCH] Refactor exception handler to a RCS client. PiperOrigin-RevId: 828642518 --- .../Common/GlobalExceptionHandler.cs | 245 ++++-------------- .../GoogleMobileAds/Common/RcsClient.cs | 168 ++++++++++++ 2 files changed, 218 insertions(+), 195 deletions(-) create mode 100644 source/plugin/Assets/GoogleMobileAds/Common/RcsClient.cs diff --git a/source/plugin/Assets/GoogleMobileAds/Common/GlobalExceptionHandler.cs b/source/plugin/Assets/GoogleMobileAds/Common/GlobalExceptionHandler.cs index 9c3437ef3..fe6d6c43e 100644 --- a/source/plugin/Assets/GoogleMobileAds/Common/GlobalExceptionHandler.cs +++ b/source/plugin/Assets/GoogleMobileAds/Common/GlobalExceptionHandler.cs @@ -8,12 +8,52 @@ namespace GoogleMobileAds.Common { + #region Exception payload definition + [Serializable] + public struct ExceptionLoggablePayload + { + public ExceptionReport unity_gma_sdk_exception_message; + } + + /// + /// A data structure to hold all relevant info for a single exception event. + /// + [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 + /// /// 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. /// - public class GlobalExceptionHandler : MonoBehaviour + public class GlobalExceptionHandler : RcsClient { private static GlobalExceptionHandler _instance; public static GlobalExceptionHandler Instance @@ -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 _exceptionQueue = - new Queue(); - private static readonly object _queueLock = new object(); - private float _timeOfNextBatch; - - #region Exception payload definition - [Serializable] - public struct LoggableRemoteCaptureRequest - { - public List payloads; - public ClientPingMetadata client_ping_metadata; - } - - [Serializable] - public struct LoggablePayload - { - public ExceptionReport unity_gma_sdk_exception_message; - } - - /// - /// A data structure to hold all relevant info for a single exception event. - /// - [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() { @@ -118,12 +90,6 @@ private void Awake() DontDestroyOnLoad(gameObject); } - private void Start() - { - RcsPayload.InitializeStaticMetadata(); - ResetBatchTimer(); - } - private void OnEnable() { Application.logMessageReceivedThreaded += OnLogMessageReceivedThreaded; @@ -133,32 +99,6 @@ private void OnDisable() { Application.logMessageReceivedThreaded -= OnLogMessageReceivedThreaded; } - - /// - /// Runs every frame to check if either of our batching triggers has been met. - /// - private void Update() - { - int count; - lock (_queueLock) - { - count = _exceptionQueue.Count; - } - bool isCountThresholdMet = count >= CountThreshold; - bool isTimeThresholdMet = Time.time >= _timeOfNextBatch; - if (isCountThresholdMet || isTimeThresholdMet) - { - ProcessAndSendBatch(); - } - } - - /// - /// Sends pending exceptions before the application quits. - /// - private void OnApplicationQuit() - { - ProcessAndSendBatch(); - } #endregion #region Public reporting method @@ -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 /// /// This callback handles UNTRAPPED exceptions from *any* thread. /// It must be thread-safe and very fast. @@ -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) @@ -243,24 +173,10 @@ private string Sha256Hash(string rawData) } /// - /// Drains the queue and passes the resulting batch to be sent. + /// Builds and sends a batch of exception reports. /// - internal void ProcessAndSendBatch() + protected override void SendBatch(List batch) { - Debug.Log("Processing and sending a batch of exceptions..."); - - ResetBatchTimer(); - - List batch = new List(); - lock (_queueLock) - { - if (_exceptionQueue.Count == 0) return; - while(_exceptionQueue.Count > 0) - { - batch.Add(_exceptionQueue.Dequeue()); - } - } - var staticMetadata = RcsPayload.GetStaticMetadata(); var dynamicMetadata = RcsPayload.GetDynamicMetadata(); @@ -280,37 +196,16 @@ internal void ProcessAndSendBatch() report.orientation = dynamicMetadata.orientation; } - if (batch.Count > 0) - { - SendToRcs(batch); - } - } - - /// - /// Resets the batch timer to the current time plus the threshold. - /// - private void ResetBatchTimer() - { - _timeOfNextBatch = Time.time + TimeThresholdInSeconds; - } - - /// - /// Builds the final JSON payload (conforming to JSPB rules) and sends it to RCS. - /// - protected virtual void SendToRcs(List batch) - { - Debug.Log(string.Format("Sending a batch of {0} exception(s)...", batch.Count)); - - var payloads = new List(); + var payloads = new List(); 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 { payloads = payloads, client_ping_metadata = new ClientPingMetadata @@ -320,47 +215,7 @@ protected virtual void SendToRcs(List 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)); - } - - /// - /// Coroutine to send a JSON payload via HTTP POST. - /// - 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 } diff --git a/source/plugin/Assets/GoogleMobileAds/Common/RcsClient.cs b/source/plugin/Assets/GoogleMobileAds/Common/RcsClient.cs new file mode 100644 index 000000000..a22d086d4 --- /dev/null +++ b/source/plugin/Assets/GoogleMobileAds/Common/RcsClient.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using UnityEngine; +using UnityEngine.Networking; + +namespace GoogleMobileAds.Common +{ + [Serializable] + public struct LoggableRemoteCaptureRequest + { + public List payloads; + public ClientPingMetadata client_ping_metadata; + } + + [Serializable] + public struct ClientPingMetadata + { + public int binary_name; + } + + /// + /// An abstract base class for clients that send batches of items to RCS. + /// It handles queueing, batching triggers (count or time), and POSTing data. + /// + public abstract class RcsClient : MonoBehaviour where TReport : class + { + // Batching triggers can be overridden by subclasses. We don't need to expose them in Unity + // Editor. If any trigger fires, a batch of items will get sent. + protected virtual int CountThreshold => 20; + protected virtual float TimeThresholdInSeconds => 120.0f; + + // RCS endpoint for reporting. The `e=1` URL parameter defines JSPB encoding. + private const string ProdRcsUrl = "https://pagead2.googlesyndication.com/pagead/ping?e=1"; + + internal static readonly Queue _queue = new Queue(); + private static readonly object _queueLock = new object(); + private float _timeOfNextBatch; + + /// + /// Initializes the client when it is enabled. + /// + private void Start() + { + RcsPayload.InitializeStaticMetadata(); + ResetBatchTimer(); + } + + /// + /// Runs every frame to check if either of our batching triggers has been met. + /// + private void Update() + { + int count; + lock (_queueLock) + { + count = _queue.Count; + } + bool isCountThresholdMet = count >= CountThreshold; + bool isTimeThresholdMet = Time.time >= _timeOfNextBatch; + if (isCountThresholdMet || isTimeThresholdMet) + { + ProcessAndSendBatch(); + } + } + + /// + /// Sends pending items before the application quits. + /// + private void OnApplicationQuit() + { + ProcessAndSendBatch(); + } + + /// + /// Adds an item to the queue. This method is thread-safe. + /// + protected void Enqueue(TReport item) + { + if (item == null) return; + lock (_queueLock) + { + _queue.Enqueue(item); + } + } + + /// + /// Returns the Unix epoch in milliseconds. + /// + protected string GetEpochMillis() + { + return ((long)DateTime.UtcNow + .Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)) + .TotalMilliseconds).ToString(); + } + + /// + /// Sends the batch of items to RCS. + /// + protected void SendToRcs(string jsonPayload) + { + StartCoroutine(PostRequest(ProdRcsUrl, jsonPayload)); + } + + /// + /// Drains the queue and passes the resulting batch to be sent. + /// + internal void ProcessAndSendBatch() + { + ResetBatchTimer(); + List batch = new List(); + lock (_queueLock) + { + if (_queue.Count == 0) return; + while(_queue.Count > 0) + { + batch.Add(_queue.Dequeue()); + } + } + + if (batch.Count > 0) + { + SendBatch(batch); + } + } + + /// + /// Resets the batch timer to the current time plus the threshold. + /// + private void ResetBatchTimer() + { + _timeOfNextBatch = Time.time + TimeThresholdInSeconds; + } + + /// + /// Coroutine to send a JSON payload via HTTP POST. + /// + 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 batch: {0} | Response code: {1}.", + uwr.error, uwr.responseCode)); + } + } + } + + /// + /// Concrete classes must implement this to process and send a batch of items. + /// + protected abstract void SendBatch(List batch); + } +}