1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-31 03:00:05 +00:00

Analytics: Added an new analytics module

This commit is contained in:
tpoole 2017-10-23 13:52:30 +01:00
parent 55a917ebe5
commit 413164f46a
60 changed files with 11508 additions and 0 deletions

View file

@ -0,0 +1,217 @@
#include "../JuceLibraryCode/JuceHeader.h"
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()
{
// 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&tid=" + apiKey + "&t=event&");
StringArray postData;
for (auto& event : events)
{
StringPairArray data;
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
{
continue;
}
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 = new WebInputStream (url, true);
}
const 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 != 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!
XmlDocument previouslySavedEvents (savedEventsFile);
ScopedPointer<XmlElement> xml = previouslySavedEvents.getDocumentElement();
if (xml == nullptr || xml->getTagName() != "events")
xml = new XmlElement ("events");
for (auto& event : eventsToSave)
{
auto* xmlEvent = new XmlElement ("google_analytics_event");
xmlEvent->setAttribute ("name", event.name);
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 restoreUnloggedEvents (std::deque<AnalyticsEvent>& restoredEventQueue) override
{
XmlDocument savedEvents (savedEventsFile);
ScopedPointer<XmlElement> xml = savedEvents.getDocumentElement();
if (xml == nullptr || xml->getTagName() != "events")
return;
const auto numEvents = xml->getNumChildElements();
for (auto iEvent = 0; iEvent < numEvents; ++iEvent)
{
const auto* xmlEvent = xml->getChildElement (iEvent);
StringPairArray parameters;
const auto* xmlParameters = xmlEvent->getChildByName ("parameters");
const auto numParameters = xmlParameters->getNumAttributes();
for (auto iParam = 0; iParam < numParameters; ++iParam)
parameters.set (xmlParameters->getAttributeName (iParam),
xmlParameters->getAttributeValue (iParam));
StringPairArray userProperties;
const auto* xmlUserProperties = xmlEvent->getChildByName ("user_properties");
const 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"),
(uint32) xmlEvent->getIntAttribute ("timestamp"),
parameters,
xmlEvent->getStringAttribute ("user_id"),
userProperties
});
}
savedEventsFile.deleteFile();
}
const int initialPeriodMs = 1000;
int periodMs = initialPeriodMs;
CriticalSection webStreamCreation;
bool shouldExit = false;
ScopedPointer<WebInputStream> webStream;
String apiKey;
File savedEventsFile;
};

View file

@ -0,0 +1,106 @@
#include "../JuceLibraryCode/JuceHeader.h"
#include "GoogleAnalyticsDestination.h"
#include "MainComponent.h"
//==============================================================================
class AnalyticsCollectionApplication : public JUCEApplication
{
public:
//==============================================================================
AnalyticsCollectionApplication() {}
const String getApplicationName() override { return ProjectInfo::projectName; }
const String getApplicationVersion() override { return ProjectInfo::versionString; }
bool moreThanOneInstanceAllowed() override { return true; }
//==============================================================================
void initialise (const String&) override
{
// Add an analytics identifier for the user. Make sure you don't collect
// identifiable information accidentally 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());
Analytics::getInstance()->logEvent ("startup", {});
mainWindow = new MainWindow (getApplicationName());
}
void shutdown() override
{
Analytics::getInstance()->logEvent ("shutdown", {});
// Add your application's shutdown code here..
mainWindow = nullptr; // (deletes our window)
}
//==============================================================================
void systemRequestedQuit() override
{
// This is called when the app is being asked to quit: you can ignore this
// request and let the app carry on running, or call quit() to allow the app to close.
quit();
}
void anotherInstanceStarted (const String&) override
{
// When another instance of the app is launched while this one is running,
// this method is invoked, and the commandLine parameter tells you what
// the other instance's command-line arguments were.
}
//==============================================================================
/*
This class implements the desktop window that contains an instance of
our MainContentComponent class.
*/
class MainWindow : public DocumentWindow
{
public:
MainWindow (String name) : DocumentWindow (name,
Desktop::getInstance().getDefaultLookAndFeel()
.findColour (ResizableWindow::backgroundColourId),
DocumentWindow::allButtons)
{
setUsingNativeTitleBar (true);
setContentOwned (new MainContentComponent(), true);
centreWithSize (getWidth(), getHeight());
setVisible (true);
}
void closeButtonPressed() override
{
// This is called when the user tries to close this window. Here, we'll just
// ask the app to quit when this happens, but you can change this to do
// whatever you need.
JUCEApplication::getInstance()->systemRequestedQuit();
}
/* Note: Be careful if you override any DocumentWindow methods - the base
class uses a lot of them, so by overriding you might break its functionality.
It's best to do all your work in your content component instead, but if
you really have to override any DocumentWindow methods, make sure your
subclass also calls the superclass's method.
*/
private:
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainWindow)
};
private:
ScopedPointer<MainWindow> mainWindow;
};
//==============================================================================
// This macro generates the main() routine that launches the app.
START_JUCE_APPLICATION (AnalyticsCollectionApplication)

View file

@ -0,0 +1,38 @@
#pragma once
#include "../JuceLibraryCode/JuceHeader.h"
class MainContentComponent : public Component
{
public:
//==============================================================================
MainContentComponent()
{
addAndMakeVisible (eventButton);
setSize (300, 200);
StringPairArray logButtonPressParameters;
logButtonPressParameters.set ("id", "a");
logEventButtonPress = new ButtonTracker (eventButton, "button_press", logButtonPressParameters);
}
~MainContentComponent() {}
void paint (Graphics& g) override
{
g.fillAll (getLookAndFeel().findColour (ResizableWindow::backgroundColourId));
}
void resized() override
{
eventButton.centreWithSize (100, 50);
}
private:
//==============================================================================
TextButton eventButton { "Press me!" };
ScopedPointer<ButtonTracker> logEventButtonPress;
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainContentComponent)
};