PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/Assets/GameAnalytics/Plugins/Framework/Scripts/GA_Submit.cs

https://bitbucket.org/AgentCodeMonkey/gameframework-unity-project
C# | 531 lines | 296 code | 68 blank | 167 comment | 69 complexity | 585c993f856e523b698b60a92fc71439 MD5 | raw file
  1. /// <summary>
  2. /// This class handles sending data to the Game Analytics servers.
  3. /// JSON data is sent using a MD5 hashed authorization header, containing the JSON data and private key
  4. /// </summary>
  5. using UnityEngine;
  6. using System.Collections;
  7. using System.Collections.Generic;
  8. using System.Security.Cryptography;
  9. using System.Text;
  10. using System;
  11. using LitJson;
  12. using System.Linq;
  13. public class GA_Submit
  14. {
  15. /// <summary>
  16. /// Handlers for success and fail during submit to the GA server
  17. /// </summary>
  18. public delegate void SubmitSuccessHandler(List<Item> items, bool success);
  19. public delegate void SubmitErrorHandler(List<Item> items);
  20. /// <summary>
  21. /// Types of services on the GA server
  22. /// </summary>
  23. public enum CategoryType { GA_User, GA_Event, GA_Log, GA_Purchase }
  24. /// <summary>
  25. /// An item is a message (parameters) and the category (GA service) the message should be sent to
  26. /// </summary>
  27. public struct Item
  28. {
  29. public CategoryType Type;
  30. public Dictionary<string, object> Parameters;
  31. public float AddTime;
  32. public int Count;
  33. }
  34. /// <summary>
  35. /// All the different types of GA services
  36. /// </summary>
  37. public Dictionary<CategoryType, string> Categories = new Dictionary<CategoryType, string>()
  38. {
  39. { CategoryType.GA_User, "user" },
  40. { CategoryType.GA_Event, "design" },
  41. { CategoryType.GA_Log, "quality" },
  42. { CategoryType.GA_Purchase, "business" }
  43. };
  44. #region private values
  45. private string _publicKey;
  46. private string _privateKey;
  47. private string _baseURL = "http://api.gameanalytics.com";
  48. private string _version = "1";
  49. #endregion
  50. #region public methods
  51. /// <summary>
  52. /// Sets the users public and private keys for the GA server
  53. /// </summary>
  54. /// <param name="publicKey">
  55. /// The public key which identifies this users game <see cref="System.String"/>
  56. /// </param>
  57. /// <param name="privateKey">
  58. /// The private key used to encode messages <see cref="System.String"/>
  59. /// </param>
  60. public void SetupKeys(string publicKey, string privateKey)
  61. {
  62. _publicKey = publicKey;
  63. _privateKey = privateKey;
  64. }
  65. /// <summary>
  66. /// Devides a list of messages into categories and calls Submit to send the messages to the GA servers.
  67. /// </summary>
  68. /// <param name="item">
  69. /// The list of messages (queue) <see cref="Item"/>
  70. /// </param>
  71. /// <param name="successEvent">
  72. /// If successful this will be fired <see cref="SubmitSuccessHandler"/>
  73. /// </param>
  74. /// <param name="errorEvent">
  75. /// If an error occurs this will be fired <see cref="SubmitErrorHandler"/>
  76. /// </param>
  77. public void SubmitQueue(List<Item> queue, SubmitSuccessHandler successEvent, SubmitErrorHandler errorEvent)
  78. {
  79. if (_publicKey.Equals("") || _privateKey.Equals(""))
  80. {
  81. GA.LogError("Game Key and/or Secret Key not set. Open GA_Settings to set keys.");
  82. return;
  83. }
  84. //GA_TODO: Optimize by moving dictionary outside this fucntion. Submit is called often
  85. Dictionary<CategoryType, List<Item>> categories = new Dictionary<CategoryType, List<Item>>();
  86. /* Put all the items in the queue into a list containing only the messages of that category type.
  87. * This way we end up with a list of items for each category type */
  88. foreach (Item item in queue)
  89. {
  90. if (categories.ContainsKey(item.Type))
  91. {
  92. /* If we already added another item of this type then remove the UserID, SessionID, and Build values if necessary.
  93. * These values only need to be present in each message once, since they will be the same for all items */
  94. /* TODO: below not supported yet in API (exclude information)
  95. * activate once redundant data can be trimmed */
  96. /*
  97. if (item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]))
  98. item.Parameters.Remove(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]);
  99. if (item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.SessionID]))
  100. item.Parameters.Remove(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.SessionID]);
  101. if (item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.Build]))
  102. item.Parameters.Remove(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.Build]);
  103. */
  104. /* TODO: remove below when API supports exclusion of data */
  105. if (!item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]))
  106. item.Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID], GA.API.GenericInfo.UserID);
  107. if (!item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.SessionID]))
  108. item.Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.SessionID], GA.API.GenericInfo.SessionID);
  109. if (!item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.Build]))
  110. item.Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.Build], GA.Settings.Build);
  111. categories[item.Type].Add(item);
  112. }
  113. else
  114. {
  115. /* If we did not add another item of this type yet, then add the UserID, SessionID, and Build values if necessary.
  116. * These values only need to be present in each message once, since they will be the same for all items */
  117. if (!item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]))
  118. item.Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID], GA.API.GenericInfo.UserID);
  119. if (!item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.SessionID]))
  120. item.Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.SessionID], GA.API.GenericInfo.SessionID);
  121. if (!item.Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.Build]))
  122. item.Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.Build], GA.Settings.Build);
  123. categories.Add(item.Type, new List<Item> { item });
  124. }
  125. }
  126. GA.RunCoroutine(Submit(categories, successEvent, errorEvent));
  127. }
  128. /// <summary>
  129. /// Takes a dictionary with a item list for each category type. All items in each category are submitted together to the GA server.
  130. /// </summary>
  131. /// <param name="item">
  132. /// The list of items, each holding a message and service type <see cref="Item"/>
  133. /// </param>
  134. /// <param name="successEvent">
  135. /// If successful this will be fired <see cref="SubmitSuccessHandler"/>
  136. /// </param>
  137. /// <param name="errorEvent">
  138. /// If an error occurs this will be fired <see cref="SubmitErrorHandler"/>
  139. /// </param>
  140. /// <returns>
  141. /// A <see cref="IEnumerator"/>
  142. /// </returns>
  143. public IEnumerator Submit(Dictionary<CategoryType, List<Item>> categories, SubmitSuccessHandler successEvent, SubmitErrorHandler errorEvent)
  144. {
  145. //For each existing category, submit a message containing all the items of that category type
  146. foreach (KeyValuePair<CategoryType, List<Item>> kvp in categories)
  147. {
  148. List<Item> items = kvp.Value;
  149. if (items.Count == 0)
  150. {
  151. yield break;
  152. }
  153. //Since all the items must have the same category (we make sure they do below) we can get the category from the first item
  154. CategoryType serviceType = items[0].Type;
  155. string url = GetURL(Categories[serviceType]);
  156. //Make sure that all items are of the same category type, and put all the parameter collections into a list
  157. List<Dictionary<string, object>> itemsParameters = new List<Dictionary<string, object>>();
  158. for (int i = 0; i < items.Count; i++)
  159. {
  160. if (serviceType != items[i].Type)
  161. {
  162. GA.LogWarning("GA Error: All messages in a submit must be of the same service/category type.");
  163. if (errorEvent != null)
  164. {
  165. errorEvent(items);
  166. }
  167. yield break;
  168. }
  169. // if user ID is missing from the item add it now (could f.x. happen if custom user id is enabled,
  170. // and the item was added before the custom user id was provided)
  171. if (!items[i].Parameters.ContainsKey(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]))
  172. items[i].Parameters.Add(GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID], GA.API.GenericInfo.UserID);
  173. else if (items[i].Parameters[GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]] == null)
  174. items[i].Parameters[GA_ServerFieldTypes.Fields[GA_ServerFieldTypes.FieldType.UserID]] = GA.API.GenericInfo.UserID;
  175. Dictionary<string, object> parameters;
  176. if (items[i].Count > 1)
  177. {
  178. /* so far we don't do anything special with stacked messages - we just send a single message
  179. * GA_TODO: stacked messages should be handle correctly.*/
  180. parameters = items[i].Parameters;
  181. }
  182. else
  183. {
  184. parameters = items[i].Parameters;
  185. }
  186. itemsParameters.Add(parameters);
  187. }
  188. //Make a JSON array string out of the list of parameter collections
  189. string json = DictToJson(itemsParameters);
  190. /* If we do not have access to a network connection (or we are roaming (mobile devices) and GA_static_api.Settings.ALLOWROAMING is false),
  191. * and data is set to be archived, then archive the data and pretend the message was sent successfully */
  192. if (GA.Settings.ArchiveData && !GA.Settings.InternetConnectivity)
  193. {
  194. if (GA.Settings.DebugMode)
  195. {
  196. GA.Log("GA: Archiving data (no network connection).");
  197. }
  198. GA.API.Archive.ArchiveData(json, serviceType);
  199. if (successEvent != null)
  200. {
  201. successEvent(items, true);
  202. }
  203. yield break;
  204. }
  205. else if (!GA.Settings.InternetConnectivity)
  206. {
  207. GA.LogWarning("GA Error: No network connection.");
  208. if (errorEvent != null)
  209. {
  210. errorEvent(items);
  211. }
  212. yield break;
  213. }
  214. //Prepare the JSON array string for sending by converting it to a byte array
  215. byte[] data = Encoding.ASCII.GetBytes(json);
  216. //Set the authorization header to contain an MD5 hash of the JSON array string + the private key
  217. Hashtable headers = new Hashtable();
  218. headers.Add("Authorization", CreateMD5Hash(json + _privateKey));
  219. //Try to send the data
  220. WWW www = new WWW(url, data, headers);
  221. //Set thread priority low
  222. www.threadPriority = ThreadPriority.Low;
  223. //Wait for response
  224. yield return www;
  225. if (GA.Settings.DebugMode)
  226. {
  227. GA.Log("GA URL: " + url);
  228. GA.Log("GA Submit: " + json);
  229. GA.Log("GA Hash: " + CreateMD5Hash(json + _privateKey));
  230. }
  231. try
  232. {
  233. if (www.error != null && !CheckServerReply(www))
  234. {
  235. throw new Exception(www.error);
  236. }
  237. //Get the JSON object from the response
  238. Dictionary<string, object> returnParam = JsonMapper.ToObject<Dictionary<string, object>>(www.text);
  239. //If the response contains the key "status" with the value "ok" we know that the message was sent and recieved successfully
  240. if ((returnParam != null &&
  241. returnParam.ContainsKey("status") && returnParam["status"].ToString().Equals("ok")) ||
  242. CheckServerReply(www))
  243. {
  244. if (GA.Settings.DebugMode)
  245. {
  246. GA.Log("GA Result: " + www.text);
  247. }
  248. if (successEvent != null)
  249. {
  250. successEvent(items, true);
  251. }
  252. }
  253. else
  254. {
  255. /* The message was not sent and recieved successfully: Stop submitting all together if something
  256. * is completely wrong and we know we will not be able to submit any messages at all..
  257. * Such as missing or invalid public and/or private keys */
  258. if (returnParam != null &&
  259. returnParam.ContainsKey("message") && returnParam["message"].ToString().Equals("Game not found") &&
  260. returnParam.ContainsKey("code") && returnParam["code"].ToString().Equals("400"))
  261. {
  262. GA.LogWarning("GA Error: " + www.text + " (NOTE: make sure your Game Key and Secret Key match the keys you recieved from the Game Analytics website. It might take a few minutes before a newly added game will be able to recieve data.)");
  263. //An error event with a null parameter will stop the GA wrapper from submitting messages
  264. if (errorEvent != null)
  265. {
  266. errorEvent(null);
  267. }
  268. }
  269. else
  270. {
  271. GA.LogWarning("GA Error: " + www.text);
  272. if (errorEvent != null)
  273. {
  274. errorEvent(items);
  275. }
  276. }
  277. }
  278. }
  279. catch (Exception e)
  280. {
  281. GA.LogWarning("GA Error: " + e.Message);
  282. /* If we hit one of these errors we should not attempt to send the message again
  283. * (if necessary we already threw a GA Error which may be tracked) */
  284. if (e.Message.Contains("400 Bad Request"))
  285. {
  286. //An error event with a null parameter will stop the GA wrapper from submitting messages
  287. if (errorEvent != null)
  288. {
  289. errorEvent(null);
  290. }
  291. }
  292. else
  293. {
  294. if (errorEvent != null)
  295. {
  296. errorEvent(items);
  297. }
  298. }
  299. }
  300. }
  301. }
  302. /// <summary>
  303. /// Gets the base url to the GA server
  304. /// </summary>
  305. /// <param name="inclVersion">
  306. /// Should the version be included? <see cref="System.Boolean"/>
  307. /// </param>
  308. /// <returns>
  309. /// A string representing the base url (+ version if inclVersion is true) <see cref="System.String"/>
  310. /// </returns>
  311. public string GetBaseURL(bool inclVersion)
  312. {
  313. if (inclVersion)
  314. return _baseURL + "/" + _version;
  315. return _baseURL;
  316. }
  317. /// <summary>
  318. /// Gets the url on the GA server matching the specific service we are interested in
  319. /// </summary>
  320. /// <param name="category">
  321. /// Determines the GA service/category <see cref="System.String"/>
  322. /// </param>
  323. /// <returns>
  324. /// A string representing the url matching our service choice on the GA server <see cref="System.String"/>
  325. /// </returns>
  326. public string GetURL(string category)
  327. {
  328. return _baseURL + "/" + _version + "/" + _publicKey + "/" + category;
  329. }
  330. /// <summary>
  331. /// Encodes the input as a MD5 hash
  332. /// </summary>
  333. /// <param name="input">
  334. /// The input we want encoded <see cref="System.String"/>
  335. /// </param>
  336. /// <returns>
  337. /// The MD5 hash encoded result of input <see cref="System.String"/>
  338. /// </returns>
  339. public string CreateMD5Hash(string input)
  340. {
  341. // Gets the MD5 hash for input
  342. MD5 md5 = new MD5CryptoServiceProvider();
  343. byte[] data = Encoding.Default.GetBytes(input);
  344. byte[] hash = md5.ComputeHash(data);
  345. // Transforms as hexa
  346. string hexaHash = "";
  347. foreach (byte b in hash) {
  348. hexaHash += String.Format("{0:x2}", b);
  349. }
  350. // Returns MD5 hexa hash as string
  351. return hexaHash;
  352. }
  353. /// <summary>
  354. /// Encodes the input as a MD5 hash
  355. /// </summary>
  356. /// <param name="input">
  357. /// The input we want encoded <see cref="System.String"/>
  358. /// </param>
  359. /// <returns>
  360. /// The MD5 hash encoded result of input <see cref="System.String"/>
  361. /// </returns>
  362. public string CreateMD5Hash(byte[] input)
  363. {
  364. // Gets the MD5 hash for input
  365. MD5 md5 = new MD5CryptoServiceProvider();
  366. byte[] hash = md5.ComputeHash(input);
  367. // Transforms as hexa
  368. string hexaHash = "";
  369. foreach (byte b in hash) {
  370. hexaHash += String.Format("{0:x2}", b);
  371. }
  372. // Returns MD5 hexa hash as string
  373. return hexaHash;
  374. }
  375. /// <summary>
  376. /// Encodes the input as a sha1 hash
  377. /// </summary>
  378. /// <param name="input">
  379. /// The input we want to encoded <see cref="System.String"/>
  380. /// </param>
  381. /// <returns>
  382. /// The sha1 hash encoded result of input <see cref="System.String"/>
  383. /// </returns>
  384. public string CreateSha1Hash(string input)
  385. {
  386. // Gets the sha1 hash for input
  387. SHA1 sha1 = new SHA1CryptoServiceProvider();
  388. byte[] data = Encoding.Default.GetBytes(input);
  389. byte[] hash = sha1.ComputeHash(data);
  390. // Returns sha1 hash as string
  391. return Convert.ToBase64String(hash);
  392. }
  393. public string GetPrivateKey()
  394. {
  395. return _privateKey;
  396. }
  397. #endregion
  398. #region private methods
  399. /// <summary>
  400. /// Check if a reply from the server was accepted. All response codes from 200 to 299 are accepted.
  401. /// </summary>
  402. /// <param name="www">
  403. /// The www object which contains response headers
  404. /// </param>
  405. /// <returns>
  406. /// Return true if response code is from 200 to 299. Otherwise returns false.
  407. /// </returns>
  408. public bool CheckServerReply(WWW www)
  409. {
  410. if (www.error != null)
  411. {
  412. string errStart = www.error.Substring(0, 3);
  413. if (errStart.Equals("201") || errStart.Equals("202") || errStart.Equals("203") || errStart.Equals("204") || errStart.Equals("205") || errStart.Equals("206"))
  414. return true;
  415. }
  416. if (!www.responseHeaders.ContainsKey("STATUS"))
  417. return false;
  418. string status = www.responseHeaders["STATUS"];
  419. string[] splitStatus = status.Split(' ');
  420. int responseCode;
  421. if (splitStatus.Length > 1 && int.TryParse(splitStatus[1], out responseCode))
  422. {
  423. if (responseCode >= 200 && responseCode < 300)
  424. return true;
  425. }
  426. return false;
  427. }
  428. #endregion
  429. /// <summary>
  430. /// Dicts to json. This function is 35% faster than the LitJson library.
  431. /// </summary>
  432. /// <returns>
  433. /// The to json.
  434. /// </returns>
  435. /// <param name='list'>
  436. /// List.
  437. /// </param>
  438. public static string DictToJson(List<Dictionary<string, object>> list)
  439. {
  440. StringBuilder b = new StringBuilder("[");
  441. int d = 0;
  442. int c = 0;
  443. foreach(var dict in list)
  444. {
  445. b.Append('{');
  446. c = 0;
  447. foreach(var key in dict.Keys)
  448. {
  449. c++;
  450. b.AppendFormat("\"{0}\":\"{1}\"", key, dict[key]);
  451. if(c<dict.Keys.Count)
  452. b.Append(',');
  453. }
  454. b.Append('}');
  455. d++;
  456. if(d<list.Count)
  457. b.Append(',');
  458. }
  459. return b.Append("]").ToString();
  460. }
  461. }