Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@
[assembly: InternalsVisibleTo("GoogleMobileAds.iOS")]
[assembly: InternalsVisibleTo("GoogleMobileAds.Unity")]
[assembly: InternalsVisibleTo("GoogleMobileAdsNative.Api")]
[assembly: InternalsVisibleTo("UnitTests")]
238 changes: 238 additions & 0 deletions source/plugin/Assets/GoogleMobileAds/Common/GlobalExceptionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
using UnityEngine.Networking;

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 : RcsClient<ExceptionReport>
{
private static GlobalExceptionHandler _instance;
public static GlobalExceptionHandler Instance
{
get
{
if (_instance == null && Application.isPlaying)
{
_instance = FindObjectOfType<GlobalExceptionHandler>();
if (_instance == null)
{
_instance = new GameObject("GlobalExceptionHandler")
.AddComponent<GlobalExceptionHandler>();
}
}
return _instance;
}
private set
{
_instance = value;
}
}

#region Unity lifecycle methods
private void Awake()
{
// Enforce the singleton pattern.
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
}

private void OnEnable()
{
Application.logMessageReceivedThreaded += OnLogMessageReceivedThreaded;
}

private void OnDisable()
{
Application.logMessageReceivedThreaded -= OnLogMessageReceivedThreaded;
}
#endregion

#region Public reporting method
/// <summary>
/// Call this from any 'try-catch' block to report a TRAPPED exception.
/// This method is thread-safe and adds the exception to the queue.
/// </summary>
public void ReportTrappedException(Exception e, string name = null)
{
if (e == null) return;

var report = new ExceptionReport
{
time_msec = GetEpochMillis(),
trapped = true,
name = name,
exception_class = e.GetType().FullName,
top_exception = e.Message,
stacktrace = e.StackTrace ?? "",
stacktrace_hash = Sha256Hash(e.StackTrace ?? ""),
};
Enqueue(report);
if (Debug.isDebugBuild)
{
Debug.Log("Trapped exception queued for batch.");
}
}
#endregion

#region Core logic
/// <summary>
/// This callback handles UNTRAPPED exceptions from *any* thread.
/// It must be thread-safe and very fast.
/// </summary>
internal void OnLogMessageReceivedThreaded(string logString, string stackTrace, LogType type)
{
if (type != LogType.Exception) return;

// Parse exception details from the log string.
string topException = logString.Split(new[] { '\n' }, 2)[0].Trim();
string exceptionClass = topException.Split(':')[0].Trim();

var report = new ExceptionReport
{
time_msec = GetEpochMillis(),
trapped = false,
exception_class = exceptionClass,
top_exception = topException,
stacktrace = stackTrace ?? "",
stacktrace_hash = Sha256Hash(stackTrace ?? ""),
};
Enqueue(report);
if (Debug.isDebugBuild)
{
Debug.Log("Untrapped exception queued for batch.");
}
}

private string Sha256Hash(string rawData)
{
try
{
using (SHA256 sha256Hash = SHA256.Create())
{
byte[] bytes = sha256Hash.ComputeHash(Encoding.UTF8.GetBytes(rawData));
// Convert each byte of the computed hash into a two-character hexadecimal
// string.
StringBuilder builder = new StringBuilder();
for (int i = 0; i < bytes.Length; i++)
{
builder.Append(bytes[i].ToString("X2"));
}
return builder.ToString();
}
}
catch (Exception)
{
return "";
}
}

/// <summary>
/// Builds and sends a batch of exception reports.
/// </summary>
protected override void SendBatch(List<ExceptionReport> batch)
{
if (Debug.isDebugBuild)
{
Debug.Log(string.Format("Processing and sending a batch of {0} exceptions...",
batch.Count));
}

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

foreach(var report in batch)
{
report.session_id = staticMetadata.session_id;
report.app_id = staticMetadata.app_id;
report.app_version_name = staticMetadata.app_version_name;
report.platform = staticMetadata.platform;
report.unity_version = staticMetadata.unity_version;
report.os_version = staticMetadata.os_version;
report.device_model = staticMetadata.device_model;
report.country = staticMetadata.country;
report.total_cpu = staticMetadata.total_cpu;
report.total_memory_bytes = staticMetadata.total_memory_bytes;
report.network_type = dynamicMetadata.network_type;
report.orientation = dynamicMetadata.orientation;
}

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

var request = new LoggableRemoteCaptureRequest<ExceptionLoggablePayload>
{
payloads = payloads,
client_ping_metadata = new ClientPingMetadata
{
binary_name = 21, // UNITY_GMA_SDK
}
};
string jspbPayload = JspbConverter.ToJspb(request);
if (jspbPayload != null)
{
SendToRcs(jspbPayload);
}
}
#endregion
}
}
1 change: 1 addition & 0 deletions source/plugin/Assets/GoogleMobileAds/Common/Insight.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private static void CacheBaseProperties()

public Insight()
{
Success = true;
StartTimeEpochMillis = (long)DateTime.UtcNow
.Subtract(new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc))
.TotalMilliseconds;
Expand Down
141 changes: 141 additions & 0 deletions source/plugin/Assets/GoogleMobileAds/Common/JspbConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System.Collections.Generic;
using System.Text;
using UnityEngine;

namespace GoogleMobileAds.Common
{
/// <summary>
/// A helper class to serialize objects to JSPB format.
/// </summary>
internal static class JspbConverter
{
internal static string ToJspb<TPayload>(LoggableRemoteCaptureRequest<TPayload> request)
{
var payloads = new List<string>();
if (request.payloads != null)
{
foreach (var payload in request.payloads)
{
if (payload is ExceptionLoggablePayload)
{
payloads.Add(ToJspb((ExceptionLoggablePayload)(object)payload));
}
else
{
Debug.LogError("JspbConverter encountered an unknown payload type: " +
payload.GetType());
}
}
}
if (payloads.Count == 0)
{
Debug.LogError("No payloads found in the request.");
return null;
}
return string.Format("[[{0}],{1}]",
string.Join(",", payloads.ToArray()),
ToJspb(request.client_ping_metadata));
}

// VisibleForTesting
internal static string ToJspb(ClientPingMetadata metadata)
{
return string.Format("[{0}]", metadata.binary_name);
}

// VisibleForTesting
internal static string QuoteString(string s)
{
if (s == null) return "null";

StringBuilder sb = new StringBuilder();
sb.Append("\"");
foreach (char c in s)
{
switch (c)
{
// Escape quotes and slashes.
case '\"':
sb.Append("\\\"");
break;
case '\\':
sb.Append("\\\\");
break;
case '/':
sb.Append("\\/");
break;
// Escape control characters.
case '\b':
sb.Append("\\b");
break;
case '\f':
sb.Append("\\f");
break;
case '\n':
sb.Append("\\n");
break;
case '\r':
sb.Append("\\r");
break;
case '\t':
sb.Append("\\t");
break;
default:
// Characters within the printable ASCII range (32-126) are appended
// directly. Other characters (control characters or outside ASCII) are
// escaped as Unicode.
int i = (int)c;
if (i < 32 || i > 126)
{
sb.AppendFormat("\\u{0:X4}", i);
}
else
{
sb.Append(c);
}
break;
}
}
sb.Append("\"");
return sb.ToString();
}

#region Exceptions handling
// VisibleForTesting
internal static string ToJspb(ExceptionLoggablePayload payload)
{
if (payload.unity_gma_sdk_exception_message == null) return "[]";

// unity_gma_sdk_exception_message has field index 35.
return string.Format("[{{\"35\":{0}}}]",
ToJspb(payload.unity_gma_sdk_exception_message));
}

// VisibleForTesting
internal static string ToJspb(ExceptionReport report)
{
return string.Format(
"[{0},{1},{2},{3},{4},{5},{6},{7},{8},{9},{10},{11},{12},{13},{14},{15},{16},{17},{18}]",
QuoteString(report.time_msec),
report.trapped.ToString().ToLower(),
QuoteString(report.name),
QuoteString(report.exception_class),
QuoteString(report.top_exception),
QuoteString(report.stacktrace),
QuoteString(report.stacktrace_hash),
QuoteString(report.session_id),
QuoteString(report.app_id),
QuoteString(report.app_version_name),
QuoteString(report.platform),
QuoteString(report.unity_version),
QuoteString(report.os_version),
QuoteString(report.device_model),
QuoteString(report.country),
report.total_cpu,
QuoteString(report.total_memory_bytes),
QuoteString(report.network_type),
QuoteString(report.orientation));
}
#endregion
}
}
Loading