Did a few things

This commit is contained in:
2021-07-23 09:45:12 +02:00
parent 3da75e4cc6
commit db537bd7b7
122 changed files with 15101 additions and 619 deletions

View File

@@ -0,0 +1,44 @@
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Provides extensions for working with attachments.
/// </summary>
public static class AttachmentExtensions
{
#region Static Methods
/// <summary>
/// Adds a JSON attachment.
/// </summary>
/// <param name="instance">The extended instance.</param>
/// <param name="name">The name of the attachment.</param>
/// <param name="fileName">The file name.</param>
/// <param name="contents">The contents.</param>
public static void AddJson(this List<UserReportAttachment> instance, string name, string fileName, string contents)
{
if (instance != null)
{
instance.Add(new UserReportAttachment(name, fileName, "application/json", System.Text.Encoding.UTF8.GetBytes(contents)));
}
}
/// <summary>
/// Adds a text attachment.
/// </summary>
/// <param name="instance">The extended instance.</param>
/// <param name="name">The name of the attachment.</param>
/// <param name="fileName">The file name.</param>
/// <param name="contents">The contents.</param>
public static void AddText(this List<UserReportAttachment> instance, string name, string fileName, string contents)
{
if (instance != null)
{
instance.Add(new UserReportAttachment(name, fileName, "text/plain", System.Text.Encoding.UTF8.GetBytes(contents)));
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4af71f750a87f404d96c2fe0ef956ad8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,346 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Unity.Cloud
{
/// <summary>
/// Represents a cyclical list.
/// </summary>
/// <typeparam name="T">The type of the items in the list.</typeparam>
public class CyclicalList<T> : IList<T>
{
#region Nested Types
/// <summary>
/// Represents an enumerator for a cyclical list.
/// </summary>
private struct Enumerator : IEnumerator<T>
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="Enumerator"/> class.
/// </summary>
/// <param name="list">The list.</param>
public Enumerator(CyclicalList<T> list)
{
this.list = list;
this.currentIndex = -1;
}
#endregion
#region Fields
private int currentIndex;
private CyclicalList<T> list;
#endregion
#region Properties
/// <summary>
/// Gets the current item.
/// </summary>
public T Current
{
get
{
if (this.currentIndex < 0 || this.currentIndex >= this.list.Count)
{
return default(T);
}
return this.list[this.currentIndex];
}
}
/// <summary>
/// Gets the current item.
/// </summary>
object IEnumerator.Current
{
get { return this.Current; }
}
#endregion
#region Methods
/// <summary>
/// Disposes of the enumerator.
/// </summary>
public void Dispose()
{
// Empty
}
/// <summary>
/// Moves to the next item.
/// </summary>
/// <returns>A value indicating whether the move was successful.</returns>
public bool MoveNext()
{
this.currentIndex++;
return this.currentIndex < this.list.count;
}
/// <summary>
/// Resets the enumerator.
/// </summary>
public void Reset()
{
this.currentIndex = 0;
}
#endregion
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="CyclicalList{T}"/> class.
/// </summary>
/// <param name="capacity">The capacity.</param>
public CyclicalList(int capacity)
{
this.items = new T[capacity];
}
#endregion
#region Fields
private int count;
private T[] items;
private int nextPointer;
#endregion
#region Properties
/// <summary>
/// Gets the capacity.
/// </summary>
public int Capacity
{
get { return this.items.Length; }
}
/// <summary>
/// Gets the count.
/// </summary>
public int Count
{
get { return this.count; }
}
/// <summary>
/// Gets a value indicating whether the cyclical list is read only.
/// </summary>
public bool IsReadOnly
{
get { return false; }
}
/// <summary>
/// Indexes into the cyclical list.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The item at the specified index.</returns>
public T this[int index]
{
get
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
return this.items[this.GetPointer(index)];
}
set
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
this.items[this.GetPointer(index)] = value;
}
}
#endregion
#region Methods
/// <summary>
/// Adds an item to the cyclical list.
/// </summary>
/// <param name="item">The item.</param>
public void Add(T item)
{
this.items[this.nextPointer] = item;
this.count++;
if (this.count > this.items.Length)
{
this.count = this.items.Length;
}
this.nextPointer++;
if (this.nextPointer >= this.items.Length)
{
this.nextPointer = 0;
}
}
/// <summary>
/// Clears the cyclical list.
/// </summary>
public void Clear()
{
this.count = 0;
this.nextPointer = 0;
}
/// <summary>
/// Determines whether the cyclical list contains the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A value indicating whether the cyclical list contains the specified item.</returns>
public bool Contains(T item)
{
foreach (T listItem in this)
{
if (listItem.Equals(item))
{
return true;
}
}
return false;
}
/// <summary>
/// Copies the cyclical list to an array.
/// </summary>
/// <param name="array">The array.</param>
/// <param name="arrayIndex">The array index.</param>
public void CopyTo(T[] array, int arrayIndex)
{
int i = 0;
foreach (T listItem in this)
{
int currentArrayIndex = arrayIndex + i;
if (currentArrayIndex >= array.Length)
{
break;
}
array[currentArrayIndex] = listItem;
i++;
}
}
/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
public IEnumerator<T> GetEnumerator()
{
return new Enumerator(this);
}
/// <summary>
/// Gets the enumerator.
/// </summary>
/// <returns>The enumerator.</returns>
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
/// <summary>
/// Gets the next eviction.
/// </summary>
/// <returns>The next eviction.</returns>
public T GetNextEviction()
{
return this.items[this.nextPointer];
}
/// <summary>
/// Gets a pointer.
/// </summary>
/// <param name="index">The index.</param>
/// <returns>The pointer.</returns>
private int GetPointer(int index)
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
if (this.count < this.items.Length)
{
return index;
}
return (this.nextPointer + index) % this.count;
}
/// <summary>
/// Gets the index of the specified item.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>The index of the specified item.</returns>
public int IndexOf(T item)
{
int i = 0;
foreach (T listItem in this)
{
if (listItem.Equals(item))
{
return i;
}
i++;
}
return -1;
}
/// <summary>
/// Inserts an item into the cyclical list. This is a no-op on a cyclical list.
/// </summary>
/// <param name="index">The index.</param>
/// <param name="item">The item.</param>
public void Insert(int index, T item)
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
}
/// <summary>
/// Removes an item from the cyclical list.
/// </summary>
/// <param name="item">The item.</param>
/// <returns>A value indicating whether the removal was successful. This is a no-op on a cyclical list.</returns>
public bool Remove(T item)
{
return false;
}
/// <summary>
/// Removes an item from the cyclical list at the specified index. This is a no-op on a cyclical list.
/// </summary>
/// <param name="index">The index.</param>
public void RemoveAt(int index)
{
if (index < 0 || index >= this.count)
{
throw new IndexOutOfRangeException();
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ea7b024ee3157f4682b1e25563f0801
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a user reporting platform.
/// </summary>
public interface IUserReportingPlatform
{
#region Methods
/// <summary>
/// Deserialized the specified JSON.
/// </summary>
/// <typeparam name="T">The type.</typeparam>
/// <param name="json">The JSON.</param>
/// <returns>The deserialized object instance.</returns>
T DeserializeJson<T>(string json);
/// <summary>
/// Gets device metadata.
/// </summary>
/// <returns>Device metadata.</returns>
IDictionary<string, string> GetDeviceMetadata();
/// <summary>
/// Modifies a user report.
/// </summary>
/// <param name="userReport">The user report.</param>
void ModifyUserReport(UserReport userReport);
/// <summary>
/// Called at the end of a frame.
/// </summary>
/// <param name="client">The client.</param>
void OnEndOfFrame(UserReportingClient client);
/// <summary>
/// Posts to an endpoint.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="contentType">The content type.</param>
/// <param name="content">The content.</param>
/// <param name="progressCallback">The progress callback. Provides the upload and download progress.</param>
/// <param name="callback">The callback. Provides a value indicating whether the post was successful and provides the resulting byte array.</param>
void Post(string endpoint, string contentType, byte[] content, Action<float, float> progressCallback, Action<bool, byte[]> callback);
/// <summary>
/// Runs a task asynchronously.
/// </summary>
/// <param name="task">The task.</param>
/// <param name="callback">The callback.</param>
void RunTask(Func<object> task, Action<object> callback);
/// <summary>
/// Sends an analytics event.
/// </summary>
/// <param name="eventName">The event name.</param>
/// <param name="eventData">The event data.</param>
void SendAnalyticsEvent(string eventName, Dictionary<string, object> eventData);
/// <summary>
/// Serializes the specified object instance.
/// </summary>
/// <param name="instance">The object instance.</param>
/// <returns>The JSON.</returns>
string SerializeJson(object instance);
/// <summary>
/// Takes a screenshot.
/// </summary>
/// <param name="frameNumber">The frame number.</param>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="source">The source. Passing null will capture the screen. Passing a camera will capture the camera's view. Passing a render texture will capture the render texture.</param>
/// <param name="callback">The callback. Provides the screenshot.</param>
void TakeScreenshot(int frameNumber, int maximumWidth, int maximumHeight, object source, Action<int, byte[]> callback);
/// <summary>
/// Called on update.
/// </summary>
/// <param name="client">The client.</param>
void Update(UserReportingClient client);
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5e4889c60fb7bfe47a47c238c63badde
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace Unity.Cloud.Authorization
{
public enum LicenseLevel
{
None = 0,
Personal = 10,
Plus = 20,
Pro = 30,
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 61d18ba6de6f7724caf99f1af5eec743
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a metrics gathering mode.
/// </summary>
public enum MetricsGatheringMode
{
/// <summary>
/// Automatic. Some metrics are gathered automatically.
/// </summary>
Automatic = 0,
/// <summary>
/// Manual. No metrics are gathered automatically.
/// </summary>
Manual = 1,
/// <summary>
/// Disabled. No metrics are gathered. Sampling a metric is a no-op.
/// </summary>
Disabled = 2
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b64ba1730b76fd459ba483b0b108bd5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a null user reporting platform.
/// </summary>
public class NullUserReportingPlatform : IUserReportingPlatform
{
#region Methods
/// <inheritdoc cref="IUserReportingPlatform"/>
public T DeserializeJson<T>(string json)
{
return default(T);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public IDictionary<string, string> GetDeviceMetadata()
{
return new Dictionary<string, string>();
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void ModifyUserReport(UserReport userReport)
{
// Empty
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void OnEndOfFrame(UserReportingClient client)
{
// Empty
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Post(string endpoint, string contentType, byte[] content, Action<float, float> progressCallback, Action<bool, byte[]> callback)
{
progressCallback(1, 1);
callback(true, content);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void RunTask(Func<object> task, Action<object> callback)
{
callback(task());
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void SendAnalyticsEvent(string eventName, Dictionary<string, object> eventData)
{
// Empty
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public string SerializeJson(object instance)
{
return string.Empty;
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void TakeScreenshot(int frameNumber, int maximumWidth, int maximumHeight, object source, Action<int, byte[]> callback)
{
callback(frameNumber, new byte[0]);
}
/// <inheritdoc cref="IUserReportingPlatform"/>
public void Update(UserReportingClient client)
{
// Empty
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 53a1dcd097bd55a45a676eced2947d02
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Provides static methods for helping with PNG images.
/// </summary>
public static class PngHelper
{
#region Static Methods
/// <summary>
/// Gets a PNG image's height from base 64 encoded data.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>The height.</returns>
public static int GetPngHeightFromBase64Data(string data)
{
// Preconditions
if (data == null || data.Length < 32)
{
return 0;
}
// Implementation
byte[] bytes = Convert.FromBase64String(data.Substring(0, 32));
byte[] heightBytes = PngHelper.Slice(bytes, 20, 24);
Array.Reverse(heightBytes);
int height = BitConverter.ToInt32(heightBytes, 0);
return height;
}
/// <summary>
/// Gets a PNG image's width from base 64 encoded data.
/// </summary>
/// <param name="data">The data.</param>
/// <returns>The width.</returns>
public static int GetPngWidthFromBase64Data(string data)
{
// Preconditions
if (data == null || data.Length < 32)
{
return 0;
}
// Implementation
byte[] bytes = Convert.FromBase64String(data.Substring(0, 32));
byte[] widthBytes = PngHelper.Slice(bytes, 16, 20);
Array.Reverse(widthBytes);
int width = BitConverter.ToInt32(widthBytes, 0);
return width;
}
/// <summary>
/// Slices a byte array.
/// </summary>
/// <param name="source">The source.</param>
/// <param name="start">The start.</param>
/// <param name="end">The end.</param>
/// <returns>The slices byte array.</returns>
private static byte[] Slice(byte[] source, int start, int end)
{
if (end < 0)
{
end = source.Length + end;
}
int len = end - start;
byte[] res = new byte[len];
for (int i = 0; i < len; i++)
{
res[i] = source[i + start];
}
return res;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c70eccc2beb89c54a93499fd68044b27
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
using System;
namespace Unity.Cloud
{
/// <summary>
/// Provides static methods for helping with preconditions.
/// </summary>
public static class Preconditions
{
#region Static Methods
/// <summary>
/// Ensures that an argument is less than or equal to the specified length.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="length">The length.</param>
/// <param name="argumentName">The argument name.</param>
public static void ArgumentIsLessThanOrEqualToLength(object value, int length, string argumentName)
{
string stringValue = value as string;
if (stringValue != null && stringValue.Length > length)
{
throw new ArgumentException(argumentName);
}
}
/// <summary>
/// Ensures that an argument is not null or whitespace.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="argumentName">The argument name.</param>
public static void ArgumentNotNullOrWhitespace(object value, string argumentName)
{
if (value == null)
{
throw new ArgumentNullException(argumentName);
}
string stringValue = value as string;
if (stringValue != null && stringValue.Trim() == string.Empty)
{
throw new ArgumentNullException(argumentName);
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cf95f7e6c9cf4c74388b0ff0888257c5
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Unity.Cloud
{
/// <summary>
/// Represents a serializable exception.
/// </summary>
public class SerializableException
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="SerializableException"/> class.
/// </summary>
public SerializableException()
{
// Empty
}
/// <summary>
/// Creates a new instance of the <see cref="SerializableException"/> class.
/// </summary>
/// <param name="exception">The exception.</param>
public SerializableException(Exception exception)
{
// Message
this.Message = exception.Message;
// Full Text
this.FullText = exception.ToString();
// Type
Type exceptionType = exception.GetType();
this.Type = exceptionType.FullName;
// Stack Trace
this.StackTrace = new List<SerializableStackFrame>();
StackTrace stackTrace = new StackTrace(exception, true);
foreach (StackFrame stackFrame in stackTrace.GetFrames())
{
this.StackTrace.Add(new SerializableStackFrame(stackFrame));
}
// Problem Identifier
if (this.StackTrace.Count > 0)
{
SerializableStackFrame stackFrame = this.StackTrace[0];
this.ProblemIdentifier = string.Format("{0} at {1}.{2}", this.Type, stackFrame.DeclaringType, stackFrame.MethodName);
}
else
{
this.ProblemIdentifier = this.Type;
}
// Detailed Problem Identifier
if (this.StackTrace.Count > 1)
{
SerializableStackFrame stackFrame1 = this.StackTrace[0];
SerializableStackFrame stackFrame2 = this.StackTrace[1];
this.DetailedProblemIdentifier = string.Format("{0} at {1}.{2} from {3}.{4}", this.Type, stackFrame1.DeclaringType, stackFrame1.MethodName, stackFrame2.DeclaringType, stackFrame2.MethodName);
}
// Inner Exception
if (exception.InnerException != null)
{
this.InnerException = new SerializableException(exception.InnerException);
}
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the detailed problem identifier.
/// </summary>
public string DetailedProblemIdentifier { get; set; }
/// <summary>
/// Gets or sets the full text.
/// </summary>
public string FullText { get; set; }
/// <summary>
/// Gets or sets the inner exception.
/// </summary>
public SerializableException InnerException { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the problem identifier.
/// </summary>
public string ProblemIdentifier { get; set; }
/// <summary>
/// Gets or sets the stack trace.
/// </summary>
public List<SerializableStackFrame> StackTrace { get; set; }
/// <summary>
/// Gets or sets the type.
/// </summary>
public string Type { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1036b9a96f07b7648bf8e729d177e7a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,74 @@
using System;
using System.Diagnostics;
using System.Reflection;
namespace Unity.Cloud
{
/// <summary>
/// Represents a serializable stack frame.
/// </summary>
public class SerializableStackFrame
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="SerializableStackFrame"/> class.
/// </summary>
public SerializableStackFrame()
{
// Empty
}
/// <summary>
/// Creates a new instance of the <see cref="SerializableStackFrame"/> class.
/// </summary>
/// <param name="stackFrame">The stack frame.</param>
public SerializableStackFrame(StackFrame stackFrame)
{
MethodBase method = stackFrame.GetMethod();
Type declaringType = method.DeclaringType;
this.DeclaringType = declaringType != null ? declaringType.FullName : null;
this.Method = method.ToString();
this.MethodName = method.Name;
this.FileName = stackFrame.GetFileName();
this.FileLine = stackFrame.GetFileLineNumber();
this.FileColumn = stackFrame.GetFileColumnNumber();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the declaring type.
/// </summary>
public string DeclaringType { get; set; }
/// <summary>
/// Gets or sets the file column.
/// </summary>
public int FileColumn { get; set; }
/// <summary>
/// Gets or sets the file line.
/// </summary>
public int FileLine { get; set; }
/// <summary>
/// Gets or sets the file name.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the method.
/// </summary>
public string Method { get; set; }
/// <summary>
/// Gets or sets the method name.
/// </summary>
public string MethodName { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dd25aacdb05e8244888888bc4b834af0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,247 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report.
/// </summary>
public class UserReport : UserReportPreview
{
#region Nested Types
/// <summary>
/// Provides sorting for metrics.
/// </summary>
private class UserReportMetricSorter : IComparer<UserReportMetric>
{
#region Methods
/// <inheritdoc />
public int Compare(UserReportMetric x, UserReportMetric y)
{
return string.Compare(x.Name, y.Name, StringComparison.Ordinal);
}
#endregion
}
#endregion
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReport"/> class.
/// </summary>
public UserReport()
{
this.AggregateMetrics = new List<UserReportMetric>();
this.Attachments = new List<UserReportAttachment>();
this.ClientMetrics = new List<UserReportMetric>();
this.DeviceMetadata = new List<UserReportNamedValue>();
this.Events = new List<UserReportEvent>();
this.Fields = new List<UserReportNamedValue>();
this.Measures = new List<UserReportMeasure>();
this.Screenshots = new List<UserReportScreenshot>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the attachments.
/// </summary>
public List<UserReportAttachment> Attachments { get; set; }
/// <summary>
/// Gets or sets the client metrics.
/// </summary>
public List<UserReportMetric> ClientMetrics { get; set; }
/// <summary>
/// Gets or sets the device metadata.
/// </summary>
public List<UserReportNamedValue> DeviceMetadata { get; set; }
/// <summary>
/// Gets or sets the events.
/// </summary>
public List<UserReportEvent> Events { get; set; }
/// <summary>
/// Gets or sets the fields.
/// </summary>
public List<UserReportNamedValue> Fields { get; set; }
/// <summary>
/// Gets or sets the measures.
/// </summary>
public List<UserReportMeasure> Measures { get; set; }
/// <summary>
/// Gets or sets the screenshots.
/// </summary>
public List<UserReportScreenshot> Screenshots { get; set; }
#endregion
#region Methods
/// <summary>
/// Clones the user report.
/// </summary>
/// <returns>The cloned user report.</returns>
public UserReport Clone()
{
UserReport userReport = new UserReport();
userReport.AggregateMetrics = this.AggregateMetrics != null ? this.AggregateMetrics.ToList() : null;
userReport.Attachments = this.Attachments != null ? this.Attachments.ToList() : null;
userReport.ClientMetrics = this.ClientMetrics != null ? this.ClientMetrics.ToList() : null;
userReport.ContentLength = this.ContentLength;
userReport.DeviceMetadata = this.DeviceMetadata != null ? this.DeviceMetadata.ToList() : null;
userReport.Dimensions = this.Dimensions.ToList();
userReport.Events = this.Events != null ? this.Events.ToList() : null;
userReport.ExpiresOn = this.ExpiresOn;
userReport.Fields = this.Fields != null ? this.Fields.ToList() : null;
userReport.Identifier = this.Identifier;
userReport.IPAddress = this.IPAddress;
userReport.Measures = this.Measures != null ? this.Measures.ToList() : null;
userReport.ProjectIdentifier = this.ProjectIdentifier;
userReport.ReceivedOn = this.ReceivedOn;
userReport.Screenshots = this.Screenshots != null ? this.Screenshots.ToList() : null;
userReport.Summary = this.Summary;
userReport.Thumbnail = this.Thumbnail;
return userReport;
}
/// <summary>
/// Completes the user report. This is called by the client and only needs to be called when constructing a user report manually.
/// </summary>
public void Complete()
{
// Thumbnail
if (this.Screenshots.Count > 0)
{
this.Thumbnail = this.Screenshots[this.Screenshots.Count - 1];
}
// Aggregate Metrics
Dictionary<string, UserReportMetric> aggregateMetrics = new Dictionary<string, UserReportMetric>();
foreach (UserReportMeasure measure in this.Measures)
{
foreach (UserReportMetric metric in measure.Metrics)
{
if (!aggregateMetrics.ContainsKey(metric.Name))
{
UserReportMetric userReportMetric = new UserReportMetric();
userReportMetric.Name = metric.Name;
aggregateMetrics.Add(metric.Name, userReportMetric);
}
UserReportMetric aggregateMetric = aggregateMetrics[metric.Name];
aggregateMetric.Sample(metric.Average);
aggregateMetrics[metric.Name] = aggregateMetric;
}
}
if (this.AggregateMetrics == null)
{
this.AggregateMetrics = new List<UserReportMetric>();
}
foreach (KeyValuePair<string, UserReportMetric> kvp in aggregateMetrics)
{
this.AggregateMetrics.Add(kvp.Value);
}
this.AggregateMetrics.Sort(new UserReportMetricSorter());
}
/// <summary>
/// Fixes the user report by replace null lists with empty lists.
/// </summary>
public void Fix()
{
this.AggregateMetrics = this.AggregateMetrics ?? new List<UserReportMetric>();
this.Attachments = this.Attachments ?? new List<UserReportAttachment>();
this.ClientMetrics = this.ClientMetrics ?? new List<UserReportMetric>();
this.DeviceMetadata = this.DeviceMetadata ?? new List<UserReportNamedValue>();
this.Dimensions = this.Dimensions ?? new List<UserReportNamedValue>();
this.Events = this.Events ?? new List<UserReportEvent>();
this.Fields = this.Fields ?? new List<UserReportNamedValue>();
this.Measures = this.Measures ?? new List<UserReportMeasure>();
this.Screenshots = this.Screenshots ?? new List<UserReportScreenshot>();
}
/// <summary>
/// Gets the dimension string for the dimensions associated with this user report.
/// </summary>
/// <returns></returns>
public string GetDimensionsString()
{
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < this.Dimensions.Count; i++)
{
UserReportNamedValue dimension = this.Dimensions[i];
stringBuilder.Append(dimension.Name);
stringBuilder.Append(": ");
stringBuilder.Append(dimension.Value);
if (i != this.Dimensions.Count - 1)
{
stringBuilder.Append(", ");
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Removes screenshots above a certain size from the user report.
/// </summary>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="totalBytes">The total bytes allowed by screenshots.</param>
/// <param name="ignoreCount">The number of screenshots to ignoreCount.</param>
public void RemoveScreenshots(int maximumWidth, int maximumHeight, int totalBytes, int ignoreCount)
{
int byteCount = 0;
for (int i = this.Screenshots.Count; i > 0; i--)
{
if (i < ignoreCount)
{
continue;
}
UserReportScreenshot screenshot = this.Screenshots[i];
byteCount += screenshot.DataBase64.Length;
if (byteCount > totalBytes)
{
break;
}
if (screenshot.Width > maximumWidth || screenshot.Height > maximumHeight)
{
this.Screenshots.RemoveAt(i);
}
}
}
/// <summary>
/// Casts the user report to a user report preview.
/// </summary>
/// <returns>The user report preview.</returns>
public UserReportPreview ToPreview()
{
UserReportPreview userReportPreview = new UserReportPreview();
userReportPreview.AggregateMetrics = this.AggregateMetrics != null ? this.AggregateMetrics.ToList() : null;
userReportPreview.ContentLength = this.ContentLength;
userReportPreview.Dimensions = this.Dimensions != null ? this.Dimensions.ToList() : null;
userReportPreview.ExpiresOn = this.ExpiresOn;
userReportPreview.Identifier = this.Identifier;
userReportPreview.IPAddress = this.IPAddress;
userReportPreview.ProjectIdentifier = this.ProjectIdentifier;
userReportPreview.ReceivedOn = this.ReceivedOn;
userReportPreview.Summary = this.Summary;
userReportPreview.Thumbnail = this.Thumbnail;
return userReportPreview;
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e237c906e28cc824ebeb2d2774f619c0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report appearance hint.
/// </summary>
public enum UserReportAppearanceHint
{
/// <summary>
/// Display normally.
/// </summary>
Normal = 0,
/// <summary>
/// Display landscape.
/// </summary>
Landscape = 1,
/// <summary>
/// Display portrait.
/// </summary>
Portrait = 2,
/// <summary>
/// Display large.
/// </summary>
Large = 3
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1060310e28bbbf942b4e46efd6d76488
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report attachment.
/// </summary>
public struct UserReportAttachment
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportAttachment"/> struct.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="fileName">The file name.</param>
/// <param name="contentType">The content type.</param>
/// <param name="data">The data.</param>
public UserReportAttachment(string name, string fileName, string contentType, byte[] data)
{
this.Name = name;
this.FileName = fileName;
this.ContentType = contentType;
this.DataBase64 = Convert.ToBase64String(data);
this.DataIdentifier = null;
}
#endregion
#region Properties
/// <summary>
/// Get or sets the content type.
/// </summary>
public string ContentType { get; set; }
/// <summary>
/// Gets or sets the data (base 64 encoded).
/// </summary>
public string DataBase64 { get; set; }
/// <summary>
/// Gets or sets the data identifier. This property will be overwritten by the server if provided.
/// </summary>
public string DataIdentifier { get; set; }
/// <summary>
/// Gets or sets the file name.
/// </summary>
public string FileName { get; set; }
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8207a3c1fa17deb429e6402efd1e6725
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,52 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report event.
/// </summary>
public struct UserReportEvent
{
#region Properties
/// <summary>
/// Gets or sets the exception.
/// </summary>
public SerializableException Exception { get; set; }
/// <summary>
/// Gets or sets the frame number.
/// </summary>
public int FrameNumber { get; set; }
/// <summary>
/// Gets or sets the full message.
/// </summary>
public string FullMessage
{
get { return string.Format("{0}{1}{2}", this.Message, Environment.NewLine, this.StackTrace); }
}
/// <summary>
/// Gets or sets the level.
/// </summary>
public UserReportEventLevel Level { get; set; }
/// <summary>
/// Gets or sets the message.
/// </summary>
public string Message { get; set; }
/// <summary>
/// Gets or sets the stack trace.
/// </summary>
public string StackTrace { get; set; }
/// <summary>
/// Gets or sets the timestamp.
/// </summary>
public DateTime Timestamp { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1167bb63e766c824a899a1e17778b1f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,28 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report event level.
/// </summary>
public enum UserReportEventLevel
{
/// <summary>
/// Info.
/// </summary>
Info = 0,
/// <summary>
/// Success.
/// </summary>
Success = 1,
/// <summary>
/// Warning.
/// </summary>
Warning = 2,
/// <summary>
/// Error.
/// </summary>
Error = 3
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 779f88f0c218c3e4db442a004d648705
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,71 @@
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report list.
/// </summary>
public class UserReportList
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportList"/> class.
/// </summary>
public UserReportList()
{
this.UserReportPreviews = new List<UserReportPreview>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the continuation token.
/// </summary>
public string ContinuationToken { get; set; }
/// <summary>
/// Gets or sets the error.
/// </summary>
public string Error { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the list has more items.
/// </summary>
public bool HasMore { get; set; }
/// <summary>
/// Gets or sets the user report previews.
/// </summary>
public List<UserReportPreview> UserReportPreviews { get; set; }
#endregion
#region Methods
/// <summary>
/// Completes the list. This only need to be called by the creator of the list.
/// </summary>
/// <param name="originalLimit">The original limit.</param>
/// <param name="continuationToken">The continuation token.</param>
public void Complete(int originalLimit, string continuationToken)
{
if (this.UserReportPreviews.Count > 0)
{
if (this.UserReportPreviews.Count > originalLimit)
{
while (this.UserReportPreviews.Count > originalLimit)
{
this.UserReportPreviews.RemoveAt(this.UserReportPreviews.Count - 1);
}
this.ContinuationToken = continuationToken;
this.HasMore = true;
}
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dc881d42ec009e14a8c8e119e935daaa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,34 @@
using System.Collections.Generic;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report measure.
/// </summary>
public struct UserReportMeasure
{
#region Properties
/// <summary>
/// Gets or sets the end frame number.
/// </summary>
public int EndFrameNumber { get; set; }
/// <summary>
/// Gets or sets the metadata.
/// </summary>
public List<UserReportNamedValue> Metadata { get; set; }
/// <summary>
/// Gets or sets the metrics.
/// </summary>
public List<UserReportMetric> Metrics { get; set; }
/// <summary>
/// Gets or sets the start frame number.
/// </summary>
public int StartFrameNumber { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df06a0bc122dc6c4fa1badd66c586aa8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,69 @@
using System;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report metrics.
/// </summary>
public struct UserReportMetric
{
#region Properties
/// <summary>
/// Gets the average.
/// </summary>
public double Average
{
get { return this.Sum / this.Count; }
}
/// <summary>
/// Gets the count.
/// </summary>
public int Count { get; set; }
/// <summary>
/// Gets the maximum.
/// </summary>
public double Maximum { get; set; }
/// <summary>
/// Gets the minimum.
/// </summary>
public double Minimum { get; set; }
/// <summary>
/// Gets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets the sum.
/// </summary>
public double Sum { get; set; }
#endregion
#region Methods
/// <summary>
/// Samples a value.
/// </summary>
/// <param name="value">The value.</param>
public void Sample(double value)
{
if (this.Count == 0)
{
this.Minimum = double.MaxValue;
this.Maximum = double.MinValue;
}
this.Count++;
this.Sum += value;
this.Minimum = Math.Min(this.Minimum, value);
this.Maximum = Math.Max(this.Maximum, value);
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fb60fdec80e5deb4d9825895b0cc7432
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,37 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report named value.
/// </summary>
public struct UserReportNamedValue
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportNamedValue"/> class.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public UserReportNamedValue(string name, string value)
{
this.Name = name;
this.Value = value;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the name.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Gets or sets the value.
/// </summary>
public string Value { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 81a8f683f28633f49a066010ec11fc54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,108 @@
using System;
using System.Collections.Generic;
using Unity.Cloud.Authorization;
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report preview or the fly weight version of a user report.
/// </summary>
public class UserReportPreview
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportPreview"/> class.
/// </summary>
public UserReportPreview()
{
this.Dimensions = new List<UserReportNamedValue>();
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the aggregate metrics.
/// </summary>
public List<UserReportMetric> AggregateMetrics { get; set; }
/// <summary>
/// Gets or sets the appearance hint.
/// </summary>
public UserReportAppearanceHint AppearanceHint { get; set; }
/// <summary>
/// Gets or sets the content length. This property will be overwritten by the server if provided.
/// </summary>
public long ContentLength { get; set; }
/// <summary>
/// Gets or sets the dimensions.
/// </summary>
public List<UserReportNamedValue> Dimensions { get; set; }
/// <summary>
/// Gets or sets the time at which the user report expires. This property will be overwritten by the server if provided.
/// </summary>
public DateTime ExpiresOn { get; set; }
/// <summary>
/// Gets or sets the geo country.
/// </summary>
public string GeoCountry { get; set; }
/// <summary>
/// Gets or sets the identifier. This property will be overwritten by the server if provided.
/// </summary>
public string Identifier { get; set; }
/// <summary>
/// Gets or sets the IP address. This property will be overwritten by the server if provided.
/// </summary>
public string IPAddress { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user report is hidden in the UI if a dimension filter is not specified. This is recommended for automated or high volume reports.
/// </summary>
public bool IsHiddenWithoutDimension { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user report is silent. Silent user reports do not send events to integrations. This is recommended for automated or high volume reports.
/// </summary>
public bool IsSilent { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the user report is temporary. Temporary user reports are short lived and not queryable.
/// </summary>
public bool IsTemporary { get; set; }
/// <summary>
/// Gets or sets the license level. This property will be overwritten by the server if provided.
/// </summary>
public LicenseLevel LicenseLevel { get; set; }
/// <summary>
/// Gets or sets the project identifier.
/// </summary>
public string ProjectIdentifier { get; set; }
/// <summary>
/// Gets or sets the time at which the user report was received. This property will be overwritten by the server if provided.
/// </summary>
public DateTime ReceivedOn { get; set; }
/// <summary>
/// Gets or sets the summary.
/// </summary>
public string Summary { get; set; }
/// <summary>
/// Gets or sets the thumbnail. This screenshot will be resized by the server if too large. Keep the last screenshot small in order to reduce report size and increase submission speed.
/// </summary>
public UserReportScreenshot Thumbnail { get; set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6328297e77ead774dbe3da7844b2d13d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
namespace Unity.Cloud.UserReporting
{
/// <summary>
/// Represents a user report screenshot.
/// </summary>
public struct UserReportScreenshot
{
#region Properties
/// <summary>
/// Gets or sets the data (base 64 encoded). Screenshots must be in PNG format.
/// </summary>
public string DataBase64 { get; set; }
/// <summary>
/// Gets or sets the data identifier. This property will be overwritten by the server if provided.
/// </summary>
public string DataIdentifier { get; set; }
/// <summary>
/// Gets or sets the frame number.
/// </summary>
public int FrameNumber { get; set; }
/// <summary>
/// Gets the height.
/// </summary>
public int Height
{
get { return PngHelper.GetPngHeightFromBase64Data(this.DataBase64); }
}
/// <summary>
/// Gets the width.
/// </summary>
public int Width
{
get { return PngHelper.GetPngWidthFromBase64Data(this.DataBase64); }
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b1e5dd7364d072845908195f56c7d791
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,665 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents a user reporting client.
/// </summary>
public class UserReportingClient
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClient"/> class.
/// </summary>
/// <param name="endpoint">The endpoint.</param>
/// <param name="projectIdentifier">The project identifier.</param>
/// <param name="platform">The platform.</param>
/// <param name="configuration">The configuration.</param>
public UserReportingClient(string endpoint, string projectIdentifier, IUserReportingPlatform platform, UserReportingClientConfiguration configuration)
{
// Arguments
this.Endpoint = endpoint;
this.ProjectIdentifier = projectIdentifier;
this.Platform = platform;
this.Configuration = configuration;
// Configuration Clean Up
this.Configuration.FramesPerMeasure = this.Configuration.FramesPerMeasure > 0 ? this.Configuration.FramesPerMeasure : 1;
this.Configuration.MaximumEventCount = this.Configuration.MaximumEventCount > 0 ? this.Configuration.MaximumEventCount : 1;
this.Configuration.MaximumMeasureCount = this.Configuration.MaximumMeasureCount > 0 ? this.Configuration.MaximumMeasureCount : 1;
this.Configuration.MaximumScreenshotCount = this.Configuration.MaximumScreenshotCount > 0 ? this.Configuration.MaximumScreenshotCount : 1;
// Lists
this.clientMetrics = new Dictionary<string, UserReportMetric>();
this.currentMeasureMetadata = new Dictionary<string, string>();
this.currentMetrics = new Dictionary<string, UserReportMetric>();
this.events = new CyclicalList<UserReportEvent>(configuration.MaximumEventCount);
this.measures = new CyclicalList<UserReportMeasure>(configuration.MaximumMeasureCount);
this.screenshots = new CyclicalList<UserReportScreenshot>(configuration.MaximumScreenshotCount);
// Device Metadata
this.deviceMetadata = new List<UserReportNamedValue>();
foreach (KeyValuePair<string, string> kvp in this.Platform.GetDeviceMetadata())
{
this.AddDeviceMetadata(kvp.Key, kvp.Value);
}
// Client Version
this.AddDeviceMetadata("UserReportingClientVersion", "2.0");
// Synchronized Action
this.synchronizedActions = new List<Action>();
this.currentSynchronizedActions = new List<Action>();
// Update Stopwatch
this.updateStopwatch = new Stopwatch();
// Is Connected to Logger
this.IsConnectedToLogger = true;
}
#endregion
#region Fields
private Dictionary<string, UserReportMetric> clientMetrics;
private Dictionary<string, string> currentMeasureMetadata;
private Dictionary<string, UserReportMetric> currentMetrics;
private List<Action> currentSynchronizedActions;
private List<UserReportNamedValue> deviceMetadata;
private CyclicalList<UserReportEvent> events;
private int frameNumber;
private bool isMeasureBoundary;
private int measureFrames;
private CyclicalList<UserReportMeasure> measures;
private CyclicalList<UserReportScreenshot> screenshots;
private int screenshotsSaved;
private int screenshotsTaken;
private List<Action> synchronizedActions;
private Stopwatch updateStopwatch;
#endregion
#region Properties
/// <summary>
/// Gets the configuration.
/// </summary>
public UserReportingClientConfiguration Configuration { get; private set; }
/// <summary>
/// Gets the endpoint.
/// </summary>
public string Endpoint { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether the client is connected to the logger. If true, log messages will be included in user reports.
/// </summary>
public bool IsConnectedToLogger { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the client is self reporting. If true, event and metrics about the client will be included in user reports.
/// </summary>
public bool IsSelfReporting { get; set; }
/// <summary>
/// Gets the platform.
/// </summary>
public IUserReportingPlatform Platform { get; private set; }
/// <summary>
/// Gets the project identifier.
/// </summary>
public string ProjectIdentifier { get; private set; }
/// <summary>
/// Gets or sets a value indicating whether user reporting events should be sent to analytics.
/// </summary>
public bool SendEventsToAnalytics { get; set; }
#endregion
#region Methods
/// <summary>
/// Adds device metadata.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void AddDeviceMetadata(string name, string value)
{
lock (this.deviceMetadata)
{
UserReportNamedValue userReportNamedValue = new UserReportNamedValue();
userReportNamedValue.Name = name;
userReportNamedValue.Value = value;
this.deviceMetadata.Add(userReportNamedValue);
}
}
/// <summary>
/// Adds measure metadata. Measure metadata is associated with a period of time.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void AddMeasureMetadata(string name, string value)
{
if (this.currentMeasureMetadata.ContainsKey(name))
{
this.currentMeasureMetadata[name] = value;
}
else
{
this.currentMeasureMetadata.Add(name, value);
}
}
/// <summary>
/// Adds a synchronized action.
/// </summary>
/// <param name="action">The action.</param>
private void AddSynchronizedAction(Action action)
{
if (action == null)
{
throw new ArgumentNullException("action");
}
lock (this.synchronizedActions)
{
this.synchronizedActions.Add(action);
}
}
/// <summary>
/// Clears the screenshots.
/// </summary>
public void ClearScreenshots()
{
lock (this.screenshots)
{
this.screenshots.Clear();
}
}
/// <summary>
/// Creates a user report.
/// </summary>
/// <param name="callback">The callback. Provides the user report that was created.</param>
public void CreateUserReport(Action<UserReport> callback)
{
this.LogEvent(UserReportEventLevel.Info, "Creating user report.");
this.WaitForPerforation(this.screenshotsTaken, () =>
{
this.Platform.RunTask(() =>
{
// Start Stopwatch
Stopwatch stopwatch = Stopwatch.StartNew();
// Copy Data
UserReport userReport = new UserReport();
userReport.ProjectIdentifier = this.ProjectIdentifier;
// Device Metadata
lock (this.deviceMetadata)
{
userReport.DeviceMetadata = this.deviceMetadata.ToList();
}
// Events
lock (this.events)
{
userReport.Events = this.events.ToList();
}
// Measures
lock (this.measures)
{
userReport.Measures = this.measures.ToList();
}
// Screenshots
lock (this.screenshots)
{
userReport.Screenshots = this.screenshots.ToList();
}
// Complete
userReport.Complete();
// Modify
this.Platform.ModifyUserReport(userReport);
// Stop Stopwatch
stopwatch.Stop();
// Sample Client Metric
this.SampleClientMetric("UserReportingClient.CreateUserReport.Task", stopwatch.ElapsedMilliseconds);
// Copy Client Metrics
foreach (KeyValuePair<string, UserReportMetric> kvp in this.clientMetrics)
{
userReport.ClientMetrics.Add(kvp.Value);
}
// Return
return userReport;
}, (result) => { callback(result as UserReport); });
});
}
/// <summary>
/// Gets the endpoint.
/// </summary>
/// <returns>The endpoint.</returns>
public string GetEndpoint()
{
if (this.Endpoint == null)
{
return "https://localhost";
}
return this.Endpoint.Trim();
}
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
public void LogEvent(UserReportEventLevel level, string message)
{
this.LogEvent(level, message, null, null);
}
/// <summary>
/// Logs an event.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
/// <param name="stackTrace">The stack trace.</param>
public void LogEvent(UserReportEventLevel level, string message, string stackTrace)
{
this.LogEvent(level, message, stackTrace, null);
}
/// <summary>
/// Logs an event with a stack trace and exception.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
/// <param name="stackTrace">The stack trace.</param>
/// <param name="exception">The exception.</param>
private void LogEvent(UserReportEventLevel level, string message, string stackTrace, Exception exception)
{
lock (this.events)
{
UserReportEvent userReportEvent = new UserReportEvent();
userReportEvent.Level = level;
userReportEvent.Message = message;
userReportEvent.FrameNumber = this.frameNumber;
userReportEvent.StackTrace = stackTrace;
userReportEvent.Timestamp = DateTime.UtcNow;
if (exception != null)
{
userReportEvent.Exception = new SerializableException(exception);
}
this.events.Add(userReportEvent);
}
}
/// <summary>
/// Logs an exception.
/// </summary>
/// <param name="exception">The exception.</param>
public void LogException(Exception exception)
{
this.LogEvent(UserReportEventLevel.Error, null, null, exception);
}
/// <summary>
/// Samples a client metric. These metrics are only sample when self reporting is enabled.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void SampleClientMetric(string name, double value)
{
if (double.IsInfinity(value) || double.IsNaN(value))
{
return;
}
if (!this.clientMetrics.ContainsKey(name))
{
UserReportMetric newUserReportMetric = new UserReportMetric();
newUserReportMetric.Name = name;
this.clientMetrics.Add(name, newUserReportMetric);
}
UserReportMetric userReportMetric = this.clientMetrics[name];
userReportMetric.Sample(value);
this.clientMetrics[name] = userReportMetric;
// Self Reporting
if (this.IsSelfReporting)
{
this.SampleMetric(name, value);
}
}
/// <summary>
/// Samples a metric. Metrics can be sampled frequently and have low overhead.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="value">The value.</param>
public void SampleMetric(string name, double value)
{
if (this.Configuration.MetricsGatheringMode == MetricsGatheringMode.Disabled)
{
return;
}
if (double.IsInfinity(value) || double.IsNaN(value))
{
return;
}
if (!this.currentMetrics.ContainsKey(name))
{
UserReportMetric newUserReportMetric = new UserReportMetric();
newUserReportMetric.Name = name;
this.currentMetrics.Add(name, newUserReportMetric);
}
UserReportMetric userReportMetric = this.currentMetrics[name];
userReportMetric.Sample(value);
this.currentMetrics[name] = userReportMetric;
}
/// <summary>
/// Saves a user report to disk.
/// </summary>
/// <param name="userReport">The user report.</param>
public void SaveUserReportToDisk(UserReport userReport)
{
this.LogEvent(UserReportEventLevel.Info, "Saving user report to disk.");
string json = this.Platform.SerializeJson(userReport);
File.WriteAllText("UserReport.json", json);
}
/// <summary>
/// Sends a user report to the server.
/// </summary>
/// <param name="userReport">The user report.</param>
/// <param name="callback">The callback. Provides a value indicating whether sending the user report was successful and provides the user report after it is modified by the server.</param>
public void SendUserReport(UserReport userReport, Action<bool, UserReport> callback)
{
this.SendUserReport(userReport, null, callback);
}
/// <summary>
/// Sends a user report to the server.
/// </summary>
/// <param name="userReport">The user report.</param>
/// <param name="progressCallback">The progress callback. Provides the upload and download progress.</param>
/// <param name="callback">The callback. Provides a value indicating whether sending the user report was successful and provides the user report after it is modified by the server.</param>
public void SendUserReport(UserReport userReport, Action<float, float> progressCallback, Action<bool, UserReport> callback)
{
try
{
if (userReport == null)
{
return;
}
if (userReport.Identifier != null)
{
this.LogEvent(UserReportEventLevel.Warning, "Identifier cannot be set on the client side. The value provided was discarded.");
return;
}
if (userReport.ContentLength != 0)
{
this.LogEvent(UserReportEventLevel.Warning, "ContentLength cannot be set on the client side. The value provided was discarded.");
return;
}
if (userReport.ReceivedOn != default(DateTime))
{
this.LogEvent(UserReportEventLevel.Warning, "ReceivedOn cannot be set on the client side. The value provided was discarded.");
return;
}
if (userReport.ExpiresOn != default(DateTime))
{
this.LogEvent(UserReportEventLevel.Warning, "ExpiresOn cannot be set on the client side. The value provided was discarded.");
return;
}
this.LogEvent(UserReportEventLevel.Info, "Sending user report.");
string json = this.Platform.SerializeJson(userReport);
byte[] jsonData = Encoding.UTF8.GetBytes(json);
string endpoint = this.GetEndpoint();
string url = string.Format(string.Format("{0}/api/userreporting", endpoint));
this.Platform.Post(url, "application/json", jsonData, (uploadProgress, downloadProgress) =>
{
if (progressCallback != null)
{
progressCallback(uploadProgress, downloadProgress);
}
}, (success, result) =>
{
this.AddSynchronizedAction(() =>
{
if (success)
{
try
{
string jsonResult = Encoding.UTF8.GetString(result);
UserReport userReportResult = this.Platform.DeserializeJson<UserReport>(jsonResult);
if (userReportResult != null)
{
if (this.SendEventsToAnalytics)
{
Dictionary<string, object> eventData = new Dictionary<string, object>();
eventData.Add("UserReportIdentifier", userReport.Identifier);
this.Platform.SendAnalyticsEvent("UserReportingClient.SendUserReport", eventData);
}
callback(success, userReportResult);
}
else
{
callback(false, null);
}
}
catch (Exception ex)
{
this.LogEvent(UserReportEventLevel.Error, string.Format("Sending user report failed: {0}", ex.ToString()));
callback(false, null);
}
}
else
{
this.LogEvent(UserReportEventLevel.Error, "Sending user report failed.");
callback(false, null);
}
});
});
}
catch (Exception ex)
{
this.LogEvent(UserReportEventLevel.Error, string.Format("Sending user report failed: {0}", ex.ToString()));
callback(false, null);
}
}
/// <summary>
/// Takes a screenshot.
/// </summary>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="callback">The callback. Provides the screenshot.</param>
public void TakeScreenshot(int maximumWidth, int maximumHeight, Action<UserReportScreenshot> callback)
{
this.TakeScreenshotFromSource(maximumWidth, maximumHeight, null, callback);
}
/// <summary>
/// Takes a screenshot.
/// </summary>
/// <param name="maximumWidth">The maximum width.</param>
/// <param name="maximumHeight">The maximum height.</param>
/// <param name="source">The source. Passing null will capture the screen. Passing a camera will capture the camera's view. Passing a render texture will capture the render texture.</param>
/// <param name="callback">The callback. Provides the screenshot.</param>
public void TakeScreenshotFromSource(int maximumWidth, int maximumHeight, object source, Action<UserReportScreenshot> callback)
{
this.LogEvent(UserReportEventLevel.Info, "Taking screenshot.");
this.screenshotsTaken++;
this.Platform.TakeScreenshot(this.frameNumber, maximumWidth, maximumHeight, source, (passedFrameNumber, data) =>
{
this.AddSynchronizedAction(() =>
{
lock (this.screenshots)
{
UserReportScreenshot userReportScreenshot = new UserReportScreenshot();
userReportScreenshot.FrameNumber = passedFrameNumber;
userReportScreenshot.DataBase64 = Convert.ToBase64String(data);
this.screenshots.Add(userReportScreenshot);
this.screenshotsSaved++;
callback(userReportScreenshot);
}
});
});
}
/// <summary>
/// Updates the user reporting client, which updates networking communication, screenshotting, and metrics gathering.
/// </summary>
public void Update()
{
// Stopwatch
this.updateStopwatch.Reset();
this.updateStopwatch.Start();
// Update Platform
this.Platform.Update(this);
// Measures
if (this.Configuration.MetricsGatheringMode != MetricsGatheringMode.Disabled)
{
this.isMeasureBoundary = false;
int framesPerMeasure = this.Configuration.FramesPerMeasure;
if (this.measureFrames >= framesPerMeasure)
{
lock (this.measures)
{
UserReportMeasure userReportMeasure = new UserReportMeasure();
userReportMeasure.StartFrameNumber = this.frameNumber - framesPerMeasure;
userReportMeasure.EndFrameNumber = this.frameNumber - 1;
UserReportMeasure evictedUserReportMeasure = this.measures.GetNextEviction();
if (evictedUserReportMeasure.Metrics != null)
{
userReportMeasure.Metadata = evictedUserReportMeasure.Metadata;
userReportMeasure.Metrics = evictedUserReportMeasure.Metrics;
}
else
{
userReportMeasure.Metadata = new List<UserReportNamedValue>();
userReportMeasure.Metrics = new List<UserReportMetric>();
}
userReportMeasure.Metadata.Clear();
userReportMeasure.Metrics.Clear();
foreach (KeyValuePair<string, string> kvp in this.currentMeasureMetadata)
{
UserReportNamedValue userReportNamedValue = new UserReportNamedValue();
userReportNamedValue.Name = kvp.Key;
userReportNamedValue.Value = kvp.Value;
userReportMeasure.Metadata.Add(userReportNamedValue);
}
foreach (KeyValuePair<string, UserReportMetric> kvp in this.currentMetrics)
{
userReportMeasure.Metrics.Add(kvp.Value);
}
this.currentMetrics.Clear();
this.measures.Add(userReportMeasure);
this.measureFrames = 0;
this.isMeasureBoundary = true;
}
}
this.measureFrames++;
}
else
{
this.isMeasureBoundary = true;
}
// Synchronization
lock (this.synchronizedActions)
{
foreach (Action synchronizedAction in this.synchronizedActions)
{
this.currentSynchronizedActions.Add(synchronizedAction);
}
this.synchronizedActions.Clear();
}
// Perform Synchronized Actions
foreach (Action synchronizedAction in this.currentSynchronizedActions)
{
synchronizedAction();
}
this.currentSynchronizedActions.Clear();
// Frame Number
this.frameNumber++;
// Stopwatch
this.updateStopwatch.Stop();
this.SampleClientMetric("UserReportingClient.Update", this.updateStopwatch.ElapsedMilliseconds);
}
/// <summary>
/// Updates the user reporting client at the end of the frame, which updates networking communication, screenshotting, and metrics gathering.
/// </summary>
public void UpdateOnEndOfFrame()
{
// Stopwatch
this.updateStopwatch.Reset();
this.updateStopwatch.Start();
// Update Platform
this.Platform.OnEndOfFrame(this);
// Stopwatch
this.updateStopwatch.Stop();
this.SampleClientMetric("UserReportingClient.UpdateOnEndOfFrame", this.updateStopwatch.ElapsedMilliseconds);
}
/// <summary>
/// Waits for perforation, a boundary between measures when no screenshots are in progress.
/// </summary>
/// <param name="currentScreenshotsTaken">The current screenshots taken.</param>
/// <param name="callback">The callback.</param>
private void WaitForPerforation(int currentScreenshotsTaken, Action callback)
{
if (this.screenshotsSaved >= currentScreenshotsTaken && this.isMeasureBoundary)
{
callback();
}
else
{
this.AddSynchronizedAction(() => { this.WaitForPerforation(currentScreenshotsTaken, callback); });
}
}
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 192cdc972af5b47488dec15571884eec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
namespace Unity.Cloud.UserReporting.Client
{
/// <summary>
/// Represents configuration for the user reporting client.
/// </summary>
public class UserReportingClientConfiguration
{
#region Constructors
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClientConfiguration"/> class.
/// </summary>
public UserReportingClientConfiguration()
{
this.MaximumEventCount = 100;
this.MaximumMeasureCount = 300;
this.FramesPerMeasure = 60;
this.MaximumScreenshotCount = 10;
}
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClientConfiguration"/> class.
/// </summary>
/// <param name="maximumEventCount">The maximum event count. This is a rolling window.</param>
/// <param name="maximumMeasureCount">The maximum measure count. This is a rolling window.</param>
/// <param name="framesPerMeasure">The number of frames per measure. A user report is only created on the boundary between measures. A large number of frames per measure will increase user report creation time by this number of frames in the worst case.</param>
/// <param name="maximumScreenshotCount">The maximum screenshot count. This is a rolling window.</param>
public UserReportingClientConfiguration(int maximumEventCount, int maximumMeasureCount, int framesPerMeasure, int maximumScreenshotCount)
{
this.MaximumEventCount = maximumEventCount;
this.MaximumMeasureCount = maximumMeasureCount;
this.FramesPerMeasure = framesPerMeasure;
this.MaximumScreenshotCount = maximumScreenshotCount;
}
/// <summary>
/// Creates a new instance of the <see cref="UserReportingClientConfiguration"/> class.
/// </summary>
/// <param name="maximumEventCount">The maximum event count. This is a rolling window.</param>
/// <param name="metricsGatheringMode">The metrics gathering mode.</param>
/// <param name="maximumMeasureCount">The maximum measure count. This is a rolling window.</param>
/// <param name="framesPerMeasure">The number of frames per measure. A user report is only created on the boundary between measures. A large number of frames per measure will increase user report creation time by this number of frames in the worst case.</param>
/// <param name="maximumScreenshotCount">The maximum screenshot count. This is a rolling window.</param>
public UserReportingClientConfiguration(int maximumEventCount, MetricsGatheringMode metricsGatheringMode, int maximumMeasureCount, int framesPerMeasure, int maximumScreenshotCount)
{
this.MaximumEventCount = maximumEventCount;
this.MetricsGatheringMode = metricsGatheringMode;
this.MaximumMeasureCount = maximumMeasureCount;
this.FramesPerMeasure = framesPerMeasure;
this.MaximumScreenshotCount = maximumScreenshotCount;
}
#endregion
#region Properties
/// <summary>
/// Gets or sets the frames per measure.
/// </summary>
public int FramesPerMeasure { get; internal set; }
/// <summary>
/// Gets or sets the maximum event count.
/// </summary>
public int MaximumEventCount { get; internal set; }
/// <summary>
/// Gets or sets the maximum measure count.
/// </summary>
public int MaximumMeasureCount { get; internal set; }
/// <summary>
/// Gets or sets the maximum screenshot count.
/// </summary>
public int MaximumScreenshotCount { get; internal set; }
/// <summary>
/// Gets or sets the metrics gathering mode.
/// </summary>
public MetricsGatheringMode MetricsGatheringMode { get; internal set; }
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ec0cef74a76c284fa3d592f3005f6f9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: