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:
parent
55a917ebe5
commit
413164f46a
60 changed files with 11508 additions and 0 deletions
217
examples/AnalyticsCollection/Source/GoogleAnalyticsDestination.h
Normal file
217
examples/AnalyticsCollection/Source/GoogleAnalyticsDestination.h
Normal 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;
|
||||
};
|
||||
106
examples/AnalyticsCollection/Source/Main.cpp
Normal file
106
examples/AnalyticsCollection/Source/Main.cpp
Normal 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)
|
||||
38
examples/AnalyticsCollection/Source/MainComponent.h
Normal file
38
examples/AnalyticsCollection/Source/MainComponent.h
Normal 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)
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue