using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
namespace Unity.Cloud.UserReporting.Client
{
///
/// Represents a user reporting client.
///
public class UserReportingClient
{
#region Constructors
///
/// Creates a new instance of the class.
///
/// The endpoint.
/// The project identifier.
/// The platform.
/// The configuration.
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();
this.currentMeasureMetadata = new Dictionary();
this.currentMetrics = new Dictionary();
this.events = new CyclicalList(configuration.MaximumEventCount);
this.measures = new CyclicalList(configuration.MaximumMeasureCount);
this.screenshots = new CyclicalList(configuration.MaximumScreenshotCount);
// Device Metadata
this.deviceMetadata = new List();
foreach (KeyValuePair kvp in this.Platform.GetDeviceMetadata())
{
this.AddDeviceMetadata(kvp.Key, kvp.Value);
}
// Client Version
this.AddDeviceMetadata("UserReportingClientVersion", "2.0");
// Synchronized Action
this.synchronizedActions = new List();
this.currentSynchronizedActions = new List();
// Update Stopwatch
this.updateStopwatch = new Stopwatch();
// Is Connected to Logger
this.IsConnectedToLogger = true;
}
#endregion
#region Fields
private Dictionary clientMetrics;
private Dictionary currentMeasureMetadata;
private Dictionary currentMetrics;
private List currentSynchronizedActions;
private List deviceMetadata;
private CyclicalList events;
private int frameNumber;
private bool isMeasureBoundary;
private int measureFrames;
private CyclicalList measures;
private CyclicalList screenshots;
private int screenshotsSaved;
private int screenshotsTaken;
private List synchronizedActions;
private Stopwatch updateStopwatch;
#endregion
#region Properties
///
/// Gets the configuration.
///
public UserReportingClientConfiguration Configuration { get; private set; }
///
/// Gets the endpoint.
///
public string Endpoint { get; private set; }
///
/// Gets or sets a value indicating whether the client is connected to the logger. If true, log messages will be included in user reports.
///
public bool IsConnectedToLogger { get; set; }
///
/// 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.
///
public bool IsSelfReporting { get; set; }
///
/// Gets the platform.
///
public IUserReportingPlatform Platform { get; private set; }
///
/// Gets the project identifier.
///
public string ProjectIdentifier { get; private set; }
///
/// Gets or sets a value indicating whether user reporting events should be sent to analytics.
///
public bool SendEventsToAnalytics { get; set; }
#endregion
#region Methods
///
/// Adds device metadata.
///
/// The name.
/// The value.
public void AddDeviceMetadata(string name, string value)
{
lock (this.deviceMetadata)
{
UserReportNamedValue userReportNamedValue = new UserReportNamedValue();
userReportNamedValue.Name = name;
userReportNamedValue.Value = value;
this.deviceMetadata.Add(userReportNamedValue);
}
}
///
/// Adds measure metadata. Measure metadata is associated with a period of time.
///
/// The name.
/// The value.
public void AddMeasureMetadata(string name, string value)
{
if (this.currentMeasureMetadata.ContainsKey(name))
{
this.currentMeasureMetadata[name] = value;
}
else
{
this.currentMeasureMetadata.Add(name, value);
}
}
///
/// Adds a synchronized action.
///
/// The action.
private void AddSynchronizedAction(Action action)
{
if (action == null)
{
throw new ArgumentNullException("action");
}
lock (this.synchronizedActions)
{
this.synchronizedActions.Add(action);
}
}
///
/// Clears the screenshots.
///
public void ClearScreenshots()
{
lock (this.screenshots)
{
this.screenshots.Clear();
}
}
///
/// Creates a user report.
///
/// The callback. Provides the user report that was created.
public void CreateUserReport(Action 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 kvp in this.clientMetrics)
{
userReport.ClientMetrics.Add(kvp.Value);
}
// Return
return userReport;
}, (result) => { callback(result as UserReport); });
});
}
///
/// Gets the endpoint.
///
/// The endpoint.
public string GetEndpoint()
{
if (this.Endpoint == null)
{
return "https://localhost";
}
return this.Endpoint.Trim();
}
///
/// Logs an event.
///
/// The level.
/// The message.
public void LogEvent(UserReportEventLevel level, string message)
{
this.LogEvent(level, message, null, null);
}
///
/// Logs an event.
///
/// The level.
/// The message.
/// The stack trace.
public void LogEvent(UserReportEventLevel level, string message, string stackTrace)
{
this.LogEvent(level, message, stackTrace, null);
}
///
/// Logs an event with a stack trace and exception.
///
/// The level.
/// The message.
/// The stack trace.
/// The exception.
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);
}
}
///
/// Logs an exception.
///
/// The exception.
public void LogException(Exception exception)
{
this.LogEvent(UserReportEventLevel.Error, null, null, exception);
}
///
/// Samples a client metric. These metrics are only sample when self reporting is enabled.
///
/// The name.
/// The value.
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);
}
}
///
/// Samples a metric. Metrics can be sampled frequently and have low overhead.
///
/// The name.
/// The value.
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;
}
///
/// Saves a user report to disk.
///
/// The user report.
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);
}
///
/// Sends a user report to the server.
///
/// The user report.
/// 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.
public void SendUserReport(UserReport userReport, Action callback)
{
this.SendUserReport(userReport, null, callback);
}
///
/// Sends a user report to the server.
///
/// The user report.
/// The progress callback. Provides the upload and download progress.
/// 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.
public void SendUserReport(UserReport userReport, Action progressCallback, Action 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(jsonResult);
if (userReportResult != null)
{
if (this.SendEventsToAnalytics)
{
Dictionary eventData = new Dictionary();
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);
}
}
///
/// Takes a screenshot.
///
/// The maximum width.
/// The maximum height.
/// The callback. Provides the screenshot.
public void TakeScreenshot(int maximumWidth, int maximumHeight, Action callback)
{
this.TakeScreenshotFromSource(maximumWidth, maximumHeight, null, callback);
}
///
/// Takes a screenshot.
///
/// The maximum width.
/// The maximum height.
/// 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.
/// The callback. Provides the screenshot.
public void TakeScreenshotFromSource(int maximumWidth, int maximumHeight, object source, Action 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);
}
});
});
}
///
/// Updates the user reporting client, which updates networking communication, screenshotting, and metrics gathering.
///
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();
userReportMeasure.Metrics = new List();
}
userReportMeasure.Metadata.Clear();
userReportMeasure.Metrics.Clear();
foreach (KeyValuePair kvp in this.currentMeasureMetadata)
{
UserReportNamedValue userReportNamedValue = new UserReportNamedValue();
userReportNamedValue.Name = kvp.Key;
userReportNamedValue.Value = kvp.Value;
userReportMeasure.Metadata.Add(userReportNamedValue);
}
foreach (KeyValuePair 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);
}
///
/// Updates the user reporting client at the end of the frame, which updates networking communication, screenshotting, and metrics gathering.
///
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);
}
///
/// Waits for perforation, a boundary between measures when no screenshots are in progress.
///
/// The current screenshots taken.
/// The callback.
private void WaitForPerforation(int currentScreenshotsTaken, Action callback)
{
if (this.screenshotsSaved >= currentScreenshotsTaken && this.isMeasureBoundary)
{
callback();
}
else
{
this.AddSynchronizedAction(() => { this.WaitForPerforation(currentScreenshotsTaken, callback); });
}
}
#endregion
}
}