I followed the ‘Analytics for beginners and small businesses | Google Analytics | Google for Developers’ guide to setup The Google Analytics 4 Measurement Protocol.
Only Step 1: Part 1: Set up a property
Create new Google Analytics 4 Property
Add new Data Stream (Web)
Get your Measurement ID (“apikey”) G-XXXXXXXXXX and generate new ‘secret key’ in Data Stream settings.
Add the Google gtag to your product website (I used my router http ‘Hello world’ HTML page). Google also support website builders etc.
Test and verify Google Analytics is receiving Measurement Protocol events e.g. by CURLing with these examples ส่งเหตุการณ์ Measurement Protocol ไปยัง Google Analytics | Google for Developers . According to Google this can take up to 48 hours.
Use this slightly modified AnalyticsCollectionTutorial.h. Plug in your G-XXXXXXXXXX apikey and secret key into corresponding variables. Do something with ‘setUserId’.
#pragma once
enum DemoAnalyticsEventTypes
{
event,
sessionStart,
sessionEnd,
screenView,
exception
};
//==============================================================================
class GoogleAnalyticsDestination : public juce::ThreadedAnalyticsDestination
{
public:
GoogleAnalyticsDestination()
: ThreadedAnalyticsDestination("GoogleAnalyticsThread")
{
{
// Choose where to save any unsent events.
auto appDataDir = juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory)
.getChildFile(juce::JUCEApplication::getInstance()->getApplicationName()); // [1]
if (!appDataDir.exists())
appDataDir.createDirectory(); // [2]
savedEventsFile = appDataDir.getChildFile("analytics_events.xml"); // [3]
}
{
// 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 = "G-XXXXXXXXXXXX";
secret = "XXXXXXXXXXXXXXXXXXXXX";
}
startAnalyticsThread(initialPeriodMs); // [4]
}
~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!
juce::Thread::sleep(initialPeriodMs); // [5]
stopAnalyticsThread(1000); // [6]
}
int getMaximumBatchSize() override
{
return 20;
}
bool logBatchedEvents(const juce::Array<AnalyticsEvent>& events) override
{
juce::StringArray postData;
for (auto& event : events)
{
juce::DynamicObject::Ptr eventData = new juce::DynamicObject();
eventData->setProperty("client_id", event.userID);
juce::DynamicObject::Ptr eventDetails = new juce::DynamicObject();
if (event.eventType == DemoAnalyticsEventTypes::event)
{
if (event.name == "startup")
{
eventDetails->setProperty("name", "app_started");
}
else if (event.name == "shutdown")
{
eventDetails->setProperty("name", "app_stopped");
}
else if (event.name == "button_press")
{
eventDetails->setProperty("name", "button_press");
juce::DynamicObject::Ptr params = new juce::DynamicObject();
params->setProperty("id", event.parameters["id"]);
eventDetails->setProperty("params", juce::var(params.get()));
}
else if (event.name == "crash")
{
eventDetails->setProperty("name", "app_crash");
}
else
{
jassertfalse;
continue;
}
}
else
{
jassertfalse;
continue;
}
juce::Array<juce::var> eventsArray;
eventsArray.add(juce::var(eventDetails.get()));
eventData->setProperty("events", eventsArray);
juce::String jsonString = juce::JSON::toString(juce::var(eventData));
postData.add(jsonString);
}
juce::String baseURL = "https://www.google-analytics.com/mp/collect?";
auto url = juce::URL(baseURL)
.withParameter("measurement_id", apiKey)
.withParameter("api_secret", secret)
.withPOSTData(postData.joinIntoString("\r\n"));
{
const juce::ScopedLock lock(webStreamCreation);
if (shouldExit)
return false;
webStream.reset(new juce::WebInputStream(url, false));
}
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 juce::ScopedLock lock(webStreamCreation); // [1]
shouldExit = true; // [2]
if (webStream.get() != nullptr) // [3]
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!
juce::XmlDocument previouslySavedEvents(savedEventsFile);
std::unique_ptr<juce::XmlElement> xml(previouslySavedEvents.getDocumentElement()); // [1]
if (xml.get() == nullptr || xml->getTagName() != "events") // [2]
xml.reset(new juce::XmlElement("events"));
for (auto& event : eventsToSave)
{
auto* xmlEvent = new juce::XmlElement("google_analytics_event"); // [3]
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 juce::XmlElement("parameters"); // [4]
for (auto& key : event.parameters.getAllKeys())
parameters->setAttribute(key, event.parameters[key]);
xmlEvent->addChildElement(parameters);
auto* userProperties = new juce::XmlElement("user_properties"); // [5]
for (auto& key : event.userProperties.getAllKeys())
userProperties->setAttribute(key, event.userProperties[key]);
xmlEvent->addChildElement(userProperties);
xml->addChildElement(xmlEvent); // [6]
}
xml->writeTo(savedEventsFile); // [7]
}
void restoreUnloggedEvents(std::deque<AnalyticsEvent>& restoredEventQueue) override
{
juce::XmlDocument savedEvents(savedEventsFile);
std::unique_ptr<juce::XmlElement> xml(savedEvents.getDocumentElement()); // [1]
if (xml.get() == nullptr || xml->getTagName() != "events") // [2]
return;
auto numEvents = xml->getNumChildElements();
for (auto iEvent = 0; iEvent < numEvents; ++iEvent)
{
auto* xmlEvent = xml->getChildElement(iEvent); // [3]
juce::StringPairArray parameters;
auto* xmlParameters = xmlEvent->getChildByName("parameters"); // [4]
auto numParameters = xmlParameters->getNumAttributes();
for (auto iParam = 0; iParam < numParameters; ++iParam)
parameters.set(xmlParameters->getAttributeName(iParam), xmlParameters->getAttributeValue(iParam));
juce::StringPairArray userProperties;
auto* xmlUserProperties = xmlEvent->getChildByName("user_properties"); // [5]
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"), // [6]
xmlEvent->getIntAttribute("type"),
static_cast<juce::uint32>(xmlEvent->getIntAttribute("timestamp")),
parameters,
xmlEvent->getStringAttribute("user_id"),
userProperties}
);
}
savedEventsFile.deleteFile(); // [7]
}
const int initialPeriodMs = 1000;
int periodMs = initialPeriodMs;
juce::CriticalSection webStreamCreation;
bool shouldExit = false;
std::unique_ptr<juce::WebInputStream> webStream;
juce::String apiKey;
juce::String secret;
juce::File savedEventsFile;
};
//==============================================================================
class MainContentComponent : public juce::Component
{
public:
//==============================================================================
MainContentComponent()
{
// Add an analytics identifier for the user. Make sure you don't accidentally
// collect identifiable information if you haven't asked for permission!
juce::Analytics::getInstance()->setUserId("123456.7654321"); // [1]
// Add any other constant user information.
juce::StringPairArray userData;
userData.set("group", "beta");
juce::Analytics::getInstance()->setUserProperties(userData); // [2]
// Add any analytics destinations we want to use to the Analytics singleton.
juce::Analytics::getInstance()->addDestination(new GoogleAnalyticsDestination()); // [3]
// The event type here should probably be DemoAnalyticsEventTypes::sessionStart
// in a more advanced app.
juce::Analytics::getInstance()->logEvent("startup", {}, DemoAnalyticsEventTypes::event); // [4]
crashButton.onClick = [this] { sendCrash(); };
addAndMakeVisible(eventButton);
addAndMakeVisible(crashButton);
setSize(300, 200);
juce::StringPairArray logButtonPressParameters;
logButtonPressParameters.set("id", "a");
logEventButtonPress.reset(new juce::ButtonTracker(eventButton, "button_press", logButtonPressParameters)
); // [2]
}
~MainContentComponent() override
{
// The event type here should probably be DemoAnalyticsEventTypes::sessionEnd
// in a more advanced app.
juce::Analytics::getInstance()->logEvent("shutdown", {}, DemoAnalyticsEventTypes::event); // [5]
}
void paint(juce::Graphics& g) override
{
g.fillAll(getLookAndFeel().findColour(juce::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.
juce::Analytics::getInstance()->logEvent("crash", {}, DemoAnalyticsEventTypes::event);
juce::Analytics::getInstance()->getDestinations().clear();
juce::JUCEApplication::getInstance()->quit();
}
juce::TextButton eventButton{"Press me!"}, crashButton{"Simulate crash!"};
std::unique_ptr<juce::ButtonTracker> logEventButtonPress; // [1]
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainContentComponent)
};