The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

246 lines
8.4KB

  1. #include "../JuceLibraryCode/JuceHeader.h"
  2. #include "DemoAnalyticsEventTypes.h"
  3. class GoogleAnalyticsDestination : public ThreadedAnalyticsDestination
  4. {
  5. public:
  6. GoogleAnalyticsDestination()
  7. : ThreadedAnalyticsDestination ("GoogleAnalyticsThread")
  8. {
  9. {
  10. // Choose where to save any unsent events.
  11. auto appDataDir = File::getSpecialLocation (File::userApplicationDataDirectory)
  12. .getChildFile (JUCEApplication::getInstance()->getApplicationName());
  13. if (! appDataDir.exists())
  14. appDataDir.createDirectory();
  15. savedEventsFile = appDataDir.getChildFile ("analytics_events.xml");
  16. }
  17. {
  18. // It's often a good idea to construct any analytics service API keys
  19. // at runtime, so they're not searchable in the binary distribution of
  20. // your application (but we've not done this here). You should replace
  21. // the following key with your own to get this example application
  22. // fully working.
  23. apiKey = "UA-XXXXXXXXX-1";
  24. }
  25. startAnalyticsThread (initialPeriodMs);
  26. }
  27. ~GoogleAnalyticsDestination()
  28. {
  29. // Here we sleep so that our background thread has a chance to send the
  30. // last lot of batched events. Be careful - if your app takes too long to
  31. // shut down then some operating systems will kill it forcibly!
  32. Thread::sleep (initialPeriodMs);
  33. stopAnalyticsThread (1000);
  34. }
  35. int getMaximumBatchSize() override { return 20; }
  36. bool logBatchedEvents (const Array<AnalyticsEvent>& events) override
  37. {
  38. // Send events to Google Analytics.
  39. String appData ("v=1&aip=1&tid=" + apiKey);
  40. StringArray postData;
  41. for (auto& event : events)
  42. {
  43. StringPairArray data;
  44. switch (event.eventType)
  45. {
  46. case (DemoAnalyticsEventTypes::event):
  47. {
  48. data.set ("t", "event");
  49. if (event.name == "startup")
  50. {
  51. data.set ("ec", "info");
  52. data.set ("ea", "appStarted");
  53. }
  54. else if (event.name == "shutdown")
  55. {
  56. data.set ("ec", "info");
  57. data.set ("ea", "appStopped");
  58. }
  59. else if (event.name == "button_press")
  60. {
  61. data.set ("ec", "button_press");
  62. data.set ("ea", event.parameters["id"]);
  63. }
  64. else if (event.name == "crash")
  65. {
  66. data.set ("ec", "crash");
  67. data.set ("ea", "crash");
  68. }
  69. else
  70. {
  71. jassertfalse;
  72. continue;
  73. }
  74. break;
  75. }
  76. default:
  77. {
  78. // Unknown event type! In this demo app we're just using a
  79. // single event type, but in a real app you probably want to
  80. // handle multiple ones.
  81. jassertfalse;
  82. break;
  83. }
  84. }
  85. data.set ("cid", event.userID);
  86. StringArray eventData;
  87. for (auto& key : data.getAllKeys())
  88. eventData.add (key + "=" + URL::addEscapeChars (data[key], true));
  89. postData.add (appData + "&" + eventData.joinIntoString ("&"));
  90. }
  91. auto url = URL ("https://www.google-analytics.com/batch")
  92. .withPOSTData (postData.joinIntoString ("\n"));
  93. {
  94. const ScopedLock lock (webStreamCreation);
  95. if (shouldExit)
  96. return false;
  97. webStream = new WebInputStream (url, true);
  98. }
  99. const auto success = webStream->connect (nullptr);
  100. // Do an exponential backoff if we failed to connect.
  101. if (success)
  102. periodMs = initialPeriodMs;
  103. else
  104. periodMs *= 2;
  105. setBatchPeriod (periodMs);
  106. return success;
  107. }
  108. void stopLoggingEvents() override
  109. {
  110. const ScopedLock lock (webStreamCreation);
  111. shouldExit = true;
  112. if (webStream != nullptr)
  113. webStream->cancel();
  114. }
  115. private:
  116. void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) override
  117. {
  118. // Save unsent events to disk. Here we use XML as a serialisation format, but
  119. // you can use anything else as long as the restoreUnloggedEvents method can
  120. // restore events from disk. If you're saving very large numbers of events then
  121. // a binary format may be more suitable if it is faster - remember that this
  122. // method is called on app shutdown so it needs to complete quickly!
  123. XmlDocument previouslySavedEvents (savedEventsFile);
  124. ScopedPointer<XmlElement> xml = previouslySavedEvents.getDocumentElement();
  125. if (xml == nullptr || xml->getTagName() != "events")
  126. xml = new XmlElement ("events");
  127. for (auto& event : eventsToSave)
  128. {
  129. auto* xmlEvent = new XmlElement ("google_analytics_event");
  130. xmlEvent->setAttribute ("name", event.name);
  131. xmlEvent->setAttribute ("type", event.eventType);
  132. xmlEvent->setAttribute ("timestamp", (int) event.timestamp);
  133. xmlEvent->setAttribute ("user_id", event.userID);
  134. auto* parameters = new XmlElement ("parameters");
  135. for (auto& key : event.parameters.getAllKeys())
  136. parameters->setAttribute (key, event.parameters[key]);
  137. xmlEvent->addChildElement (parameters);
  138. auto* userProperties = new XmlElement ("user_properties");
  139. for (auto& key : event.userProperties.getAllKeys())
  140. userProperties->setAttribute (key, event.userProperties[key]);
  141. xmlEvent->addChildElement (userProperties);
  142. xml->addChildElement (xmlEvent);
  143. }
  144. xml->writeToFile (savedEventsFile, {});
  145. }
  146. void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
  147. {
  148. XmlDocument savedEvents (savedEventsFile);
  149. ScopedPointer<XmlElement> xml = savedEvents.getDocumentElement();
  150. if (xml == nullptr || xml->getTagName() != "events")
  151. return;
  152. const auto numEvents = xml->getNumChildElements();
  153. for (auto iEvent = 0; iEvent < numEvents; ++iEvent)
  154. {
  155. const auto* xmlEvent = xml->getChildElement (iEvent);
  156. StringPairArray parameters;
  157. const auto* xmlParameters = xmlEvent->getChildByName ("parameters");
  158. const auto numParameters = xmlParameters->getNumAttributes();
  159. for (auto iParam = 0; iParam < numParameters; ++iParam)
  160. parameters.set (xmlParameters->getAttributeName (iParam),
  161. xmlParameters->getAttributeValue (iParam));
  162. StringPairArray userProperties;
  163. const auto* xmlUserProperties = xmlEvent->getChildByName ("user_properties");
  164. const auto numUserProperties = xmlUserProperties->getNumAttributes();
  165. for (auto iProp = 0; iProp < numUserProperties; ++iProp)
  166. userProperties.set (xmlUserProperties->getAttributeName (iProp),
  167. xmlUserProperties->getAttributeValue (iProp));
  168. restoredEventQueue.push_back ({
  169. xmlEvent->getStringAttribute ("name"),
  170. xmlEvent->getIntAttribute ("type"),
  171. (uint32) xmlEvent->getIntAttribute ("timestamp"),
  172. parameters,
  173. xmlEvent->getStringAttribute ("user_id"),
  174. userProperties
  175. });
  176. }
  177. savedEventsFile.deleteFile();
  178. }
  179. const int initialPeriodMs = 1000;
  180. int periodMs = initialPeriodMs;
  181. CriticalSection webStreamCreation;
  182. bool shouldExit = false;
  183. ScopedPointer<WebInputStream> webStream;
  184. String apiKey;
  185. File savedEventsFile;
  186. };