/* ============================================================================== This file is part of the JUCE 6 technical preview. Copyright (c) 2017 - ROLI Ltd. You may use this code under the terms of the GPL v3 (see www.gnu.org/licenses). For this technical preview, this file is not subject to commercial licensing. 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) { auto xml = parseXMLIfTagMatches (savedEventsFile, "events"); if (xml == nullptr) xml = std::make_unique ("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->writeTo (savedEventsFile, {}); } void ProjucerAnalyticsDestination::restoreUnloggedEvents (std::deque& restoredEventQueue) { auto xml = parseXMLIfTagMatches (savedEventsFile, "events"); if (xml == nullptr) 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(); }