subrepo: subdir: "deps/juce" merged: "b13f9084e" upstream: origin: "https://github.com/essej/JUCE.git" branch: "sono6good" commit: "b13f9084e" git-subrepo: version: "0.4.3" origin: "https://github.com/ingydotnet/git-subrepo.git" commit: "2f68596"
		
			
				
	
	
		
			370 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			370 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
/*
 | 
						|
  ==============================================================================
 | 
						|
 | 
						|
   This file is part of the JUCE examples.
 | 
						|
   Copyright (c) 2020 - Raw Material Software Limited
 | 
						|
 | 
						|
   The code included in this file is provided under the terms of the ISC license
 | 
						|
   http://www.isc.org/downloads/software-support-policy/isc-license. Permission
 | 
						|
   To use, copy, modify, and/or distribute this software for any purpose with or
 | 
						|
   without fee is hereby granted provided that the above copyright notice and
 | 
						|
   this permission notice appear in all copies.
 | 
						|
 | 
						|
   THE SOFTWARE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES,
 | 
						|
   WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR
 | 
						|
   PURPOSE, ARE DISCLAIMED.
 | 
						|
 | 
						|
  ==============================================================================
 | 
						|
*/
 | 
						|
 | 
						|
/*******************************************************************************
 | 
						|
 The block below describes the properties of this PIP. A PIP is a short snippet
 | 
						|
 of code that can be read by the Projucer and used to generate a JUCE project.
 | 
						|
 | 
						|
 BEGIN_JUCE_PIP_METADATA
 | 
						|
 | 
						|
 name:             AnalyticsCollectionDemo
 | 
						|
 version:          1.0.0
 | 
						|
 vendor:           JUCE
 | 
						|
 website:          http://juce.com
 | 
						|
 description:      Collects analytics data.
 | 
						|
 | 
						|
 dependencies:     juce_analytics, juce_core, juce_data_structures, juce_events,
 | 
						|
                   juce_graphics, juce_gui_basics
 | 
						|
 exporters:        xcode_mac, vs2019, linux_make, xcode_iphone, androidstudio
 | 
						|
 | 
						|
 moduleFlags:      JUCE_STRICT_REFCOUNTEDPOINTER=1
 | 
						|
 | 
						|
 type:             Component
 | 
						|
 mainClass:        AnalyticsCollectionDemo
 | 
						|
 | 
						|
 useLocalCopy:     1
 | 
						|
 | 
						|
 END_JUCE_PIP_METADATA
 | 
						|
 | 
						|
*******************************************************************************/
 | 
						|
 | 
						|
#pragma once
 | 
						|
 | 
						|
 | 
						|
//==============================================================================
 | 
						|
enum DemoAnalyticsEventTypes
 | 
						|
{
 | 
						|
    event,
 | 
						|
    sessionStart,
 | 
						|
    sessionEnd,
 | 
						|
    screenView,
 | 
						|
    exception
 | 
						|
};
 | 
						|
 | 
						|
//==============================================================================
 | 
						|
class GoogleAnalyticsDestination  : public ThreadedAnalyticsDestination
 | 
						|
{
 | 
						|
public:
 | 
						|
    GoogleAnalyticsDestination()
 | 
						|
        : ThreadedAnalyticsDestination ("GoogleAnalyticsThread")
 | 
						|
    {
 | 
						|
        {
 | 
						|
            // Choose where to save any unsent events.
 | 
						|
 | 
						|
            auto appDataDir = File::getSpecialLocation (File::userApplicationDataDirectory)
 | 
						|
                                   .getChildFile (JUCEApplication::getInstance()->getApplicationName());
 | 
						|
 | 
						|
            if (! appDataDir.exists())
 | 
						|
                appDataDir.createDirectory();
 | 
						|
 | 
						|
            savedEventsFile = appDataDir.getChildFile ("analytics_events.xml");
 | 
						|
        }
 | 
						|
 | 
						|
        {
 | 
						|
            // It's often a good idea to construct any analytics service API keys
 | 
						|
            // at runtime, so they're not searchable in the binary distribution of
 | 
						|
            // your application (but we've not done this here). You should replace
 | 
						|
            // the following key with your own to get this example application
 | 
						|
            // fully working.
 | 
						|
 | 
						|
            apiKey = "UA-XXXXXXXXX-1";
 | 
						|
        }
 | 
						|
 | 
						|
        startAnalyticsThread (initialPeriodMs);
 | 
						|
    }
 | 
						|
 | 
						|
    ~GoogleAnalyticsDestination() override
 | 
						|
    {
 | 
						|
        // Here we sleep so that our background thread has a chance to send the
 | 
						|
        // last lot of batched events. Be careful - if your app takes too long to
 | 
						|
        // shut down then some operating systems will kill it forcibly!
 | 
						|
        Thread::sleep (initialPeriodMs);
 | 
						|
 | 
						|
        stopAnalyticsThread (1000);
 | 
						|
    }
 | 
						|
 | 
						|
    int getMaximumBatchSize() override   { return 20; }
 | 
						|
 | 
						|
    bool logBatchedEvents (const Array<AnalyticsEvent>& events) override
 | 
						|
    {
 | 
						|
        // Send events to Google Analytics.
 | 
						|
 | 
						|
        String appData ("v=1&aip=1&tid=" + apiKey);
 | 
						|
 | 
						|
        StringArray postData;
 | 
						|
 | 
						|
        for (auto& event : events)
 | 
						|
        {
 | 
						|
            StringPairArray data;
 | 
						|
 | 
						|
            switch (event.eventType)
 | 
						|
            {
 | 
						|
                case (DemoAnalyticsEventTypes::event):
 | 
						|
                {
 | 
						|
                    data.set ("t", "event");
 | 
						|
 | 
						|
                    if (event.name == "startup")
 | 
						|
                    {
 | 
						|
                        data.set ("ec",  "info");
 | 
						|
                        data.set ("ea",  "appStarted");
 | 
						|
                    }
 | 
						|
                    else if (event.name == "shutdown")
 | 
						|
                    {
 | 
						|
                        data.set ("ec",  "info");
 | 
						|
                        data.set ("ea",  "appStopped");
 | 
						|
                    }
 | 
						|
                    else if (event.name == "button_press")
 | 
						|
                    {
 | 
						|
                        data.set ("ec",  "button_press");
 | 
						|
                        data.set ("ea",  event.parameters["id"]);
 | 
						|
                    }
 | 
						|
                    else if (event.name == "crash")
 | 
						|
                    {
 | 
						|
                        data.set ("ec",  "crash");
 | 
						|
                        data.set ("ea",  "crash");
 | 
						|
                    }
 | 
						|
                    else
 | 
						|
                    {
 | 
						|
                        jassertfalse;
 | 
						|
                        continue;
 | 
						|
                    }
 | 
						|
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
 | 
						|
                default:
 | 
						|
                {
 | 
						|
                    // Unknown event type! In this demo app we're just using a
 | 
						|
                    // single event type, but in a real app you probably want to
 | 
						|
                    // handle multiple ones.
 | 
						|
                    jassertfalse;
 | 
						|
                    break;
 | 
						|
                }
 | 
						|
            }
 | 
						|
 | 
						|
            data.set ("cid", event.userID);
 | 
						|
 | 
						|
            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 stopLoggingEvents() override
 | 
						|
    {
 | 
						|
        const ScopedLock lock (webStreamCreation);
 | 
						|
 | 
						|
        shouldExit = true;
 | 
						|
 | 
						|
        if (webStream.get() != nullptr)
 | 
						|
            webStream->cancel();
 | 
						|
    }
 | 
						|
 | 
						|
private:
 | 
						|
    void saveUnloggedEvents (const std::deque<AnalyticsEvent>& eventsToSave) override
 | 
						|
    {
 | 
						|
        // Save unsent events to disk. Here we use XML as a serialisation format, but
 | 
						|
        // you can use anything else as long as the restoreUnloggedEvents method can
 | 
						|
        // restore events from disk. If you're saving very large numbers of events then
 | 
						|
        // a binary format may be more suitable if it is faster - remember that this
 | 
						|
        // method is called on app shutdown so it needs to complete quickly!
 | 
						|
 | 
						|
        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 restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
 | 
						|
    {
 | 
						|
        if (auto xml = parseXMLIfTagMatches (savedEventsFile, "events"))
 | 
						|
        {
 | 
						|
            auto numEvents = xml->getNumChildElements();
 | 
						|
 | 
						|
            for (auto iEvent = 0; iEvent < numEvents; ++iEvent)
 | 
						|
            {
 | 
						|
                auto* xmlEvent = xml->getChildElement (iEvent);
 | 
						|
 | 
						|
                StringPairArray parameters;
 | 
						|
                auto* xmlParameters = xmlEvent->getChildByName ("parameters");
 | 
						|
                auto numParameters = xmlParameters->getNumAttributes();
 | 
						|
 | 
						|
                for (auto 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 (auto 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();
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    const int initialPeriodMs = 1000;
 | 
						|
    int periodMs = initialPeriodMs;
 | 
						|
 | 
						|
    CriticalSection webStreamCreation;
 | 
						|
    bool shouldExit = false;
 | 
						|
    std::unique_ptr<WebInputStream> webStream;
 | 
						|
 | 
						|
    String apiKey;
 | 
						|
 | 
						|
    File savedEventsFile;
 | 
						|
};
 | 
						|
 | 
						|
//==============================================================================
 | 
						|
class AnalyticsCollectionDemo   : public Component
 | 
						|
{
 | 
						|
public:
 | 
						|
    //==============================================================================
 | 
						|
    AnalyticsCollectionDemo()
 | 
						|
    {
 | 
						|
        // Add an analytics identifier for the user. Make sure you don't accidentally
 | 
						|
        // collect identifiable information if you haven't asked for permission!
 | 
						|
        Analytics::getInstance()->setUserId ("AnonUser1234");
 | 
						|
 | 
						|
        // Add any other constant user information.
 | 
						|
        StringPairArray userData;
 | 
						|
        userData.set ("group", "beta");
 | 
						|
        Analytics::getInstance()->setUserProperties (userData);
 | 
						|
 | 
						|
        // Add any analytics destinations we want to use to the Analytics singleton.
 | 
						|
        Analytics::getInstance()->addDestination (new GoogleAnalyticsDestination());
 | 
						|
 | 
						|
        // The event type here should probably be DemoAnalyticsEventTypes::sessionStart
 | 
						|
        // in a more advanced app.
 | 
						|
        Analytics::getInstance()->logEvent ("startup", {}, DemoAnalyticsEventTypes::event);
 | 
						|
 | 
						|
        crashButton.onClick = [this] { sendCrash(); };
 | 
						|
 | 
						|
        addAndMakeVisible (eventButton);
 | 
						|
        addAndMakeVisible (crashButton);
 | 
						|
 | 
						|
        setSize (300, 200);
 | 
						|
 | 
						|
        StringPairArray logButtonPressParameters;
 | 
						|
        logButtonPressParameters.set ("id", "a");
 | 
						|
        logEventButtonPress.reset (new ButtonTracker (eventButton, "button_press", logButtonPressParameters));
 | 
						|
    }
 | 
						|
 | 
						|
    ~AnalyticsCollectionDemo() override
 | 
						|
    {
 | 
						|
        // The event type here should probably be DemoAnalyticsEventTypes::sessionEnd
 | 
						|
        // in a more advanced app.
 | 
						|
        Analytics::getInstance()->logEvent ("shutdown", {}, DemoAnalyticsEventTypes::event);
 | 
						|
    }
 | 
						|
 | 
						|
    void paint (Graphics& g) override
 | 
						|
    {
 | 
						|
        g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
 | 
						|
    }
 | 
						|
 | 
						|
    void resized() override
 | 
						|
    {
 | 
						|
        eventButton.centreWithSize (100, 40);
 | 
						|
        eventButton.setBounds (eventButton.getBounds().translated (0, 25));
 | 
						|
        crashButton.setBounds (eventButton.getBounds().translated (0, -50));
 | 
						|
    }
 | 
						|
 | 
						|
private:
 | 
						|
    //==============================================================================
 | 
						|
    void sendCrash()
 | 
						|
    {
 | 
						|
        // In a more advanced application you would probably use a different event
 | 
						|
        // type here.
 | 
						|
        Analytics::getInstance()->logEvent ("crash", {}, DemoAnalyticsEventTypes::event);
 | 
						|
        Analytics::getInstance()->getDestinations().clear();
 | 
						|
        JUCEApplication::getInstance()->shutdown();
 | 
						|
    }
 | 
						|
 | 
						|
    TextButton eventButton { "Press me!" }, crashButton { "Simulate crash!" };
 | 
						|
    std::unique_ptr<ButtonTracker> logEventButtonPress;
 | 
						|
 | 
						|
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (AnalyticsCollectionDemo)
 | 
						|
};
 |