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.

370 lines
13KB

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