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.

372 lines
13KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE examples.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. The code included in this file is provided under the terms of the ISC license
  6. http://www.isc.org/downloads/software-support-policy/isc-license. Permission
  7. To use, copy, modify, and/or distribute this software for any purpose with or
  8. without fee is hereby granted provided that the above copyright notice and
  9. this permission notice appear in all copies.
  10. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
  11. WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
  12. PURPOSE, ARE DISCLAIMED.
  13. ==============================================================================
  14. */
  15. /*******************************************************************************
  16. The block below describes the properties of this PIP. A PIP is a short snippet
  17. of code that can be read by the Projucer and used to generate a JUCE project.
  18. BEGIN_JUCE_PIP_METADATA
  19. name: AnalyticsCollectionDemo
  20. version: 1.0.0
  21. vendor: JUCE
  22. website: http://juce.com
  23. description: Collects analytics data.
  24. dependencies: juce_analytics, juce_core, juce_data_structures, juce_events,
  25. juce_graphics, juce_gui_basics
  26. exporters: xcode_mac, vs2017, linux_make, xcode_iphone, androidstudio
  27. type: Component
  28. mainClass: AnalyticsCollectionDemo
  29. useLocalCopy: 1
  30. END_JUCE_PIP_METADATA
  31. *******************************************************************************/
  32. #pragma once
  33. //==============================================================================
  34. enum DemoAnalyticsEventTypes
  35. {
  36. event,
  37. sessionStart,
  38. sessionEnd,
  39. screenView,
  40. exception
  41. };
  42. //==============================================================================
  43. class GoogleAnalyticsDestination : public ThreadedAnalyticsDestination
  44. {
  45. public:
  46. GoogleAnalyticsDestination()
  47. : ThreadedAnalyticsDestination ("GoogleAnalyticsThread")
  48. {
  49. {
  50. // Choose where to save any unsent events.
  51. auto appDataDir = File::getSpecialLocation (File::userApplicationDataDirectory)
  52. .getChildFile (JUCEApplication::getInstance()->getApplicationName());
  53. if (! appDataDir.exists())
  54. appDataDir.createDirectory();
  55. savedEventsFile = appDataDir.getChildFile ("analytics_events.xml");
  56. }
  57. {
  58. // It's often a good idea to construct any analytics service API keys
  59. // at runtime, so they're not searchable in the binary distribution of
  60. // your application (but we've not done this here). You should replace
  61. // the following key with your own to get this example application
  62. // fully working.
  63. apiKey = "UA-XXXXXXXXX-1";
  64. }
  65. startAnalyticsThread (initialPeriodMs);
  66. }
  67. ~GoogleAnalyticsDestination()
  68. {
  69. // Here we sleep so that our background thread has a chance to send the
  70. // last lot of batched events. Be careful - if your app takes too long to
  71. // shut down then some operating systems will kill it forcibly!
  72. Thread::sleep (initialPeriodMs);
  73. stopAnalyticsThread (1000);
  74. }
  75. int getMaximumBatchSize() override { return 20; }
  76. bool logBatchedEvents (const Array<AnalyticsEvent>& events) override
  77. {
  78. // Send events to Google Analytics.
  79. String appData ("v=1&aip=1&tid=" + apiKey);
  80. StringArray postData;
  81. for (auto& event : events)
  82. {
  83. StringPairArray data;
  84. switch (event.eventType)
  85. {
  86. case (DemoAnalyticsEventTypes::event):
  87. {
  88. data.set ("t", "event");
  89. if (event.name == "startup")
  90. {
  91. data.set ("ec", "info");
  92. data.set ("ea", "appStarted");
  93. }
  94. else if (event.name == "shutdown")
  95. {
  96. data.set ("ec", "info");
  97. data.set ("ea", "appStopped");
  98. }
  99. else if (event.name == "button_press")
  100. {
  101. data.set ("ec", "button_press");
  102. data.set ("ea", event.parameters["id"]);
  103. }
  104. else if (event.name == "crash")
  105. {
  106. data.set ("ec", "crash");
  107. data.set ("ea", "crash");
  108. }
  109. else
  110. {
  111. jassertfalse;
  112. continue;
  113. }
  114. break;
  115. }
  116. default:
  117. {
  118. // Unknown event type! In this demo app we're just using a
  119. // single event type, but in a real app you probably want to
  120. // handle multiple ones.
  121. jassertfalse;
  122. break;
  123. }
  124. }
  125. data.set ("cid", event.userID);
  126. StringArray eventData;
  127. for (auto& key : data.getAllKeys())
  128. eventData.add (key + "=" + URL::addEscapeChars (data[key], true));
  129. postData.add (appData + "&" + eventData.joinIntoString ("&"));
  130. }
  131. auto url = URL ("https://www.google-analytics.com/batch")
  132. .withPOSTData (postData.joinIntoString ("\n"));
  133. {
  134. const ScopedLock lock (webStreamCreation);
  135. if (shouldExit)
  136. return false;
  137. webStream.reset (new WebInputStream (url, true));
  138. }
  139. auto success = webStream->connect (nullptr);
  140. // Do an exponential backoff if we failed to connect.
  141. if (success)
  142. periodMs = initialPeriodMs;
  143. else
  144. periodMs *= 2;
  145. setBatchPeriod (periodMs);
  146. return success;
  147. }
  148. void stopLoggingEvents() override
  149. {
  150. const ScopedLock lock (webStreamCreation);
  151. shouldExit = true;
  152. if (webStream.get() != nullptr)
  153. webStream->cancel();
  154. }
  155. private:
  156. void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) override
  157. {
  158. // Save unsent events to disk. Here we use XML as a serialisation format, but
  159. // you can use anything else as long as the restoreUnloggedEvents method can
  160. // restore events from disk. If you're saving very large numbers of events then
  161. // a binary format may be more suitable if it is faster - remember that this
  162. // method is called on app shutdown so it needs to complete quickly!
  163. XmlDocument previouslySavedEvents (savedEventsFile);
  164. std::unique_ptr<XmlElement> xml (previouslySavedEvents.getDocumentElement());
  165. if (xml.get() == nullptr || xml->getTagName() != "events")
  166. xml.reset (new XmlElement ("events"));
  167. for (auto& event : eventsToSave)
  168. {
  169. auto* xmlEvent = new XmlElement ("google_analytics_event");
  170. xmlEvent->setAttribute ("name", event.name);
  171. xmlEvent->setAttribute ("type", event.eventType);
  172. xmlEvent->setAttribute ("timestamp", (int) event.timestamp);
  173. xmlEvent->setAttribute ("user_id", event.userID);
  174. auto* parameters = new XmlElement ("parameters");
  175. for (auto& key : event.parameters.getAllKeys())
  176. parameters->setAttribute (key, event.parameters[key]);
  177. xmlEvent->addChildElement (parameters);
  178. auto* userProperties = new XmlElement ("user_properties");
  179. for (auto& key : event.userProperties.getAllKeys())
  180. userProperties->setAttribute (key, event.userProperties[key]);
  181. xmlEvent->addChildElement (userProperties);
  182. xml->addChildElement (xmlEvent);
  183. }
  184. xml->writeToFile (savedEventsFile, {});
  185. }
  186. void restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
  187. {
  188. XmlDocument savedEvents (savedEventsFile);
  189. std::unique_ptr<XmlElement> xml (savedEvents.getDocumentElement());
  190. if (xml.get() == nullptr || xml->getTagName() != "events")
  191. return;
  192. auto numEvents = xml->getNumChildElements();
  193. for (auto iEvent = 0; iEvent < numEvents; ++iEvent)
  194. {
  195. auto* xmlEvent = xml->getChildElement (iEvent);
  196. StringPairArray parameters;
  197. auto* xmlParameters = xmlEvent->getChildByName ("parameters");
  198. auto numParameters = xmlParameters->getNumAttributes();
  199. for (auto iParam = 0; iParam < numParameters; ++iParam)
  200. parameters.set (xmlParameters->getAttributeName (iParam),
  201. xmlParameters->getAttributeValue (iParam));
  202. StringPairArray userProperties;
  203. auto* xmlUserProperties = xmlEvent->getChildByName ("user_properties");
  204. auto numUserProperties = xmlUserProperties->getNumAttributes();
  205. for (auto iProp = 0; iProp < numUserProperties; ++iProp)
  206. userProperties.set (xmlUserProperties->getAttributeName (iProp),
  207. xmlUserProperties->getAttributeValue (iProp));
  208. restoredEventQueue.push_back ({
  209. xmlEvent->getStringAttribute ("name"),
  210. xmlEvent->getIntAttribute ("type"),
  211. static_cast<uint32> (xmlEvent->getIntAttribute ("timestamp")),
  212. parameters,
  213. xmlEvent->getStringAttribute ("user_id"),
  214. userProperties
  215. });
  216. }
  217. savedEventsFile.deleteFile();
  218. }
  219. const int initialPeriodMs = 1000;
  220. int periodMs = initialPeriodMs;
  221. CriticalSection webStreamCreation;
  222. bool shouldExit = false;
  223. std::unique_ptr<WebInputStream> webStream;
  224. String apiKey;
  225. File savedEventsFile;
  226. };
  227. //==============================================================================
  228. class AnalyticsCollectionDemo : public Component
  229. {
  230. public:
  231. //==============================================================================
  232. AnalyticsCollectionDemo()
  233. {
  234. // Add an analytics identifier for the user. Make sure you don't accidentally
  235. // collect identifiable information if you haven't asked for permission!
  236. Analytics::getInstance()->setUserId ("AnonUser1234");
  237. // Add any other constant user information.
  238. StringPairArray userData;
  239. userData.set ("group", "beta");
  240. Analytics::getInstance()->setUserProperties (userData);
  241. // Add any analytics destinations we want to use to the Analytics singleton.
  242. Analytics::getInstance()->addDestination (new GoogleAnalyticsDestination());
  243. // The event type here should probably be DemoAnalyticsEventTypes::sessionStart
  244. // in a more advanced app.
  245. Analytics::getInstance()->logEvent ("startup", {}, DemoAnalyticsEventTypes::event);
  246. crashButton.onClick = [this] { sendCrash(); };
  247. addAndMakeVisible (eventButton);
  248. addAndMakeVisible (crashButton);
  249. setSize (300, 200);
  250. StringPairArray logButtonPressParameters;
  251. logButtonPressParameters.set ("id", "a");
  252. logEventButtonPress.reset (new ButtonTracker (eventButton, "button_press", logButtonPressParameters));
  253. }
  254. ~AnalyticsCollectionDemo()
  255. {
  256. // The event type here should probably be DemoAnalyticsEventTypes::sessionEnd
  257. // in a more advanced app.
  258. Analytics::getInstance()->logEvent ("shutdown", {}, DemoAnalyticsEventTypes::event);
  259. }
  260. void paint (Graphics& g) override
  261. {
  262. g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
  263. }
  264. void resized() override
  265. {
  266. eventButton.centreWithSize (100, 40);
  267. eventButton.setBounds (eventButton.getBounds().translated (0, 25));
  268. crashButton.setBounds (eventButton.getBounds().translated (0, -50));
  269. }
  270. private:
  271. //==============================================================================
  272. void sendCrash()
  273. {
  274. // In a more advanced application you would probably use a different event
  275. // type here.
  276. Analytics::getInstance()->logEvent ("crash", {}, DemoAnalyticsEventTypes::event);
  277. Analytics::getInstance()->getDestinations().clear();
  278. JUCEApplication::getInstance()->shutdown();
  279. }
  280. TextButton eventButton { "Press me!" }, crashButton { "Simulate crash!" };
  281. std::unique_ptr<ButtonTracker> logEventButtonPress;
  282. JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyticsCollectionDemo)
  283. };