summaryrefslogtreecommitdiff
path: root/meshmc/libraries/ganalytics
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:45:07 +0300
commit31b9a8949ed0a288143e23bf739f2eb64fdc63be (patch)
tree8a984fa143c38fccad461a77792d6864f3e82cd3 /meshmc/libraries/ganalytics
parent934382c8a1ce738589dee9ee0f14e1cec812770e (diff)
parentfad6a1066616b69d7f5fef01178efdf014c59537 (diff)
downloadProject-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.tar.gz
Project-Tick-31b9a8949ed0a288143e23bf739f2eb64fdc63be.zip
Add 'meshmc/' from commit 'fad6a1066616b69d7f5fef01178efdf014c59537'
git-subtree-dir: meshmc git-subtree-mainline: 934382c8a1ce738589dee9ee0f14e1cec812770e git-subtree-split: fad6a1066616b69d7f5fef01178efdf014c59537
Diffstat (limited to 'meshmc/libraries/ganalytics')
-rw-r--r--meshmc/libraries/ganalytics/CMakeLists.txt17
-rw-r--r--meshmc/libraries/ganalytics/LICENSE.txt24
-rw-r--r--meshmc/libraries/ganalytics/README.md34
-rw-r--r--meshmc/libraries/ganalytics/include/ganalytics.h92
-rw-r--r--meshmc/libraries/ganalytics/src/ganalytics.cpp262
-rw-r--r--meshmc/libraries/ganalytics/src/ganalytics_worker.cpp267
-rw-r--r--meshmc/libraries/ganalytics/src/ganalytics_worker.h84
7 files changed, 780 insertions, 0 deletions
diff --git a/meshmc/libraries/ganalytics/CMakeLists.txt b/meshmc/libraries/ganalytics/CMakeLists.txt
new file mode 100644
index 0000000000..a5c3125ddd
--- /dev/null
+++ b/meshmc/libraries/ganalytics/CMakeLists.txt
@@ -0,0 +1,17 @@
+project(ganalytics)
+
+find_package(Qt6Core)
+find_package(Qt6Gui)
+find_package(Qt6Network)
+
+set(ganalytics_SOURCES
+src/ganalytics.cpp
+src/ganalytics_worker.cpp
+src/ganalytics_worker.h
+include/ganalytics.h
+)
+
+add_library(ganalytics STATIC ${ganalytics_SOURCES})
+target_link_libraries(ganalytics Qt6::Core Qt6::Gui Qt6::Network)
+target_include_directories(ganalytics PUBLIC include)
+target_link_libraries(ganalytics systeminfo)
diff --git a/meshmc/libraries/ganalytics/LICENSE.txt b/meshmc/libraries/ganalytics/LICENSE.txt
new file mode 100644
index 0000000000..795497ffe6
--- /dev/null
+++ b/meshmc/libraries/ganalytics/LICENSE.txt
@@ -0,0 +1,24 @@
+Copyright (c) 2014-2015, University of Applied Sciences Augsburg
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ * Neither the name of the University of Applied Sciences Augsburg nor the
+ names of its contributors may be used to endorse or promote products
+ derived from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
+OODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+UT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/meshmc/libraries/ganalytics/README.md b/meshmc/libraries/ganalytics/README.md
new file mode 100644
index 0000000000..d7e1e33c7d
--- /dev/null
+++ b/meshmc/libraries/ganalytics/README.md
@@ -0,0 +1,34 @@
+qt-google-analytics
+================
+
+Qt5 classes for providing google analytics usage in a Qt/QML application.
+
+## Building
+Include ```qt-google-analytics.pri``` in your .pro file.
+
+## Using
+Please make sure you have set your application information using ```QApplication::setApplicationName``` and ```QApplication::setApplicationVersion```.
+
+### In C++:
+```
+GAnalytics tracker("UA-my-id");
+tracker.sendScreenView("Main Screen");
+```
+
+### In QtQuick:
+Register the class on the C++ side using ```qmlRegisterType<GAnalytics>("analytics", 0, 1, "Tracker");```
+```
+Tracker {
+ id: tracker
+ trackingID: "UA-my-id"
+}
+
+[...]
+tracker.sendScreenView("Main Screen")
+```
+
+There is also an example application in the examples folder.
+
+## License
+Copyright (c) 2014-2016, University of Applied Sciences Augsburg.
+All rights reserved. Distributed under the terms and conditions of the BSD License. See separate LICENSE.txt.
diff --git a/meshmc/libraries/ganalytics/include/ganalytics.h b/meshmc/libraries/ganalytics/include/ganalytics.h
new file mode 100644
index 0000000000..8c6550a922
--- /dev/null
+++ b/meshmc/libraries/ganalytics/include/ganalytics.h
@@ -0,0 +1,92 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QObject>
+#include <QVariantMap>
+
+class QNetworkAccessManager;
+class GAnalyticsWorker;
+
+class GAnalytics : public QObject
+{
+ Q_OBJECT
+ Q_ENUMS(LogLevel)
+
+ public:
+ explicit GAnalytics(const QString& trackingID, const QString& clientID,
+ const int version, QObject* parent = 0);
+ ~GAnalytics();
+
+ public:
+ enum LogLevel { Debug, Info, Error };
+
+ int version();
+
+ void setLogLevel(LogLevel logLevel);
+ LogLevel logLevel() const;
+
+ // Getter and Setters
+ void setViewportSize(const QString& viewportSize);
+ QString viewportSize() const;
+
+ void setLanguage(const QString& language);
+ QString language() const;
+
+ void setAnonymizeIPs(bool anonymize);
+ bool anonymizeIPs();
+
+ void setSendInterval(int milliseconds);
+ int sendInterval() const;
+
+ void enable(bool state = true);
+ bool isEnabled();
+
+ /// Get or set the network access manager. If none is set, the class creates
+ /// its own on the first request
+ void setNetworkAccessManager(QNetworkAccessManager* networkAccessManager);
+ QNetworkAccessManager* networkAccessManager() const;
+
+ public slots:
+ void sendScreenView(const QString& screenName,
+ const QVariantMap& customValues = QVariantMap());
+ void sendEvent(const QString& category, const QString& action,
+ const QString& label = QString(),
+ const QVariant& value = QVariant(),
+ const QVariantMap& customValues = QVariantMap());
+ void sendException(const QString& exceptionDescription,
+ bool exceptionFatal = true,
+ const QVariantMap& customValues = QVariantMap());
+ void startSession();
+ void endSession();
+
+ private:
+ GAnalyticsWorker* d;
+
+ friend QDataStream& operator<<(QDataStream& outStream,
+ const GAnalytics& analytics);
+ friend QDataStream& operator>>(QDataStream& inStream,
+ GAnalytics& analytics);
+};
+
+QDataStream& operator<<(QDataStream& outStream, const GAnalytics& analytics);
+QDataStream& operator>>(QDataStream& inStream, GAnalytics& analytics);
diff --git a/meshmc/libraries/ganalytics/src/ganalytics.cpp b/meshmc/libraries/ganalytics/src/ganalytics.cpp
new file mode 100644
index 0000000000..fe4b7b77b0
--- /dev/null
+++ b/meshmc/libraries/ganalytics/src/ganalytics.cpp
@@ -0,0 +1,262 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ganalytics.h"
+#include "ganalytics_worker.h"
+#include "sys.h"
+
+#include <QDataStream>
+#include <QDebug>
+#include <QLocale>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+#include <QNetworkRequest>
+#include <QQueue>
+#include <QSettings>
+#include <QTimer>
+#include <QUrlQuery>
+#include <QUuid>
+
+GAnalytics::GAnalytics(const QString& trackingID, const QString& clientID,
+ const int version, QObject* parent)
+ : QObject(parent)
+{
+ d = new GAnalyticsWorker(this);
+ d->m_trackingID = trackingID;
+ d->m_clientID = clientID;
+ d->m_version = version;
+}
+
+/**
+ * Destructor of class GAnalytics.
+ */
+GAnalytics::~GAnalytics()
+{
+ delete d;
+}
+
+void GAnalytics::setLogLevel(GAnalytics::LogLevel logLevel)
+{
+ d->m_logLevel = logLevel;
+}
+
+GAnalytics::LogLevel GAnalytics::logLevel() const
+{
+ return d->m_logLevel;
+}
+
+// SETTER and GETTER
+void GAnalytics::setViewportSize(const QString& viewportSize)
+{
+ d->m_viewportSize = viewportSize;
+}
+
+QString GAnalytics::viewportSize() const
+{
+ return d->m_viewportSize;
+}
+
+void GAnalytics::setLanguage(const QString& language)
+{
+ d->m_language = language;
+}
+
+QString GAnalytics::language() const
+{
+ return d->m_language;
+}
+
+void GAnalytics::setAnonymizeIPs(bool anonymize)
+{
+ d->m_anonymizeIPs = anonymize;
+}
+
+bool GAnalytics::anonymizeIPs()
+{
+ return d->m_anonymizeIPs;
+}
+
+void GAnalytics::setSendInterval(int milliseconds)
+{
+ d->m_timer.setInterval(milliseconds);
+}
+
+int GAnalytics::sendInterval() const
+{
+ return (d->m_timer.interval());
+}
+
+bool GAnalytics::isEnabled()
+{
+ return d->m_isEnabled;
+}
+
+void GAnalytics::enable(bool state)
+{
+ d->enable(state);
+}
+
+int GAnalytics::version()
+{
+ return d->m_version;
+}
+
+void GAnalytics::setNetworkAccessManager(
+ QNetworkAccessManager* networkAccessManager)
+{
+ if (d->networkManager != networkAccessManager) {
+ // Delete the old network manager if it was our child
+ if (d->networkManager && d->networkManager->parent() == this) {
+ d->networkManager->deleteLater();
+ }
+
+ d->networkManager = networkAccessManager;
+ }
+}
+
+QNetworkAccessManager* GAnalytics::networkAccessManager() const
+{
+ return d->networkManager;
+}
+
+static void appendCustomValues(QUrlQuery& query,
+ const QVariantMap& customValues)
+{
+ for (QVariantMap::const_iterator iter = customValues.begin();
+ iter != customValues.end(); ++iter) {
+ query.addQueryItem(iter.key(), iter.value().toString());
+ }
+}
+
+/**
+ * Sent screen view is called when the user changed the applications view.
+ * These action of the user should be noticed and reported. Therefore
+ * a QUrlQuery is build in this method. It holts all the parameter for
+ * a http POST. The UrlQuery will be stored in a message Queue.
+ */
+void GAnalytics::sendScreenView(const QString& screenName,
+ const QVariantMap& customValues)
+{
+ d->logMessage(Info, QString("ScreenView: %1").arg(screenName));
+
+ QUrlQuery query = d->buildStandardPostQuery("screenview");
+ query.addQueryItem("cd", screenName);
+ query.addQueryItem("an", d->m_appName);
+ query.addQueryItem("av", d->m_appVersion);
+ appendCustomValues(query, customValues);
+
+ d->enqueQueryWithCurrentTime(query);
+}
+
+/**
+ * This method is called whenever a button was pressed in the application.
+ * A query for a POST message will be created to report this event. The
+ * created query will be stored in a message queue.
+ */
+void GAnalytics::sendEvent(const QString& category, const QString& action,
+ const QString& label, const QVariant& value,
+ const QVariantMap& customValues)
+{
+ QUrlQuery query = d->buildStandardPostQuery("event");
+ query.addQueryItem("an", d->m_appName);
+ query.addQueryItem("av", d->m_appVersion);
+ query.addQueryItem("ec", category);
+ query.addQueryItem("ea", action);
+ if (!label.isEmpty())
+ query.addQueryItem("el", label);
+ if (value.isValid())
+ query.addQueryItem("ev", value.toString());
+
+ appendCustomValues(query, customValues);
+
+ d->enqueQueryWithCurrentTime(query);
+}
+
+/**
+ * Method is called after an exception was raised. It builds a
+ * query for a POST message. These query will be stored in a
+ * message queue.
+ */
+void GAnalytics::sendException(const QString& exceptionDescription,
+ bool exceptionFatal,
+ const QVariantMap& customValues)
+{
+ QUrlQuery query = d->buildStandardPostQuery("exception");
+ query.addQueryItem("an", d->m_appName);
+ query.addQueryItem("av", d->m_appVersion);
+
+ query.addQueryItem("exd", exceptionDescription);
+
+ if (exceptionFatal) {
+ query.addQueryItem("exf", "1");
+ } else {
+ query.addQueryItem("exf", "0");
+ }
+ appendCustomValues(query, customValues);
+
+ d->enqueQueryWithCurrentTime(query);
+}
+
+/**
+ * Session starts. This event will be sent by a POST message.
+ * Query is setup in this method and stored in the message
+ * queue.
+ */
+void GAnalytics::startSession()
+{
+ QVariantMap customValues;
+ customValues.insert("sc", "start");
+ sendEvent("Session", "Start", QString(), QVariant(), customValues);
+}
+
+/**
+ * Session ends. This event will be sent by a POST message.
+ * Query is setup in this method and stored in the message
+ * queue.
+ */
+void GAnalytics::endSession()
+{
+ QVariantMap customValues;
+ customValues.insert("sc", "end");
+ sendEvent("Session", "End", QString(), QVariant(), customValues);
+}
+
+/**
+ * Qut stream to persist class GAnalytics.
+ */
+QDataStream& operator<<(QDataStream& outStream, const GAnalytics& analytics)
+{
+ outStream << analytics.d->persistMessageQueue();
+
+ return outStream;
+}
+
+/**
+ * In stream to read GAnalytics from file.
+ */
+QDataStream& operator>>(QDataStream& inStream, GAnalytics& analytics)
+{
+ QList<QString> dataList;
+ inStream >> dataList;
+ analytics.d->readMessagesFromFile(dataList);
+
+ return inStream;
+}
diff --git a/meshmc/libraries/ganalytics/src/ganalytics_worker.cpp b/meshmc/libraries/ganalytics/src/ganalytics_worker.cpp
new file mode 100644
index 0000000000..115b39d5d4
--- /dev/null
+++ b/meshmc/libraries/ganalytics/src/ganalytics_worker.cpp
@@ -0,0 +1,267 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#include "ganalytics.h"
+#include "ganalytics_worker.h"
+#include "sys.h"
+
+#include <QCoreApplication>
+#include <QNetworkAccessManager>
+#include <QNetworkReply>
+
+#include <QGuiApplication>
+#include <QScreen>
+
+const QLatin1String
+ GAnalyticsWorker::dateTimeFormat("yyyy,MM,dd-hh:mm::ss:zzz");
+
+GAnalyticsWorker::GAnalyticsWorker(GAnalytics* parent)
+ : QObject(parent), q(parent), m_logLevel(GAnalytics::Error)
+{
+ m_appName = QCoreApplication::instance()->applicationName();
+ m_appVersion = QCoreApplication::instance()->applicationVersion();
+ m_request.setUrl(QUrl("https://www.google-analytics.com/collect"));
+ m_request.setHeader(QNetworkRequest::ContentTypeHeader,
+ "application/x-www-form-urlencoded");
+ m_request.setHeader(QNetworkRequest::UserAgentHeader, getUserAgent());
+
+ m_language = QLocale::system().name().toLower().replace("_", "-");
+ m_screenResolution = getScreenResolution();
+
+ m_timer.setInterval(m_timerInterval);
+ connect(&m_timer, &QTimer::timeout, this, &GAnalyticsWorker::postMessage);
+}
+
+void GAnalyticsWorker::enable(bool state)
+{
+ // state change to the same is not valid.
+ if (m_isEnabled == state) {
+ return;
+ }
+
+ m_isEnabled = state;
+ if (m_isEnabled) {
+ // enable -> start doing things :)
+ m_timer.start();
+ } else {
+ // disable -> stop the timer
+ m_timer.stop();
+ }
+}
+
+void GAnalyticsWorker::logMessage(GAnalytics::LogLevel level,
+ const QString& message)
+{
+ if (m_logLevel > level) {
+ return;
+ }
+
+ qDebug() << "[Analytics]" << message;
+}
+
+/**
+ * Build the POST query. Adds all parameter to the query
+ * which are used in every POST.
+ * @param type Type of POST message. The event which is to post.
+ * @return query Most used parameter in a query for a POST.
+ */
+QUrlQuery GAnalyticsWorker::buildStandardPostQuery(const QString& type)
+{
+ QUrlQuery query;
+ query.addQueryItem("v", "1");
+ query.addQueryItem("tid", m_trackingID);
+ query.addQueryItem("cid", m_clientID);
+ if (!m_userID.isEmpty()) {
+ query.addQueryItem("uid", m_userID);
+ }
+ query.addQueryItem("t", type);
+ query.addQueryItem("ul", m_language);
+ query.addQueryItem("vp", m_viewportSize);
+ query.addQueryItem("sr", m_screenResolution);
+ if (m_anonymizeIPs) {
+ query.addQueryItem("aip", "1");
+ }
+ return query;
+}
+
+/**
+ * Get primary screen resolution.
+ * @return A QString like "800x600".
+ */
+QString GAnalyticsWorker::getScreenResolution()
+{
+ QScreen* screen = QGuiApplication::primaryScreen();
+ QSize size = screen->size();
+
+ return QString("%1x%2").arg(size.width()).arg(size.height());
+}
+
+/**
+ * Try to gain information about the system where this application
+ * is running. It needs to get the name and version of the operating
+ * system, the language and screen resolution.
+ * All this information will be send in POST messages.
+ * @return agent A QString with all the information formatted for a POST
+ * message.
+ */
+QString GAnalyticsWorker::getUserAgent()
+{
+ return QString("%1/%2").arg(m_appName).arg(m_appVersion);
+}
+
+/**
+ * The message queue contains a list of QueryBuffer object.
+ * QueryBuffer holds a QUrlQuery object and a QDateTime object.
+ * These both object are freed from the buffer object and
+ * inserted as QString objects in a QList.
+ * @return dataList The list with concartinated queue data.
+ */
+QList<QString> GAnalyticsWorker::persistMessageQueue()
+{
+ QList<QString> dataList;
+ foreach (QueryBuffer buffer, m_messageQueue) {
+ dataList << buffer.postQuery.toString();
+ dataList << buffer.time.toString(dateTimeFormat);
+ }
+ return dataList;
+}
+
+/**
+ * Reads persistent messages from a file.
+ * Gets all message data as a QList<QString>.
+ * Two lines in the list build a QueryBuffer object.
+ */
+void GAnalyticsWorker::readMessagesFromFile(const QList<QString>& dataList)
+{
+ QListIterator<QString> iter(dataList);
+ while (iter.hasNext()) {
+ QString queryString = iter.next();
+ QString dateString = iter.next();
+ QUrlQuery query;
+ query.setQuery(queryString);
+ QDateTime dateTime = QDateTime::fromString(dateString, dateTimeFormat);
+ QueryBuffer buffer;
+ buffer.postQuery = query;
+ buffer.time = dateTime;
+ m_messageQueue.enqueue(buffer);
+ }
+}
+
+/**
+ * Takes a QUrlQuery object and wrapp it together with
+ * a QTime object into a QueryBuffer struct. These struct
+ * will be stored in the message queue.
+ */
+void GAnalyticsWorker::enqueQueryWithCurrentTime(const QUrlQuery& query)
+{
+ QueryBuffer buffer;
+ buffer.postQuery = query;
+ buffer.time = QDateTime::currentDateTime();
+
+ m_messageQueue.enqueue(buffer);
+}
+
+/**
+ * This function is called by a timer interval.
+ * The function tries to send a messages from the queue.
+ * If message was successfully send then this function
+ * will be called back to send next message.
+ * If message queue contains more than one message then
+ * the connection will kept open.
+ * The message POST is asyncroniously when the server
+ * answered a signal will be emitted.
+ */
+void GAnalyticsWorker::postMessage()
+{
+ if (m_messageQueue.isEmpty()) {
+ // queue empty -> try sending later
+ m_timer.start();
+ return;
+ } else {
+ // queue has messages -> stop timer and start sending
+ m_timer.stop();
+ }
+
+ QString connection = "close";
+ if (m_messageQueue.count() > 1) {
+ connection = "keep-alive";
+ }
+
+ QueryBuffer buffer = m_messageQueue.head();
+ QDateTime sendTime = QDateTime::currentDateTime();
+ qint64 timeDiff = buffer.time.msecsTo(sendTime);
+
+ if (timeDiff > fourHours) {
+ // too old.
+ m_messageQueue.dequeue();
+ emit postMessage();
+ return;
+ }
+
+ buffer.postQuery.addQueryItem("qt", QString::number(timeDiff));
+ m_request.setRawHeader("Connection", connection.toUtf8());
+ m_request.setHeader(QNetworkRequest::ContentLengthHeader,
+ buffer.postQuery.toString().length());
+
+ logMessage(GAnalytics::Debug,
+ "Query string = " + buffer.postQuery.toString());
+
+ // Create a new network access manager if we don't have one yet
+ if (networkManager == NULL) {
+ networkManager = new QNetworkAccessManager(this);
+ }
+
+ QNetworkReply* reply = networkManager->post(
+ m_request, buffer.postQuery.query(QUrl::EncodeUnicode).toUtf8());
+ connect(reply, SIGNAL(finished()), this, SLOT(postMessageFinished()));
+}
+
+/**
+ * NetworkAccsessManager has finished to POST a message.
+ * If POST message was successfully send then the message
+ * query should be removed from queue.
+ * SIGNAL "postMessage" will be emitted to send next message
+ * if there is any.
+ * If message couldn't be send then next try is when the
+ * timer emits its signal.
+ */
+void GAnalyticsWorker::postMessageFinished()
+{
+ QNetworkReply* reply = qobject_cast<QNetworkReply*>(sender());
+
+ int httpStausCode =
+ reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
+ if (httpStausCode < 200 || httpStausCode > 299) {
+ logMessage(
+ GAnalytics::Error,
+ QString("Error posting message: %1").arg(reply->errorString()));
+
+ // An error ocurred. Try sending later.
+ m_timer.start();
+ return;
+ } else {
+ logMessage(GAnalytics::Debug, "Message sent");
+ }
+
+ m_messageQueue.dequeue();
+ postMessage();
+ reply->deleteLater();
+}
diff --git a/meshmc/libraries/ganalytics/src/ganalytics_worker.h b/meshmc/libraries/ganalytics/src/ganalytics_worker.h
new file mode 100644
index 0000000000..8acace3759
--- /dev/null
+++ b/meshmc/libraries/ganalytics/src/ganalytics_worker.h
@@ -0,0 +1,84 @@
+/* SPDX-FileCopyrightText: 2026 Project Tick
+ * SPDX-FileContributor: Project Tick
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ * MeshMC - A Custom Launcher for Minecraft
+ * Copyright (C) 2026 Project Tick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+#include <QUrlQuery>
+#include <QDateTime>
+#include <QTimer>
+#include <QNetworkRequest>
+#include <QQueue>
+
+struct QueryBuffer {
+ QUrlQuery postQuery;
+ QDateTime time;
+};
+
+class GAnalyticsWorker : public QObject
+{
+ Q_OBJECT
+
+ public:
+ explicit GAnalyticsWorker(GAnalytics* parent = 0);
+
+ GAnalytics* q;
+
+ QNetworkAccessManager* networkManager = nullptr;
+
+ QQueue<QueryBuffer> m_messageQueue;
+ QTimer m_timer;
+ QNetworkRequest m_request;
+ GAnalytics::LogLevel m_logLevel;
+
+ QString m_trackingID;
+ QString m_clientID;
+ QString m_userID;
+ QString m_appName;
+ QString m_appVersion;
+ QString m_language;
+ QString m_screenResolution;
+ QString m_viewportSize;
+
+ bool m_anonymizeIPs = false;
+ bool m_isEnabled = false;
+ int m_timerInterval = 30000;
+ int m_version = 0;
+
+ const static int fourHours = 4 * 60 * 60 * 1000;
+ const static QLatin1String dateTimeFormat;
+
+ public:
+ void logMessage(GAnalytics::LogLevel level, const QString& message);
+
+ QUrlQuery buildStandardPostQuery(const QString& type);
+ QString getScreenResolution();
+ QString getUserAgent();
+ QList<QString> persistMessageQueue();
+ void readMessagesFromFile(const QList<QString>& dataList);
+
+ void enqueQueryWithCurrentTime(const QUrlQuery& query);
+ void setIsSending(bool doSend);
+ void enable(bool state);
+
+ public slots:
+ void postMessage();
+ void postMessageFinished();
+};