/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "jucer_ProjucerAnalytics.h" //============================================================================== ProjucerAnalyticsDestination::ProjucerAnalyticsDestination() : ThreadedAnalyticsDestination ("ProjucerAnalyticsThread") { { MemoryOutputStream mo; if (Base64::convertFromBase64 (mo, BinaryData::nothingtoseehere_txt)) apiKey = mo.toString(); } auto dataDir = File::getSpecialLocation (File::userApplicationDataDirectory) #if JUCE_MAC .getChildFile ("Application Support") #endif .getChildFile ("Projucer") .getChildFile ("Analytics"); if (! dataDir.exists()) dataDir.createDirectory(); savedEventsFile = dataDir.getChildFile ("analytics_events.xml"); startAnalyticsThread (initialPeriodMs); } ProjucerAnalyticsDestination::~ProjucerAnalyticsDestination() { Thread::sleep (initialPeriodMs); stopAnalyticsThread (1000); } //============================================================================== static void setData (const AnalyticsDestination::AnalyticsEvent& event, StringPairArray& data) { data.set ("ea", event.name); if (event.parameters.getAllKeys().contains ("label")) data.set ("el", event.parameters.getValue ("label", {})); data.addArray (event.userProperties); } bool ProjucerAnalyticsDestination::logBatchedEvents (const Array& events) { String appData ("v=1&aip=1&tid=" + apiKey); StringArray postData; for (auto& event : events) { StringPairArray data; data.set ("t", "event"); data.set ("cid", event.userID); switch (event.eventType) { case ProjucerAnalyticsEvent::appEvent: { data.set ("ec", "App"); setData (event, data); break; } case ProjucerAnalyticsEvent::projectEvent: { data.set ("ec", "Project"); setData (event, data); break; } case ProjucerAnalyticsEvent::userEvent: { data.set ("ec", "User"); setData (event, data); break; } case ProjucerAnalyticsEvent::exampleEvent: { data.set ("ec", "Example"); setData (event, data); break; } case ProjucerAnalyticsEvent::startPageEvent: { data.set ("ec", "Start Page"); setData (event, data); break; } default: { // unknown event type! jassertfalse; break; } } StringArray eventData; for (auto& key : data.getAllKeys()) eventData.add (key + "=" + URL::addEscapeChars (data[key], true)); postData.add (appData + "&" + eventData.joinIntoString ("&")); } auto url = URL ("https://www.google-analytics.com/batch") .withPOSTData (postData.joinIntoString ("\n")); { const ScopedLock lock (webStreamCreation); if (shouldExit) return false; webStream.reset (new WebInputStream (url, true)); } auto success = webStream->connect (nullptr); // Do an exponential backoff if we failed to connect. if (success) periodMs = initialPeriodMs; else periodMs *= 2; setBatchPeriod (periodMs); return success; } void ProjucerAnalyticsDestination::stopLoggingEvents() { const ScopedLock lock (webStreamCreation); shouldExit = true; if (webStream.get() != nullptr) webStream->cancel(); } //============================================================================== void ProjucerAnalyticsDestination::saveUnloggedEvents (const std::deque& eventsToSave) { XmlDocument previouslySavedEvents (savedEventsFile); std::unique_ptr xml (previouslySavedEvents.getDocumentElement()); if (xml.get() == nullptr || xml->getTagName() != "events") xml.reset (new XmlElement ("events")); for (auto& event : eventsToSave) { auto* xmlEvent = new XmlElement ("google_analytics_event"); xmlEvent->setAttribute ("name", event.name); xmlEvent->setAttribute ("type", event.eventType); xmlEvent->setAttribute ("timestamp", (int) event.timestamp); xmlEvent->setAttribute ("user_id", event.userID); auto* parameters = new XmlElement ("parameters"); for (auto& key : event.parameters.getAllKeys()) parameters->setAttribute (key, event.parameters[key]); xmlEvent->addChildElement (parameters); auto* userProperties = new XmlElement ("user_properties"); for (auto& key : event.userProperties.getAllKeys()) userProperties->setAttribute (key, event.userProperties[key]); xmlEvent->addChildElement (userProperties); xml->addChildElement (xmlEvent); } xml->writeToFile (savedEventsFile, {}); } void ProjucerAnalyticsDestination::restoreUnloggedEvents (std::deque& restoredEventQueue) { XmlDocument savedEvents (savedEventsFile); std::unique_ptr xml (savedEvents.getDocumentElement()); if (xml.get() == nullptr || xml->getTagName() != "events") return; auto numEvents = xml->getNumChildElements(); for (int iEvent = 0; iEvent < numEvents; ++iEvent) { auto* xmlEvent = xml->getChildElement (iEvent); StringPairArray parameters; auto* xmlParameters = xmlEvent->getChildByName ("parameters"); auto numParameters = xmlParameters->getNumAttributes(); for (int iParam = 0; iParam < numParameters; ++iParam) parameters.set (xmlParameters->getAttributeName (iParam), xmlParameters->getAttributeValue (iParam)); StringPairArray userProperties; auto* xmlUserProperties = xmlEvent->getChildByName ("user_properties"); auto numUserProperties = xmlUserProperties->getNumAttributes(); for (int iProp = 0; iProp < numUserProperties; ++iProp) userProperties.set (xmlUserProperties->getAttributeName (iProp), xmlUserProperties->getAttributeValue (iProp)); restoredEventQueue.push_back ({ xmlEvent->getStringAttribute ("name"), xmlEvent->getIntAttribute ("type"), static_cast (xmlEvent->getIntAttribute ("timestamp")), parameters, xmlEvent->getStringAttribute ("user_id"), userProperties }); } savedEventsFile.deleteFile(); }