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.

218 lines
7.3KB

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