|  | /*
  ==============================================================================
   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<AnalyticsEvent>& 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<AnalyticsEvent>& eventsToSave)
{
    auto xml = parseXMLIfTagMatches (savedEventsFile, "events");
    if (xml == nullptr)
        xml = std::make_unique<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->writeTo (savedEventsFile, {});
}
void ProjucerAnalyticsDestination::restoreUnloggedEvents (std::deque<AnalyticsEvent>& 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<uint32> (xmlEvent->getIntAttribute ("timestamp")),
            parameters,
            xmlEvent->getStringAttribute ("user_id"),
            userProperties
        });
    }
    savedEventsFile.deleteFile();
}
 |